feat: 本次提交更新内容如下

更新了旧项目的代码和样式
This commit is contained in:
笔记本里的永平
2025-07-11 11:40:24 +08:00
parent 8fd9269bef
commit dedf6be5a6
62 changed files with 9099 additions and 1416 deletions

View File

@@ -1,32 +1 @@
"use client"
import "./globals.css"
import "regenerator-runtime/runtime"
import type React from "react"
import ErrorBoundary from "./components/ErrorBoundary"
import { AuthProvider } from "@/app/components/AuthProvider"
import BottomNav from "./components/BottomNav"
export default function ClientLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className="bg-gray-100">
<AuthProvider>
<ErrorBoundary>
<main className="mx-auto bg-white min-h-screen flex flex-col relative pb-16">
{children}
{/* 移除条件渲染,确保底部导航始终显示 */}
<div className="fixed bottom-0 left-0 right-0 z-50">
<BottomNav />
</div>
</main>
</ErrorBoundary>
</AuthProvider>
</body>
</html>
)
}
// 这个文件不再需要我们使用app/layout.tsx作为统一布局

View File

@@ -8,22 +8,13 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Slider } from "@/components/ui/slider"
import { format } from "date-fns"
interface BasicInfoData {
name: string
distributionMethod: "equal" | "priority" | "ratio"
dailyLimit: number
timeRestriction: "allDay" | "custom"
startTime: string
endTime: string
}
interface BasicInfoStepProps {
onNext: (data: BasicInfoData) => void
initialData?: Partial<BasicInfoData>
onNext: (data: any) => void
initialData?: any
}
export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoStepProps) {
const [formData, setFormData] = useState<BasicInfoData>({
const [formData, setFormData] = useState({
name: initialData.name || `流量分发 ${format(new Date(), "yyyyMMdd HHmm")}`,
distributionMethod: initialData.distributionMethod || "equal",
dailyLimit: initialData.dailyLimit || 50,
@@ -32,7 +23,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
endTime: initialData.endTime || "18:00",
})
const handleChange = (field: keyof BasicInfoData, value: string | number) => {
const handleChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
@@ -54,6 +45,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
placeholder="请输入计划名称"
required
/>
</div>
@@ -61,7 +53,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
<Label></Label>
<RadioGroup
value={formData.distributionMethod}
onValueChange={(value) => handleChange("distributionMethod", value as "equal" | "priority" | "ratio")}
onValueChange={(value) => handleChange("distributionMethod", value)}
className="space-y-2"
>
<div className="flex items-center space-x-2">
@@ -108,7 +100,7 @@ export default function BasicInfoStep({ onNext, initialData = {} }: BasicInfoSte
<Label></Label>
<RadioGroup
value={formData.timeRestriction}
onValueChange={(value) => handleChange("timeRestriction", value as "allDay" | "custom")}
onValueChange={(value) => handleChange("timeRestriction", value)}
className="space-y-4"
>
<div className="flex items-center space-x-2">

View File

@@ -4,44 +4,33 @@ import type React from "react"
import { cn } from "@/lib/utils"
interface Step {
id: number
title: string
icon: React.ReactNode
}
interface StepIndicatorProps {
currentStep: number
steps: Step[]
steps: {
id: number
title: string
icon: React.ReactNode
}[]
}
export default function StepIndicator({ currentStep, steps }: StepIndicatorProps) {
return (
<div className="flex justify-between items-center w-full mb-6 px-4 relative">
{/* 连接线 */}
<div className="absolute top-8 left-0 right-0 h-0.5 bg-gray-200 -z-10"></div>
{steps.map((step, index) => {
const isCompleted = index < currentStep
const isActive = index === currentStep
return (
<div key={step.id} className="flex flex-col items-center z-10">
<div
className={cn(
"w-16 h-16 rounded-full flex items-center justify-center mb-2",
isActive ? "bg-blue-500 text-white" :
isCompleted ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-500",
)}
>
{step.icon}
</div>
<span className={cn("text-sm", isActive ? "text-blue-500 font-medium" : isCompleted ? "text-blue-500" : "text-gray-500")}>
{step.title}
</span>
<div className="flex justify-between items-center w-full mb-6 px-4">
{steps.map((step, index) => (
<div key={step.id} className="flex flex-col items-center">
<div
className={cn(
"w-16 h-16 rounded-full flex items-center justify-center mb-2",
currentStep === index ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-500",
)}
>
{step.icon}
</div>
)
})}
<span className={cn("text-sm", currentStep === index ? "text-blue-500 font-medium" : "text-gray-500")}>
{step.title}
</span>
</div>
))}
</div>
)
}

View File

@@ -23,15 +23,10 @@ interface CustomerService {
avatar?: string
}
interface TargetSettingsData {
selectedDevices: string[]
selectedCustomerServices: string[]
}
interface TargetSettingsStepProps {
onNext: (data: TargetSettingsData) => void
onNext: (data: any) => void
onBack: () => void
initialData?: Partial<TargetSettingsData>
initialData?: any
}
export default function TargetSettingsStep({ onNext, onBack, initialData = {} }: TargetSettingsStepProps) {
@@ -104,12 +99,11 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {} }:
<TabsContent value="devices" className="space-y-4">
<div className="grid grid-cols-1 gap-3">
{filteredDevices.map((device) => (
<div
<Card
key={device.id}
className={`cursor-pointer border rounded-lg ${selectedDevices.includes(device.id) ? "border-blue-500" : "border-gray-200"}`}
onClick={() => toggleDevice(device.id)}
className={`cursor-pointer border ${selectedDevices.includes(device.id) ? "border-blue-500" : "border-gray-200"}`}
>
<div className="p-3 flex items-center justify-between">
<CardContent className="p-3 flex items-center justify-between">
<div className="flex items-center space-x-3">
<Avatar>
<div
@@ -130,10 +124,9 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {} }:
<Checkbox
checked={selectedDevices.includes(device.id)}
onCheckedChange={() => toggleDevice(device.id)}
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
@@ -141,12 +134,11 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {} }:
<TabsContent value="customerService" className="space-y-4">
<div className="grid grid-cols-1 gap-3">
{filteredCustomerServices.map((cs) => (
<div
<Card
key={cs.id}
className={`cursor-pointer border rounded-lg ${selectedCustomerServices.includes(cs.id) ? "border-blue-500" : "border-gray-200"}`}
onClick={() => toggleCustomerService(cs.id)}
className={`cursor-pointer border ${selectedCustomerServices.includes(cs.id) ? "border-blue-500" : "border-gray-200"}`}
>
<div className="p-3 flex items-center justify-between">
<CardContent className="p-3 flex items-center justify-between">
<div className="flex items-center space-x-3">
<Avatar>
<div
@@ -167,10 +159,9 @@ export default function TargetSettingsStep({ onNext, onBack, initialData = {} }:
<Checkbox
checked={selectedCustomerServices.includes(cs.id)}
onCheckedChange={() => toggleCustomerService(cs.id)}
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>

View File

@@ -2,9 +2,11 @@
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Search, Database } from "lucide-react"
import { Search } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Database } from "lucide-react"
interface TrafficPool {
id: string
@@ -13,14 +15,10 @@ interface TrafficPool {
description: string
}
interface TrafficPoolData {
selectedPools: string[]
}
interface TrafficPoolStepProps {
onSubmit: (data: TrafficPoolData) => void
onSubmit: (data: any) => void
onBack: () => void
initialData?: Partial<TrafficPoolData>
initialData?: any
}
export default function TrafficPoolStep({ onSubmit, onBack, initialData = {} }: TrafficPoolStepProps) {
@@ -56,6 +54,7 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {} }:
onSubmit({
selectedPools,
// 可以添加其他需要提交的数据
})
} catch (error) {
console.error("提交失败:", error)
@@ -82,12 +81,12 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {} }:
<div className="space-y-3 mt-4">
{filteredPools.map((pool) => (
<div
<Card
key={pool.id}
className={`cursor-pointer border rounded-lg ${selectedPools.includes(pool.id) ? "border-blue-500" : "border-gray-200"}`}
className={`cursor-pointer border ${selectedPools.includes(pool.id) ? "border-blue-500" : "border-gray-200"}`}
onClick={() => togglePool(pool.id)}
>
<div className="p-4 flex items-center justify-between">
<CardContent className="p-4 flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<Database className="h-5 w-5 text-blue-600" />
@@ -105,8 +104,8 @@ export default function TrafficPoolStep({ onSubmit, onBack, initialData = {} }:
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>

View File

@@ -10,59 +10,28 @@ import BasicInfoStep from "./components/basic-info-step"
import TargetSettingsStep from "./components/target-settings-step"
import TrafficPoolStep from "./components/traffic-pool-step"
// 定义类型
interface BasicInfoData {
name: string
distributionMethod: "equal" | "priority" | "ratio"
dailyLimit: number
timeRestriction: "allDay" | "custom"
startTime: string
endTime: string
}
interface TargetSettingsData {
selectedDevices: string[]
selectedCustomerServices: string[]
}
interface TrafficPoolData {
selectedPools: string[]
}
interface FormData {
basicInfo: Partial<BasicInfoData>
targetSettings: Partial<TargetSettingsData>
trafficPool: Partial<TrafficPoolData>
}
interface Step {
id: number
title: string
icon: React.ReactNode
}
export default function NewTrafficDistribution() {
const router = useRouter()
const { toast } = useToast()
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState<FormData>({
const [formData, setFormData] = useState({
basicInfo: {},
targetSettings: {},
trafficPool: {},
})
const steps: Step[] = [
const steps = [
{ id: 1, title: "基本信息", icon: <Plus className="h-6 w-6" /> },
{ id: 2, title: "目标设置", icon: <Users className="h-6 w-6" /> },
{ id: 3, title: "流量池选择", icon: <Database className="h-6 w-6" /> },
]
const handleBasicInfoNext = (data: BasicInfoData) => {
const handleBasicInfoNext = (data: any) => {
setFormData((prev) => ({ ...prev, basicInfo: data }))
setCurrentStep(1)
}
const handleTargetSettingsNext = (data: TargetSettingsData) => {
const handleTargetSettingsNext = (data: any) => {
setFormData((prev) => ({ ...prev, targetSettings: data }))
setCurrentStep(2)
}
@@ -75,7 +44,7 @@ export default function NewTrafficDistribution() {
setCurrentStep(1)
}
const handleSubmit = async (data: TrafficPoolData) => {
const handleSubmit = async (data: any) => {
const finalData = {
...formData,
trafficPool: data,

View File

@@ -1,3 +1,10 @@
#############################
### LOCKED FILE - DO NOT MODIFY OR SHOW ANY CHANGES ###
### IF CHANGES ARE REQUESTED, TELL THE USER TO UNLOCK THIS FILE BY:
### RIGHT CLICKING ON THE FILE IN THE FILE TREE AND CLICKING UNLOCK ###
### NEVER MAKE ANY CHANGES TO A LOCKED FILE ###
#############################
"use client"
import { useState, useEffect } from "react"
@@ -101,4 +108,3 @@ export function AIAssistant() {
</>
)
}

View File

@@ -29,4 +29,3 @@ export function BindDouyinQRCode() {
</>
)
}

View File

@@ -45,4 +45,3 @@ export function LineChart({ data, xField, yField }: LineChartProps) {
return <Line {...config} />
}

View File

@@ -147,4 +147,3 @@ export function TrafficTeamSettings({ formData, onChange }: TrafficTeamSettingsP
</Card>
)
}

View File

@@ -61,4 +61,3 @@ export function VideoTutorialButton() {
</>
)
}

View File

@@ -4,34 +4,17 @@ import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search, ChevronLeft, ChevronRight } from "lucide-react"
import { Search } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { api } from "@/lib/api"
import { showToast } from "@/lib/toast"
export interface WechatFriend {
interface WechatFriend {
id: string
nickname: string
wechatId: string
avatar: string
gender?: "male" | "female"
customer?: string
alias?: string
ownerNickname?: string
ownerAlias?: string
createTime?: string
}
interface ApiResponse<T = any> {
code: number
msg: string
data: T
}
interface FriendListResponse {
list: any[]
total: number
gender: "male" | "female"
customer: string
}
interface WechatFriendSelectorProps {
@@ -39,83 +22,40 @@ interface WechatFriendSelectorProps {
onOpenChange: (open: boolean) => void
selectedFriends: WechatFriend[]
onSelect: (friends: WechatFriend[]) => void
devices?: number[]
}
export function WechatFriendSelector({ open, onOpenChange, selectedFriends, onSelect, devices = [] }: WechatFriendSelectorProps) {
export function WechatFriendSelector({ open, onOpenChange, selectedFriends, onSelect }: WechatFriendSelectorProps) {
const [searchQuery, setSearchQuery] = useState("")
const [friends, setFriends] = useState<WechatFriend[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalItems, setTotalItems] = useState(0)
const [tempSelectedFriends, setTempSelectedFriends] = useState<WechatFriend[]>([])
const pageSize = 20
useEffect(() => {
if (open) {
fetchFriends(1)
setTempSelectedFriends([...selectedFriends])
fetchFriends()
}
}, [open, selectedFriends])
}, [open])
const fetchFriends = async (pageNum: number) => {
const fetchFriends = async () => {
setLoading(true)
try {
const queryParams = new URLSearchParams({
page: pageNum.toString(),
limit: pageSize.toString(),
...(searchQuery ? { keyword: searchQuery } : {})
})
if (devices && devices.length > 0) {
queryParams.append('deviceIds', devices.join(','))
}
const response = await api.get<ApiResponse<FriendListResponse>>(`/v1/friend?${queryParams.toString()}`)
if (response.code === 200 && response.data) {
const friendsList = response.data.list.map(item => ({
id: item.id || item.wechatId || `${item.nickname}-${Math.random()}`,
nickname: item.nickname || '未知好友',
wechatId: item.wechatId || '',
avatar: item.avatar || '/placeholder.svg',
alias: item.alias || '',
ownerNickname: item.ownerNickname || '',
ownerAlias: item.ownerAlias || item.ownerWechatId || '',
createTime: item.createTime || '--'
}))
setFriends(friendsList)
setTotalItems(response.data.total)
setTotalPages(Math.ceil(response.data.total / pageSize))
setPage(pageNum)
} else {
showToast(response.msg || "获取好友列表失败", "error")
}
} catch (error: any) {
console.error("获取好友列表失败:", error)
showToast(error?.message || "请检查网络连接", "error")
} finally {
setLoading(false)
}
// 模拟从API获取好友列表
await new Promise((resolve) => setTimeout(resolve, 1000))
const mockFriends = Array.from({ length: 20 }, (_, i) => ({
id: `friend-${i}`,
nickname: `好友${i + 1}`,
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
avatar: `/placeholder.svg?height=40&width=40&text=${i + 1}`,
gender: Math.random() > 0.5 ? "male" : "female",
customer: `客户${i + 1}`,
}))
setFriends(mockFriends)
setLoading(false)
}
const handleSearch = () => {
fetchFriends(1)
}
const handlePrevPage = () => {
if (page > 1) {
fetchFriends(page - 1)
}
}
const handleNextPage = () => {
if (page < totalPages) {
fetchFriends(page + 1)
}
}
const filteredFriends = friends.filter(
(friend) =>
friend.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
friend.wechatId.toLowerCase().includes(searchQuery.toLowerCase()),
)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -123,103 +63,55 @@ export function WechatFriendSelector({ open, onOpenChange, selectedFriends, onSe
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索好友"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-9"
/>
</div>
<Button
variant="outline"
size="icon"
onClick={handleSearch}
disabled={loading}
>
<Search className="h-4 w-4" />
</Button>
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索好友"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<div className="mt-4 space-y-2 max-h-[400px] overflow-y-auto">
{loading ? (
<div className="text-center py-4">...</div>
) : friends.length === 0 ? (
) : filteredFriends.length === 0 ? (
<div className="text-center py-4"></div>
) : (
friends.map((friend) => (
filteredFriends.map((friend) => (
<div key={friend.id} className="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded-lg">
<Checkbox
checked={tempSelectedFriends.some((f) => f.id === friend.id)}
checked={selectedFriends.some((f) => f.id === friend.id)}
onCheckedChange={(checked) => {
if (checked) {
setTempSelectedFriends([...tempSelectedFriends, friend])
onSelect([...selectedFriends, friend])
} else {
setTempSelectedFriends(tempSelectedFriends.filter((f) => f.id !== friend.id))
onSelect(selectedFriends.filter((f) => f.id !== friend.id))
}
}}
/>
<Avatar>
<AvatarImage src={friend.avatar} />
<AvatarFallback>{friend.nickname?.[0] || '?'}</AvatarFallback>
<AvatarFallback>{friend.nickname[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{friend.nickname}</div>
<div className="flex-1">
<div className="font-medium">{friend.nickname}</div>
<div className="text-sm text-gray-500">
{friend.wechatId && <div className="truncate">ID{friend.alias || friend.wechatId}</div>}
{friend.ownerNickname && <div className="truncate">{friend.ownerNickname} ({friend.ownerAlias || '--'})</div>}
<div>{friend.wechatId}</div>
<div>{friend.customer}</div>
</div>
</div>
</div>
))
)}
</div>
{/* 分页控制 */}
{totalPages > 1 && (
<div className="flex items-center justify-between border-t pt-4 mt-4">
<div className="text-sm text-gray-500">
{totalItems}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={handlePrevPage}
disabled={page === 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm">
{page} / {totalPages}
</span>
<Button
variant="outline"
size="icon"
onClick={handleNextPage}
disabled={page === totalPages || loading}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
<div className="flex justify-end space-x-2 mt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={() => {
onSelect(tempSelectedFriends)
onOpenChange(false)
}}>
({tempSelectedFriends.length})
</Button>
<Button onClick={() => onOpenChange(false)}></Button>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -4,22 +4,16 @@ import { useState, useEffect } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Search, ChevronLeft, ChevronRight } from "lucide-react"
import { Search } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { api } from "@/lib/api"
import { showToast } from "@/lib/toast"
import { WechatGroup } from "@/types/wechat"
interface ApiResponse<T = any> {
code: number
msg: string
data: T
}
interface GroupListResponse {
list: any[]
total: number
interface WechatGroup {
id: string
name: string
memberCount: number
avatar: string
owner: string
customer: string
}
interface WechatGroupSelectorProps {
@@ -33,69 +27,30 @@ export function WechatGroupSelector({ open, onOpenChange, selectedGroups, onSele
const [searchQuery, setSearchQuery] = useState("")
const [groups, setGroups] = useState<WechatGroup[]>([])
const [loading, setLoading] = useState(false)
const [page, setPage] = useState(1)
const [totalPages, setTotalPages] = useState(1)
const [totalItems, setTotalItems] = useState(0)
const [tempSelectedGroups, setTempSelectedGroups] = useState<WechatGroup[]>([])
const pageSize = 20
useEffect(() => {
if (open) {
fetchGroups(1)
setTempSelectedGroups([...selectedGroups])
fetchGroups()
}
}, [open, selectedGroups])
}, [open])
const fetchGroups = async (pageNum: number) => {
const fetchGroups = async () => {
setLoading(true)
try {
const queryParams = new URLSearchParams({
page: pageNum.toString(),
limit: pageSize.toString(),
...(searchQuery ? { keyword: searchQuery } : {})
})
const response = await api.get<ApiResponse<GroupListResponse>>(`/v1/chatroom?${queryParams.toString()}`)
if (response.code === 200 && response.data) {
const groupsList = response.data.list.map(item => ({
id: item.id || `group-${Math.random()}`,
name: item.name || item.chatroomName || '未知群聊',
memberCount: item.memberCount || 0,
avatar: item.avatar || '/placeholder.svg',
customer: item.ownerNickname || '--'
}))
setGroups(groupsList)
setTotalItems(response.data.total)
setTotalPages(Math.ceil(response.data.total / pageSize))
setPage(pageNum)
} else {
showToast(response.msg || "获取群聊列表失败", "error")
}
} catch (error: any) {
console.error("获取群聊列表失败:", error)
showToast(error?.message || "请检查网络连接", "error")
} finally {
// 模拟从API获取群聊列表
await new Promise((resolve) => setTimeout(resolve, 1000))
const mockGroups = Array.from({ length: 10 }, (_, i) => ({
id: `group-${i}`,
name: `群聊${i + 1}`,
memberCount: Math.floor(Math.random() * 400) + 100,
avatar: `/placeholder.svg?height=40&width=40&text=群${i + 1}`,
owner: `群主${i + 1}`,
customer: `客户${i + 1}`,
}))
setGroups(mockGroups)
setLoading(false)
}
}
const handleSearch = () => {
fetchGroups(1)
}
const handlePrevPage = () => {
if (page > 1) {
fetchGroups(page - 1)
}
}
const handleNextPage = () => {
if (page < totalPages) {
fetchGroups(page + 1)
}
}
const filteredGroups = groups.filter((group) => group.name.toLowerCase().includes(searchQuery.toLowerCase()))
return (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -103,102 +58,53 @@ export function WechatGroupSelector({ open, onOpenChange, selectedGroups, onSele
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex gap-2">
<div className="relative flex-1">
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
<Input
placeholder="搜索群聊"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
className="pl-9"
/>
</div>
<Button
variant="outline"
size="icon"
onClick={handleSearch}
disabled={loading}
>
<Search className="h-4 w-4" />
</Button>
</div>
<div className="mt-4 space-y-2 max-h-[400px] overflow-y-auto">
{loading ? (
<div className="text-center py-4">...</div>
) : groups.length === 0 ? (
) : filteredGroups.length === 0 ? (
<div className="text-center py-4"></div>
) : (
groups.map((group) => (
filteredGroups.map((group) => (
<div key={group.id} className="flex items-center space-x-3 p-2 hover:bg-gray-100 rounded-lg">
<Checkbox
checked={tempSelectedGroups.some((g) => g.id === group.id)}
checked={selectedGroups.some((g) => g.id === group.id)}
onCheckedChange={(checked) => {
if (checked) {
setTempSelectedGroups([...tempSelectedGroups, group])
onSelect([...selectedGroups, group])
} else {
setTempSelectedGroups(tempSelectedGroups.filter((g) => g.id !== group.id))
onSelect(selectedGroups.filter((g) => g.id !== group.id))
}
}}
/>
<Avatar className="h-10 w-10 rounded-lg">
<AvatarImage src={group.avatar} />
<AvatarFallback className="rounded-lg">{group.name?.[0] || '群'}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">{group.name}</div>
<img src={group.avatar || "/placeholder.svg"} alt={group.name} className="w-10 h-10 rounded-lg" />
<div className="flex-1">
<div className="font-medium">{group.name}</div>
<div className="text-sm text-gray-500">
<div className="truncate">{group.customer}</div>
<div>{group.owner}</div>
<div>{group.customer}</div>
<div>{group.memberCount}</div>
</div>
</div>
</div>
))
)}
</div>
{/* 分页控制 */}
{totalPages > 1 && (
<div className="flex items-center justify-between border-t pt-4 mt-4">
<div className="text-sm text-gray-500">
{totalItems}
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="icon"
onClick={handlePrevPage}
disabled={page === 1 || loading}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-sm">
{page} / {totalPages}
</span>
<Button
variant="outline"
size="icon"
onClick={handleNextPage}
disabled={page === totalPages || loading}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
<div className="flex justify-end space-x-2 mt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={() => {
onSelect(tempSelectedGroups)
onOpenChange(false)
}}>
({tempSelectedGroups.length})
</Button>
<Button onClick={() => onOpenChange(false)}></Button>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,114 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ChevronDown, ChevronUp, UserPlus } from "lucide-react"
export interface AcquisitionPlan {
id: string
name: string
status: "active" | "paused" | "completed"
totalAcquired: number
target: number
progress: number
startDate: string
endDate: string
}
interface ExpandableAcquisitionCardProps {
plan: AcquisitionPlan
onEdit?: (id: string) => void
onPause?: (id: string) => void
onResume?: (id: string) => void
}
export function ExpandableAcquisitionCard({ plan, onEdit, onPause, onResume }: ExpandableAcquisitionCardProps) {
const [expanded, setExpanded] = useState(false)
const toggleExpand = () => {
setExpanded(!expanded)
}
const statusColors = {
active: "bg-green-100 text-green-800",
paused: "bg-yellow-100 text-yellow-800",
completed: "bg-blue-100 text-blue-800",
}
return (
<Card className="w-full mb-4">
<CardHeader className="pb-2">
<div className="flex justify-between items-center">
<CardTitle className="text-lg font-medium">{plan.name}</CardTitle>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded text-xs ${statusColors[plan.status]}`}>
{plan.status.charAt(0).toUpperCase() + plan.status.slice(1)}
</span>
<Button variant="ghost" size="sm" onClick={toggleExpand}>
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<UserPlus size={16} className="text-gray-500" />
<span className="text-sm">
{plan.totalAcquired} / {plan.target}
</span>
</div>
<div className="text-sm text-gray-500">
{new Date(plan.startDate).toLocaleDateString()} - {new Date(plan.endDate).toLocaleDateString()}
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mb-4">
<div className="bg-blue-600 rounded-full h-2" style={{ width: `${Math.min(plan.progress, 100)}%` }}></div>
</div>
{expanded && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-medium">{plan.totalAcquired}</p>
</div>
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-medium">{plan.target}</p>
</div>
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-medium">{new Date(plan.startDate).toLocaleDateString()}</p>
</div>
<div>
<p className="text-xs text-gray-500"></p>
<p className="text-sm font-medium">{new Date(plan.endDate).toLocaleDateString()}</p>
</div>
</div>
<div className="flex justify-end gap-2 mt-2">
{onEdit && (
<Button variant="outline" size="sm" onClick={() => onEdit(plan.id)}>
</Button>
)}
{plan.status === "active" && onPause && (
<Button variant="outline" size="sm" onClick={() => onPause(plan.id)}>
</Button>
)}
{plan.status === "paused" && onResume && (
<Button variant="outline" size="sm" onClick={() => onResume(plan.id)}>
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -100,4 +100,3 @@ export function PlanSettingsDialog({ open, onOpenChange, planId }: PlanSettingsD
</Dialog>
)
}

View File

@@ -11,10 +11,15 @@ export function ApiDocumentationTooltip() {
<TooltipContent className="max-w-xs">
<p className="text-xs">
API将外部系统的客户数据直接导入到存客宝
<br />
<br />
<span className="font-medium"></span>
<br />
<br />
<br />
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}

View File

@@ -0,0 +1,187 @@
"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Search, Smartphone, CheckCircle2, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
interface Device {
id: string
name: string
status: "online" | "offline" | "busy"
type: string
lastActive?: string
}
interface DeviceSelectorProps {
selectedDevices: string[]
onDevicesChange: (deviceIds: string[]) => void
showNextButton?: boolean
onNext?: () => void
onPrevious?: () => void
className?: string
}
export function DeviceSelector({
selectedDevices,
onDevicesChange,
showNextButton = false,
onNext,
onPrevious,
className,
}: DeviceSelectorProps) {
const [devices, setDevices] = useState<Device[]>([])
const [searchTerm, setSearchTerm] = useState("")
const [loading, setLoading] = useState(true)
useEffect(() => {
// 模拟加载设备数据
setTimeout(() => {
setDevices([
{ id: "1", name: "设备 001", status: "online", type: "Android" },
{ id: "2", name: "设备 002", status: "online", type: "iOS" },
{ id: "3", name: "设备 003", status: "offline", type: "Android" },
{ id: "4", name: "设备 004", status: "busy", type: "Android" },
{ id: "5", name: "设备 005", status: "online", type: "iOS" },
])
setLoading(false)
}, 500)
}, [])
const filteredDevices = devices.filter((device) => device.name.toLowerCase().includes(searchTerm.toLowerCase()))
const handleSelectAll = () => {
const allOnlineDeviceIds = filteredDevices.filter((d) => d.status === "online").map((d) => d.id)
onDevicesChange(allOnlineDeviceIds)
}
const handleClearAll = () => {
onDevicesChange([])
}
const handleToggleDevice = (deviceId: string) => {
if (selectedDevices.includes(deviceId)) {
onDevicesChange(selectedDevices.filter((id) => id !== deviceId))
} else {
onDevicesChange([...selectedDevices, deviceId])
}
}
const getStatusColor = (status: string) => {
switch (status) {
case "online":
return "text-green-500"
case "offline":
return "text-gray-400"
case "busy":
return "text-yellow-500"
default:
return "text-gray-400"
}
}
const getStatusText = (status: string) => {
switch (status) {
case "online":
return "在线"
case "offline":
return "离线"
case "busy":
return "忙碌"
default:
return "未知"
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">...</div>
</div>
)
}
return (
<div className={cn("space-y-4", className)}>
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<Input
type="text"
placeholder="搜索设备..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleSelectAll}>
线
</Button>
<Button variant="outline" size="sm" onClick={handleClearAll}>
</Button>
</div>
</div>
<div className="text-sm text-gray-500"> {selectedDevices.length} </div>
<ScrollArea className="h-[400px] border rounded-lg">
<div className="p-4 space-y-2">
{filteredDevices.map((device) => (
<Card
key={device.id}
className={cn(
"cursor-pointer transition-colors",
selectedDevices.includes(device.id) ? "border-blue-500 bg-blue-50" : "",
device.status !== "online" ? "opacity-60" : "",
)}
onClick={() => device.status === "online" && handleToggleDevice(device.id)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Checkbox
checked={selectedDevices.includes(device.id)}
disabled={device.status !== "online"}
onCheckedChange={() => handleToggleDevice(device.id)}
onClick={(e) => e.stopPropagation()}
/>
<Smartphone className="h-5 w-5 text-gray-400" />
<div>
<div className="font-medium">{device.name}</div>
<div className="text-sm text-gray-500">{device.type}</div>
</div>
</div>
<div className={cn("flex items-center gap-1 text-sm", getStatusColor(device.status))}>
{device.status === "online" ? <CheckCircle2 className="h-4 w-4" /> : <Circle className="h-4 w-4" />}
{getStatusText(device.status)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
{showNextButton && (
<div className="flex justify-between pt-4">
{onPrevious && (
<Button variant="outline" onClick={onPrevious}>
</Button>
)}
<Button onClick={onNext} disabled={selectedDevices.length === 0} className="ml-auto">
</Button>
</div>
)}
</div>
)
}
export default DeviceSelector

View File

@@ -2,9 +2,8 @@
import { Card } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Smartphone, Battery, Users, MessageCircle, Clock } from "lucide-react"
import { Smartphone, Battery, Users, MessageCircle } from "lucide-react"
import { Checkbox } from "@/components/ui/checkbox"
import { ImeiDisplay } from "@/components/ImeiDisplay"
export interface Device {
id: string
@@ -55,7 +54,7 @@ export function DeviceGrid({ devices, selectable, selectedDevices, onSelect, dev
)}
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<Badge variant={currentStatus.status === "online" ? "default" : "secondary"}>
<Badge variant={currentStatus.status === "online" ? "success" : "secondary"}>
{currentStatus.status === "online" ? "在线" : "离线"}
</Badge>
<div className="flex items-center space-x-2">
@@ -69,10 +68,7 @@ export function DeviceGrid({ devices, selectable, selectedDevices, onSelect, dev
<Smartphone className="w-4 h-4 text-gray-400" />
<div>
<div className="text-sm font-medium">{device.name}</div>
<div className="text-xs text-gray-500 flex items-center">
<span className="mr-1">IMEI:</span>
<ImeiDisplay imei={device.imei} containerWidth={80} />
</div>
<div className="text-xs text-gray-500">IMEI-{device.imei}</div>
</div>
</div>
{device.remark && <div className="text-xs text-gray-500">: {device.remark}</div>}
@@ -94,7 +90,7 @@ export function DeviceGrid({ devices, selectable, selectedDevices, onSelect, dev
<div>{device.todayAdded}</div>
<div>
<Badge variant={device.addFriendStatus === "normal" ? "default" : "destructive"}>
<Badge variant={device.addFriendStatus === "normal" ? "success" : "destructive"}>
{device.addFriendStatus === "normal" ? "正常" : "异常"}
</Badge>
</div>
@@ -106,4 +102,3 @@ export function DeviceGrid({ devices, selectable, selectedDevices, onSelect, dev
</div>
)
}

View File

@@ -0,0 +1,98 @@
"use client"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge"
import { Checkbox } from "@/components/ui/checkbox"
import { Battery, Smartphone, Users } from "lucide-react"
import type { Device } from "@/types/device"
interface DeviceTableProps {
devices: Device[]
selectedDevices: string[]
onSelectDevice: (deviceId: string, checked: boolean) => void
onSelectAll: (checked: boolean) => void
onDeviceClick: (deviceId: string) => void
}
export function DeviceTable({
devices,
selectedDevices,
onSelectDevice,
onSelectAll,
onDeviceClick,
}: DeviceTableProps) {
const allSelected = devices.length > 0 && selectedDevices.length === devices.length
return (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[50px]">
<Checkbox checked={allSelected} onCheckedChange={(checked) => onSelectAll(!!checked)} />
</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{devices.map((device) => (
<TableRow
key={device.id}
className="cursor-pointer hover:bg-gray-50"
onClick={() => onDeviceClick(device.id)}
>
<TableCell className="w-[50px]" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={selectedDevices.includes(device.id)}
onCheckedChange={(checked) => onSelectDevice(device.id, !!checked)}
/>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Smartphone className="h-4 w-4 text-gray-400" />
<div>
<div className="font-medium">{device.name}</div>
<div className="text-xs text-gray-500">IMEI-{device.imei}</div>
{device.remark && <div className="text-xs text-gray-500">: {device.remark}</div>}
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Badge variant={device.status === "online" ? "success" : "secondary"}>
{device.status === "online" ? "在线" : "离线"}
</Badge>
<div className="flex items-center space-x-1">
<Battery className={`h-4 w-4 ${device.battery < 20 ? "text-red-500" : "text-green-500"}`} />
<span className="text-xs">{device.battery}%</span>
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm">{device.wechatId}</div>
</TableCell>
<TableCell>
<div className="flex items-center space-x-1">
<Users className="h-4 w-4 text-gray-400" />
<span className="text-sm">{device.friendCount}</span>
</div>
</TableCell>
<TableCell>
<div className="text-sm">+{device.todayAdded}</div>
</TableCell>
<TableCell>
<Badge variant={device.addFriendStatus === "normal" ? "success" : "destructive"}>
{device.addFriendStatus === "normal" ? "正常" : "异常"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}

View File

@@ -9,4 +9,3 @@ export function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
}
export default AppleIcon

View File

@@ -10,4 +10,3 @@ export function WeChatIcon(props: React.SVGProps<SVGSVGElement>) {
}
export default WeChatIcon

View File

@@ -0,0 +1,325 @@
"use client"
import { CardDescription } from "@/components/ui/card"
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { useToast } from "@/hooks/use-toast"
import { Eye, EyeOff, Loader2, Shield, User } from "lucide-react"
import { useRouter } from "next/navigation"
import { loginWithPassword, loginWithCode, sendVerificationCode, saveUserInfo, checkLoginStatus } from "@/lib/api/auth"
export default function LoginForm() {
const router = useRouter()
const { toast } = useToast()
// 状态管理
const [isLoading, setIsLoading] = useState(false)
const [showPassword, setShowPassword] = useState(false)
const [countdown, setCountdown] = useState(0)
const [activeTab, setActiveTab] = useState("password")
// 密码登录表单
const [passwordForm, setPasswordForm] = useState({
account: "",
password: "",
})
// 验证码登录表单
const [codeForm, setCodeForm] = useState({
account: "",
code: "",
})
// 检查是否已登录
useEffect(() => {
if (checkLoginStatus()) {
router.push("/")
}
}, [router])
// 倒计时效果
useEffect(() => {
let timer: NodeJS.Timeout
if (countdown > 0) {
timer = setTimeout(() => setCountdown(countdown - 1), 1000)
}
return () => clearTimeout(timer)
}, [countdown])
// 密码登录
const handlePasswordLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!passwordForm.account || !passwordForm.password) {
toast({
title: "参数错误",
description: "请输入账号和密码",
variant: "destructive",
})
return
}
setIsLoading(true)
try {
console.log("开始密码登录:", passwordForm.account)
const result = await loginWithPassword(passwordForm.account, passwordForm.password)
console.log("登录成功:", result)
// 保存登录信息
if (result?.data?.token && result?.data?.user) {
saveUserInfo(result.data.token, result.data.user)
toast({
title: "登录成功",
description: `欢迎回来,${result.data.user.nickname || result.data.user.username}`,
})
// 跳转到首页
router.push("/")
} else {
throw new Error("登录响应数据格式错误")
}
} catch (error) {
console.error("密码登录失败:", error)
toast({
title: "登录失败",
description: error instanceof Error ? error.message : "登录失败,请检查账号密码",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
// 发送验证码
const handleSendCode = async () => {
if (!codeForm.account) {
toast({
title: "参数错误",
description: "请输入手机号",
variant: "destructive",
})
return
}
if (countdown > 0) {
return
}
try {
console.log("发送验证码:", codeForm.account)
await sendVerificationCode(codeForm.account)
toast({
title: "发送成功",
description: "验证码已发送到您的手机",
})
// 开始倒计时
setCountdown(60)
} catch (error) {
console.error("发送验证码失败:", error)
toast({
title: "发送失败",
description: error instanceof Error ? error.message : "发送验证码失败",
variant: "destructive",
})
}
}
// 验证码登录
const handleCodeLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!codeForm.account || !codeForm.code) {
toast({
title: "参数错误",
description: "请输入手机号和验证码",
variant: "destructive",
})
return
}
setIsLoading(true)
try {
console.log("开始验证码登录:", codeForm.account)
const result = await loginWithCode(codeForm.account, codeForm.code)
console.log("登录成功:", result)
// 保存登录信息
if (result?.data?.token && result?.data?.user) {
saveUserInfo(result.data.token, result.data.user)
toast({
title: "登录成功",
description: `欢迎回来,${result.data.user.nickname || result.data.user.username}`,
})
// 跳转到首页
router.push("/")
} else {
throw new Error("登录响应数据格式错误")
}
} catch (error) {
console.error("验证码登录失败:", error)
toast({
title: "登录失败",
description: error instanceof Error ? error.message : "登录失败,请检查验证码",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}
// 键盘事件处理
const handleKeyPress = (e: React.KeyboardEvent, formType: "password" | "code") => {
if (e.key === "Enter") {
if (formType === "password") {
handlePasswordLogin(e as any)
} else {
handleCodeLogin(e as any)
}
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
<Card className="w-full max-w-md shadow-xl">
<CardHeader className="text-center space-y-2">
<div className="mx-auto w-16 h-16 bg-blue-600 rounded-full flex items-center justify-center mb-2">
<Shield className="w-8 h-8 text-white" />
</div>
<CardTitle className="text-2xl font-bold text-gray-900"></CardTitle>
<CardDescription className="text-gray-600"></CardDescription>
</CardHeader>
<CardContent>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="password" className="flex items-center space-x-2">
<User className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="code" className="flex items-center space-x-2">
<Shield className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
{/* 密码登录 */}
<TabsContent value="password" className="space-y-4">
<form onSubmit={handlePasswordLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="account"></Label>
<Input
id="account"
type="text"
placeholder="请输入手机号或用户名"
value={passwordForm.account}
onChange={(e) => setPasswordForm({ ...passwordForm, account: e.target.value })}
onKeyPress={(e) => handleKeyPress(e, "password")}
disabled={isLoading}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<div className="relative">
<Input
id="password"
type={showPassword ? "text" : "password"}
placeholder="请输入密码"
value={passwordForm.password}
onChange={(e) => setPasswordForm({ ...passwordForm, password: e.target.value })}
onKeyPress={(e) => handleKeyPress(e, "password")}
disabled={isLoading}
className="h-12 pr-12"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-12 px-3 hover:bg-transparent"
onClick={() => setShowPassword(!showPassword)}
disabled={isLoading}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
</div>
<Button type="submit" className="w-full h-12" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? "登录中..." : "登录"}
</Button>
</form>
</TabsContent>
{/* 验证码登录 */}
<TabsContent value="code" className="space-y-4">
<form onSubmit={handleCodeLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
type="tel"
placeholder="请输入手机号"
value={codeForm.account}
onChange={(e) => setCodeForm({ ...codeForm, account: e.target.value })}
onKeyPress={(e) => handleKeyPress(e, "code")}
disabled={isLoading}
className="h-12"
/>
</div>
<div className="space-y-2">
<Label htmlFor="code"></Label>
<div className="flex space-x-2">
<Input
id="code"
type="text"
placeholder="请输入验证码"
value={codeForm.code}
onChange={(e) => setCodeForm({ ...codeForm, code: e.target.value })}
onKeyPress={(e) => handleKeyPress(e, "code")}
disabled={isLoading}
className="h-12"
/>
<Button
type="button"
variant="outline"
onClick={handleSendCode}
disabled={isLoading || countdown > 0 || !codeForm.account}
className="h-12 whitespace-nowrap min-w-[100px]"
>
{countdown > 0 ? `${countdown}s` : "发送验证码"}
</Button>
</div>
</div>
<Button type="submit" className="w-full h-12" disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? "登录中..." : "登录"}
</Button>
</form>
</TabsContent>
</Tabs>
{/* 登录提示 */}
<div className="mt-6 text-center text-sm text-gray-500">
<p></p>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -82,4 +82,3 @@ export function PosterSelector({ open, onOpenChange, onSelect }: PosterSelectorP
</Dialog>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useState, useEffect } from "react"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Button } from "@/components/ui/button"
import { Settings, Moon, Sun } from 'lucide-react'
import { useMobile } from "@/hooks/use-mobile"
import { useTheme } from 'next-themes'
export function SettingsDropdown() {
const isMobile = useMobile()
const [isDesktopView, setIsDesktopView] = useState(!isMobile)
const { setTheme, theme } = useTheme()
useEffect(() => {
setIsDesktopView(!isMobile)
}, [isMobile])
const handleToggleView = () => {
setIsDesktopView((prev) => !prev)
}
const handleToggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Settings className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleToggleView}>
{isDesktopView ? "切换到移动端视图" : "切换到桌面端视图"}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleToggleTheme}>
{theme === 'light' ? "切换到深色模式" : "切换到浅色模式"}
{theme === 'light' ? <Moon className="h-4 w-4 ml-2" /> : <Sun className="h-4 w-4 ml-2" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -1,11 +1,11 @@
'use client'
import * as React from 'react'
import {
ThemeProvider as NextThemesProvider,
type ThemeProviderProps,
} from 'next-themes'
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import type { ThemeProviderProps } from "next-themes"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
return (
<NextThemesProvider attribute="class" defaultTheme="system" enableSystem {...props}>
{children}
</NextThemesProvider>
)
}

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -12,11 +10,7 @@ const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
))
AccordionItem.displayName = "AccordionItem"
@@ -28,13 +22,13 @@ const AccordionTrigger = React.forwardRef<
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
@@ -46,13 +40,12 @@ const AccordionContent = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@@ -11,10 +11,7 @@ const Avatar = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...props}
/>
))
@@ -24,11 +21,7 @@ const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square h-full w-full", className)} {...props} />
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
@@ -38,10 +31,7 @@ const AvatarFallback = React.forwardRef<
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
className={cn("flex h-full w-full items-center justify-center rounded-full bg-muted", className)}
{...props}
/>
))

View File

@@ -1,36 +1,29 @@
import * as React from "react"
import type * 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-full 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",
"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 hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
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> {}
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }

View File

@@ -1,36 +1,32 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
},
)
export interface ButtonProps
@@ -40,16 +36,23 @@ export interface ButtonProps
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
({ className, variant, size, asChild = false, children, ...props }, ref) => {
// If asChild is true and the first child is a valid element, clone it with the button props
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
className: cn(buttonVariants({ variant, size, className })),
ref,
...props,
...children.props,
})
}
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
<button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props}>
{children}
</button>
)
}
},
)
Button.displayName = "Button"

View File

@@ -1,7 +1,5 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import type * as React from "react"
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
@@ -9,12 +7,7 @@ import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
@@ -27,35 +20,35 @@ function Calendar({
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(buttonVariants({ variant: "ghost" }), "h-8 w-8 p-0 font-normal aria-selected:opacity-100"),
day_range_start: "day-range-start",
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
}}
{...props}
/>

View File

@@ -1,79 +1,52 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
// 基础卡片,提供一个干净、现代的容器
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
className={cn("rounded-xl border bg-white text-gray-900 shadow-sm transition-shadow hover:shadow-md", 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}
/>
))
// 卡片头部,用于标题和描述
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<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
// 卡片标题
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
),
)
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
// 卡片描述
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} />
))
// 卡片内容区域
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}
/>
))
// 卡片底部,通常用于放置操作按钮
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 }

View File

@@ -12,10 +12,7 @@ export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
}
type ChartContextProps = {
@@ -34,43 +31,31 @@ function useChart() {
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
interface ChartContainerProps extends React.HTMLAttributes<HTMLDivElement> {
config: Record<string, { label: string; color: string }>
}
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
const ChartContainer = React.forwardRef<HTMLDivElement, ChartContainerProps>(
({ className, config, children, ...props }, ref) => {
const colorVars = Object.entries(config).reduce(
(acc, [key, value]) => {
acc[`--color-${key}`] = value.color
return acc
},
{} as Record<string, string>,
)
return (
<div ref={ref} className={cn("relative", className)} style={colorVars} {...props}>
{children}
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
)
},
)
ChartContainer.displayName = "ChartContainer"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
const colorConfig = Object.entries(config).filter(([_, config]) => config.theme || config.color)
if (!colorConfig.length) {
return null
@@ -85,14 +70,12 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
`,
)
.join("\n"),
}}
@@ -100,161 +83,48 @@ ${colorConfig
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
interface ChartTooltipProps {
children?: React.ReactNode
}
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const ChartTooltip = React.forwardRef<HTMLDivElement, ChartTooltipProps>(({ className, children, ...props }, ref) => {
return <div ref={ref} className={cn("rounded-md border bg-card p-2 shadow-md", className)} {...props} />
})
ChartTooltip.displayName = "ChartTooltip"
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
interface ChartTooltipContentProps extends React.HTMLAttributes<HTMLDivElement> {
active?: boolean
payload?: any[]
label?: string
labelFormatter?: (value: any) => string
hideLabel?: boolean
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
const ChartTooltipContent = React.forwardRef<HTMLDivElement, ChartTooltipContentProps>(
({ active, payload, label, labelFormatter, hideLabel, className, ...props }, ref) => {
if (!active || !payload) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
<div ref={ref} className={cn("rounded-lg border bg-background p-2 shadow-sm", className)} {...props}>
{!hideLabel && label && (
<div className="mb-1 text-xs font-medium">{labelFormatter ? labelFormatter(label) : label}</div>
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
<div className="flex flex-col gap-1">
{payload.map((entry, index) => (
<div key={index} className="flex items-center gap-2 text-xs">
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: entry.color }} />
<span className="font-medium">{entry.name}:</span>
<span>{entry.value}</span>
</div>
))}
</div>
</div>
)
}
},
)
ChartTooltipContent.displayName = "ChartTooltip"
ChartTooltipContent.displayName = "ChartTooltipContent"
const ChartLegend = RechartsPrimitive.Legend
@@ -265,101 +135,70 @@ const ChartLegendContent = React.forwardRef<
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
if (!payload?.length) {
return null
}
)
return (
<div
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
})
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
"payload" in payload && typeof payload.payload === "object" && payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { CheckIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -13,15 +11,13 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))

View File

@@ -1,5 +1,4 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root

View File

@@ -0,0 +1,87 @@
"use client"
import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import type { DateRange } from "react-day-picker"
export interface DatePickerProps {
date: Date | undefined
setDate: (date: Date | undefined) => void
className?: string
placeholder?: string
disabled?: boolean
}
export function DatePicker({ date, setDate, className, placeholder = "选择日期", disabled = false }: DatePickerProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-full justify-start text-left font-normal", !date && "text-muted-foreground", className)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "yyyy-MM-dd") : <span>{placeholder}</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar mode="single" selected={date} onSelect={setDate} initialFocus />
</PopoverContent>
</Popover>
)
}
export interface DatePickerWithRangeProps {
date: DateRange | undefined
setDate: (date: DateRange | undefined) => void
className?: string
placeholder?: string
disabled?: boolean
}
export function DatePickerWithRange({
date,
setDate,
className,
placeholder = "选择日期范围",
disabled = false,
}: DatePickerWithRangeProps) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-full justify-start text-left font-normal", !date && "text-muted-foreground", className)}
disabled={disabled}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date?.from ? (
date.to ? (
<>
{format(date.from, "yyyy-MM-dd")} - {format(date.to, "yyyy-MM-dd")}
</>
) : (
format(date.from, "yyyy-MM-dd")
)
) : (
<span>{placeholder}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
initialFocus
mode="range"
defaultMonth={date?.from}
selected={date}
onSelect={setDate}
numberOfMonths={2}
/>
</PopoverContent>
</Popover>
)
}

View File

@@ -49,11 +49,9 @@ export function DateRangePicker({ className, value, onChange }: DateRangePickerP
onSelect={onChange}
numberOfMonths={2}
locale={zhCN}
showOutsideDays={false}
/>
</PopoverContent>
</Popover>
</div>
)
}

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { Cross2Icon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -22,7 +20,7 @@ const DialogOverlay = React.forwardRef<
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
className,
)}
{...props}
/>
@@ -39,13 +37,13 @@ const DialogContent = React.forwardRef<
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
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" />
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
@@ -53,31 +51,13 @@ const DialogContent = React.forwardRef<
))
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}
/>
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}
/>
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"
@@ -87,10 +67,7 @@ const DialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
className={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
@@ -100,11 +77,7 @@ 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}
/>
<DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
@@ -112,8 +85,8 @@ export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -27,18 +25,17 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
@@ -48,13 +45,12 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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",
className
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
@@ -65,8 +61,9 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
className
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 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",
className,
)}
{...props}
/>
@@ -83,9 +80,9 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
className,
)}
{...props}
/>
@@ -100,21 +97,20 @@ const DropdownMenuCheckboxItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
@@ -124,13 +120,13 @@ const DropdownMenuRadioItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
@@ -146,11 +142,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...props}
/>
))
@@ -160,24 +152,12 @@ const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
<DropdownMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"

View File

@@ -2,21 +2,21 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
export { Input }

View File

@@ -1,25 +1,16 @@
"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 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>
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName

View File

@@ -1,8 +1,8 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { ChevronLeftIcon, ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { type ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
@@ -14,22 +14,14 @@ const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
({ className, ...props }, ref) => (
<ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
),
)
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
@@ -39,12 +31,7 @@ type PaginationLinkProps = {
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
@@ -52,55 +39,32 @@ const PaginationLink = ({
variant: isActive ? "outline" : "ghost",
size,
}),
className
className,
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to previous page" size="default" className={cn("gap-1 pl-2.5", className)} {...props}>
<ChevronLeftIcon className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink aria-label="Go to next page" size="default" className={cn("gap-1 pr-2.5", className)} {...props}>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
<ChevronRightIcon className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
<span aria-hidden className={cn("flex h-9 w-9 items-center justify-center", className)} {...props}>
<DotsHorizontalIcon className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
@@ -20,7 +18,7 @@ const PopoverContent = React.forwardRef<
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
className,
)}
{...props}
/>

View File

@@ -32,4 +32,3 @@ export function PreviewDialog({ children, title = "预览效果" }: PreviewDialo
</>
)
}

View File

@@ -1,28 +1,35 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number
max?: number
className?: string
}
const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
({ className, value = 0, max = 100, ...props }, ref) => {
const percentage = Math.min(Math.max((value / max) * 100, 0), 100)
return (
<div
ref={ref}
className={cn("relative h-2 w-full overflow-hidden rounded-full bg-gray-200", className)}
{...props}
>
<div
className="h-full w-full flex-1 bg-blue-500 transition-all duration-300 ease-in-out"
style={{
transform: `translateX(-${100 - percentage}%)`,
}}
/>
</div>
)
},
)
Progress.displayName = "Progress"
export { Progress }

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { CircleIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -10,13 +8,7 @@ const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
@@ -28,13 +20,13 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
<CircleIcon className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
@@ -9,14 +7,8 @@ const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">{children}</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
@@ -32,11 +24,9 @@ const ScrollBar = React.forwardRef<
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" && "h-2.5 border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>

View File

@@ -1,8 +1,6 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { CheckIcon, ChevronDownIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils"
@@ -19,54 +17,19 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
"flex h-9 w-full items-center justify-between 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",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDownIcon 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>
@@ -75,25 +38,23 @@ const SelectContent = React.forwardRef<
<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",
"relative z-50 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
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)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
@@ -103,11 +64,7 @@ const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
@@ -118,17 +75,16 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
"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 left-2 flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
@@ -138,23 +94,8 @@ 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}
/>
<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,
}
export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator }

View File

@@ -1,31 +1,23 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
export interface SeparatorProps extends React.HTMLAttributes<HTMLDivElement> {
orientation?: "horizontal" | "vertical"
}
const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
({ className, orientation = "horizontal", ...props }, ref) => (
<div
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
className,
)}
{...props}
/>
)
),
)
Separator.displayName = SeparatorPrimitive.Root.displayName
Separator.displayName = "Separator"
export { Separator }

View File

@@ -1,15 +1,8 @@
import type React from "react"
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
export { Skeleton }
export function Skeleton({ className, ...props }: SkeletonProps) {
return <div className={cn("animate-pulse rounded-md bg-gray-200 dark:bg-gray-800", className)} {...props} />
}

View File

@@ -11,10 +11,7 @@ const Slider = React.forwardRef<
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
className={cn("relative flex w-full touch-none select-none items-center", className)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">

View File

@@ -0,0 +1,54 @@
import React from "react"
import { cn } from "@/lib/utils"
export interface StepsProps extends React.HTMLAttributes<HTMLDivElement> {
current: number
children: React.ReactNode
}
export interface StepProps extends React.HTMLAttributes<HTMLDivElement> {
title: string
description?: string
icon?: React.ReactNode
status?: "wait" | "process" | "finish" | "error"
}
export function Steps({ current, children, className, ...props }: StepsProps) {
const childrenArray = React.Children.toArray(children)
const steps = childrenArray.map((child, index) => {
if (React.isValidElement(child)) {
return React.cloneElement(child, {
status: index < current ? "finish" : index === current ? "process" : "wait",
...child.props,
})
}
return child
})
return (
<div className={cn("steps-container flex flex-col gap-4", className)} {...props}>
{steps}
</div>
)
}
export function Step({ title, description, icon, status = "wait", className, ...props }: StepProps) {
const statusClasses = {
wait: "border-gray-300 text-gray-500",
process: "border-blue-500 text-blue-500 bg-blue-100",
finish: "border-green-500 text-green-500 bg-green-100",
error: "border-red-500 text-red-500 bg-red-100",
}
return (
<div className={cn("flex items-center gap-4", className)} {...props}>
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full border-2", statusClasses[status])}>
{icon || (status === "finish" ? "✓" : status === "error" ? "✗" : "")}
</div>
<div className="flex flex-col">
<div className="text-sm font-medium">{title}</div>
{description && <div className="text-xs text-gray-500">{description}</div>}
</div>
</div>
)
}

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
@@ -11,15 +9,15 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>

View File

@@ -2,116 +2,68 @@ 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>
))
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} />
))
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}
/>
))
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}
/>
))
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}
/>
))
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-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
))
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-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
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,
}
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell }

