feat: 本次提交更新内容如下
更新了旧项目的代码和样式
This commit is contained in:
@@ -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作为统一布局
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,3 @@ export function BindDouyinQRCode() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,4 +45,3 @@ export function LineChart({ data, xField, yField }: LineChartProps) {
|
||||
|
||||
return <Line {...config} />
|
||||
}
|
||||
|
||||
|
||||
@@ -147,4 +147,3 @@ export function TrafficTeamSettings({ formData, onChange }: TrafficTeamSettingsP
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -61,4 +61,3 @@ export function VideoTutorialButton() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
114
Cunkebao/components/acquisition/ExpandableAcquisitionCard.tsx
Normal file
114
Cunkebao/components/acquisition/ExpandableAcquisitionCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -100,4 +100,3 @@ export function PlanSettingsDialog({ open, onOpenChange, planId }: PlanSettingsD
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
187
Cunkebao/components/common/DeviceSelector.tsx
Normal file
187
Cunkebao/components/common/DeviceSelector.tsx
Normal 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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
98
Cunkebao/components/device-table.tsx
Normal file
98
Cunkebao/components/device-table.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -9,4 +9,3 @@ export function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
export default AppleIcon
|
||||
|
||||
|
||||
@@ -10,4 +10,3 @@ export function WeChatIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
}
|
||||
|
||||
export default WeChatIcon
|
||||
|
||||
|
||||
325
Cunkebao/components/login-form.tsx
Normal file
325
Cunkebao/components/login-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -82,4 +82,3 @@ export function PosterSelector({ open, onOpenChange, onSelect }: PosterSelectorP
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
45
Cunkebao/components/settings-dropdown.tsx
Normal file
45
Cunkebao/components/settings-dropdown.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
))
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root
|
||||
|
||||
87
Cunkebao/components/ui/date-picker.tsx
Normal file
87
Cunkebao/components/ui/date-picker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -49,11 +49,9 @@ export function DateRangePicker({ className, value, onChange }: DateRangePickerP
|
||||
onSelect={onChange}
|
||||
numberOfMonths={2}
|
||||
locale={zhCN}
|
||||
showOutsideDays={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -32,4 +32,3 @@ export function PreviewDialog({ children, title = "预览效果" }: PreviewDialo
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
54
Cunkebao/components/ui/steps.tsx
Normal file
54
Cunkebao/components/ui/steps.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
142
Cunkebao/next.config.mjs
Normal 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
|
||||
}
|
||||
@@ -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
7346
Cunkebao/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user