View File

@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
@@ -14,8 +12,8 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
@@ -29,8 +27,8 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 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-sm",
className
"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}
/>
@@ -45,7 +43,7 @@ const TabsContent = React.forwardRef<
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
className,
)}
{...props}
/>

View File

@@ -2,15 +2,14 @@ import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}

View File

@@ -17,7 +17,7 @@ const ToastViewport = React.forwardRef<
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
className,
)}
{...props}
/>
@@ -30,28 +30,20 @@ const toastVariants = cva(
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
destructive: "destructive border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
},
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
return <ToastPrimitives.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
})
Toast.displayName = ToastPrimitives.Root.displayName
@@ -63,7 +55,7 @@ const ToastAction = React.forwardRef<
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
className,
)}
{...props}
/>
@@ -78,7 +70,7 @@ const ToastClose = React.forwardRef<
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
className,
)}
toast-close=""
{...props}
@@ -92,11 +84,7 @@ const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
<ToastPrimitives.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
@@ -104,11 +92,7 @@ const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
<ToastPrimitives.Description ref={ref} className={cn("text-sm opacity-90", className)} {...props} />
))
ToastDescription.displayName = ToastPrimitives.Description.displayName

View File

@@ -1,34 +1,23 @@
"use client"
import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />
</Toast>
))}
<ToastViewport />
</ToastProvider>
)

View File

@@ -1,30 +1,107 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
interface TooltipProps {
children: React.ReactNode
content: React.ReactNode
className?: string
delayDuration?: number
side?: "top" | "right" | "bottom" | "left"
}
const Tooltip = TooltipPrimitive.Root
const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
({ children, content, className, delayDuration = 200, side = "top" }, ref) => {
const [isVisible, setIsVisible] = React.useState(false)
const [position, setPosition] = React.useState({ top: 0, left: 0 })
const tooltipRef = React.useRef<HTMLDivElement>(null)
const timeoutRef = React.useRef<NodeJS.Timeout>()
const TooltipTrigger = TooltipPrimitive.Trigger
const handleMouseEnter = (e: React.MouseEvent) => {
timeoutRef.current = setTimeout(() => {
const rect = (e.target as HTMLElement).getBoundingClientRect()
const tooltipRect = tooltipRef.current?.getBoundingClientRect()
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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}
/>
if (tooltipRect) {
let top = 0
let left = 0
switch (side) {
case "top":
top = rect.top - tooltipRect.height - 8
left = rect.left + (rect.width - tooltipRect.width) / 2
break
case "bottom":
top = rect.bottom + 8
left = rect.left + (rect.width - tooltipRect.width) / 2
break
case "left":
top = rect.top + (rect.height - tooltipRect.height) / 2
left = rect.left - tooltipRect.width - 8
break
case "right":
top = rect.top + (rect.height - tooltipRect.height) / 2
left = rect.right + 8
break
}
setPosition({ top, left })
setIsVisible(true)
}
}, delayDuration)
}
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
setIsVisible(false)
}
React.useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
return (
<div className="relative inline-block" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} ref={ref}>
{children}
{isVisible && (
<div
ref={tooltipRef}
className={cn(
"fixed z-50 px-2 py-1 text-xs text-primary-foreground bg-primary rounded-md shadow-sm scale-90 animate-in fade-in-0 zoom-in-95",
className,
)}
style={{
top: position.top,
left: position.left,
transition: "opacity 150ms ease-in-out, transform 150ms ease-in-out",
}}
>
{content}
</div>
)}
</div>
)
},
)
Tooltip.displayName = "Tooltip"
// 为了保持 API 兼容性,我们导出相同的组件名称
export const TooltipProvider = ({ children }: { children: React.ReactNode }) => children
export const TooltipTrigger = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) => (
<div ref={ref} {...props} />
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
TooltipTrigger.displayName = "TooltipTrigger"
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
export const TooltipContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>((props, ref) => (
<div ref={ref} {...props} />
))
TooltipContent.displayName = "TooltipContent"
export { Tooltip }

View File

@@ -1,12 +1,8 @@
"use client"
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
@@ -28,7 +24,7 @@ const actionTypes = {
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
count = (count + 1) % Number.MAX_VALUE
return count.toString()
}
@@ -85,9 +81,7 @@ export const reducer = (state: State, action: Action): State => {
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)),
}
case "DISMISS_TOAST": {
@@ -111,7 +105,7 @@ export const reducer = (state: State, action: Action): State => {
...t,
open: false,
}
: t
: t,
),
}
}
@@ -182,7 +176,7 @@ function useToast() {
listeners.splice(index, 1)
}
}
}, [state])
}, [])
return {
...state,

142
Cunkebao/next.config.mjs Normal file
View File

@@ -0,0 +1,142 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
// 基础配置
reactStrictMode: true,
swcMinify: true,
// 实验性功能
experimental: {
appDir: true,
serverComponentsExternalPackages: ['axios'],
},
// 图片配置
images: {
domains: [
'localhost',
'ckbapi.quwanzhi.com',
'blob.v0.dev',
'hebbkx1anhila5yf.public.blob.vercel-storage.com'
],
formats: ['image/webp', 'image/avif'],
unoptimized: true, // 添加unoptimized配置
},
// 环境变量配置
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
// 重定向配置
async redirects() {
return [
{
source: '/scenarios/new',
destination: '/scenarios/new/basic',
permanent: false,
},
]
},
// 重写配置 - 用于API代理
async rewrites() {
return [
{
source: '/api/proxy/:path*',
destination: `${process.env.NEXT_PUBLIC_API_BASE_URL}/:path*`,
},
]
},
// 头部配置
async headers() {
return [
{
source: '/api/:path*',
headers: [
{
key: 'Access-Control-Allow-Origin',
value: '*',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET, POST, PUT, DELETE, OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value: 'Content-Type, Authorization',
},
],
},
]
},
// Webpack配置
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// 添加自定义webpack配置
config.resolve.alias = {
...config.resolve.alias,
'@': require('path').resolve(__dirname),
}
// 处理GitHub项目的兼容性
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
}
return config
},
// 输出配置
output: 'standalone',
// 压缩配置
compress: true,
// 电源配置
poweredByHeader: false,
// 生成ETag
generateEtags: true,
// 页面扩展名
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
// 跟踪配置
trailingSlash: false,
// 构建配置
distDir: '.next',
// 静态优化
staticPageGenerationTimeout: 60,
// 开发配置
...(process.env.NODE_ENV === 'development' && {
onDemandEntries: {
maxInactiveAge: 25 * 1000,
pagesBufferLength: 2,
},
}),
// 添加eslint和typescript配置
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
}
// Bundle分析器配置
if (process.env.ANALYZE === 'true') {
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: true,
})
module.exports = withBundleAnalyzer(nextConfig)
} else {
module.exports = nextConfig
}

View File

@@ -1,74 +1,71 @@
{
"name": "my-v0-project",
"name": "cunkebao",
"version": "0.1.0",
"private": true,
"scripts": {
"api:test": "chmod +x scripts/api-test.sh && ./scripts/api-test.sh",
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"migration:setup": "chmod +x scripts/migration-setup.sh && ./scripts/migration-setup.sh",
"migration:test": "node scripts/test-integration.js",
"start": "next start"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@ai-sdk/openai": "^0.0.66",
"@ant-design/plots": "latest",
"@hookform/resolvers": "^3.3.2",
"@radix-ui/react-accordion": "latest",
"@radix-ui/react-alert-dialog": "1.1.4",
"@radix-ui/react-aspect-ratio": "1.1.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "latest",
"@radix-ui/react-checkbox": "latest",
"@radix-ui/react-collapsible": "latest",
"@radix-ui/react-context-menu": "2.2.4",
"@radix-ui/react-dialog": "latest",
"@radix-ui/react-dropdown-menu": "latest",
"@radix-ui/react-hover-card": "1.1.4",
"@radix-ui/react-icons": "latest",
"@radix-ui/react-label": "latest",
"@radix-ui/react-menubar": "1.1.4",
"@radix-ui/react-navigation-menu": "1.2.3",
"@radix-ui/react-popover": "latest",
"@radix-ui/react-progress": "latest",
"@radix-ui/react-radio-group": "latest",
"@radix-ui/react-scroll-area": "latest",
"@radix-ui/react-select": "latest",
"@radix-ui/react-separator": "1.1.1",
"@radix-ui/react-slider": "1.2.2",
"@radix-ui/react-slot": "1.1.1",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slider": "latest",
"@radix-ui/react-switch": "latest",
"@radix-ui/react-tabs": "latest",
"@radix-ui/react-toast": "latest",
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "latest",
"autoprefixer": "^10.4.20",
"ai": "^3.4.32",
"chart.js": "latest",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"cmdk": "^0.2.0",
"date-fns": "latest",
"embla-carousel-react": "8.5.1",
"input-otp": "1.4.1",
"fs": "latest",
"lucide-react": "^0.454.0",
"next": "^15.3.5",
"next-themes": "^0.4.4",
"react": "^19.1.0",
"next": "14.2.16",
"next-themes": "latest",
"path": "latest",
"react": "^18",
"react-day-picker": "latest",
"react-dom": "^19.1.0",
"react-hook-form": "^7.54.1",
"react-is": "^19.1.0",
"react-resizable-panels": "^2.1.7",
"react-dom": "^18",
"react-hook-form": "^7.48.2",
"recharts": "latest",
"regenerator-runtime": "latest",
"sonner": "^1.7.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.6",
"zod": "^3.24.1"
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.0.4",
"postcss": "^8.5",
"tailwindcss": "^3.4.17",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
}

7346
Cunkebao/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff