超级管理员后台

This commit is contained in:
柳清爽
2025-04-09 09:45:06 +08:00
parent 282a301eeb
commit 3a01dc897a
86 changed files with 7535 additions and 4 deletions

View File

@@ -0,0 +1,137 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { Checkbox } from "@/components/ui/checkbox"
// Sample admin data for editing
const adminData = {
id: "2",
username: "admin_li",
name: "李管理",
permissions: ["project_management", "customer_pool"],
}
export default function EditAdminPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const [username, setUsername] = useState(adminData.username)
const [name, setName] = useState(adminData.name)
const permissions = [
{ id: "project_management", label: "项目管理" },
{ id: "customer_pool", label: "客户池" },
{ id: "admin_management", label: "管理员权限" },
]
const [selectedPermissions, setSelectedPermissions] = useState<string[]>(adminData.permissions)
const togglePermission = (permissionId: string) => {
setSelectedPermissions((prev) =>
prev.includes(permissionId) ? prev.filter((id) => id !== permissionId) : [...prev, permissionId],
)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
router.push("/dashboard/admins")
}, 1500)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/dashboard/admins">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold"></h1>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="请输入账号"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="请输入姓名"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input id="password" type="password" placeholder="留空则不修改密码" />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"></Label>
<Input id="confirmPassword" type="password" placeholder="留空则不修改密码" />
</div>
</div>
<div className="space-y-3">
<Label></Label>
<div className="grid gap-2">
{permissions.map((permission) => (
<div key={permission.id} className="flex items-center space-x-2">
<Checkbox
id={permission.id}
checked={selectedPermissions.includes(permission.id)}
onCheckedChange={() => togglePermission(permission.id)}
/>
<Label htmlFor={permission.id} className="cursor-pointer">
{permission.label}
</Label>
</div>
))}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline" asChild>
<Link href="/dashboard/admins"></Link>
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "保存中..." : "保存修改"}
</Button>
</CardFooter>
</Card>
</form>
</div>
)
}

View File

@@ -0,0 +1,4 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,115 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft } from "lucide-react"
import Link from "next/link"
import { Checkbox } from "@/components/ui/checkbox"
export default function NewAdminPage() {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const permissions = [
{ id: "project_management", label: "项目管理" },
{ id: "customer_pool", label: "客户池" },
{ id: "admin_management", label: "管理员权限" },
]
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([])
const togglePermission = (permissionId: string) => {
setSelectedPermissions((prev) =>
prev.includes(permissionId) ? prev.filter((id) => id !== permissionId) : [...prev, permissionId],
)
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
router.push("/dashboard/admins")
}, 1500)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/dashboard/admins">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold"></h1>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input id="username" placeholder="请输入账号" required />
</div>
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" placeholder="请输入姓名" required />
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input id="password" type="password" placeholder="请设置密码" required />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"></Label>
<Input id="confirmPassword" type="password" placeholder="请再次输入密码" required />
</div>
</div>
<div className="space-y-3">
<Label></Label>
<div className="grid gap-2">
{permissions.map((permission) => (
<div key={permission.id} className="flex items-center space-x-2">
<Checkbox
id={permission.id}
checked={selectedPermissions.includes(permission.id)}
onCheckedChange={() => togglePermission(permission.id)}
/>
<Label htmlFor={permission.id} className="cursor-pointer">
{permission.label}
</Label>
</div>
))}
</div>
</div>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline" asChild>
<Link href="/dashboard/admins"></Link>
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "创建中..." : "创建管理员"}
</Button>
</CardFooter>
</Card>
</form>
</div>
)
}

View File

@@ -0,0 +1,154 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Search, MoreHorizontal, Edit, Trash, UserPlus } from "lucide-react"
import { Badge } from "@/components/ui/badge"
// Sample admin data
const adminsData = [
{
id: "1",
username: "admin_zhang",
name: "张管理",
role: "超级管理员",
permissions: ["项目管理", "客户池", "管理员权限"],
createdAt: "2023-05-01",
lastLogin: "2023-06-28 09:15",
},
{
id: "2",
username: "admin_li",
name: "李管理",
role: "项目管理员",
permissions: ["项目管理", "客户池"],
createdAt: "2023-05-10",
lastLogin: "2023-06-27 14:30",
},
{
id: "3",
username: "admin_wang",
name: "王管理",
role: "客户管理员",
permissions: ["客户池"],
createdAt: "2023-05-15",
lastLogin: "2023-06-28 11:45",
},
{
id: "4",
username: "admin_zhao",
name: "赵管理",
role: "项目管理员",
permissions: ["项目管理"],
createdAt: "2023-05-20",
lastLogin: "2023-06-26 16:20",
},
]
export default function AdminsPage() {
const [searchTerm, setSearchTerm] = useState("")
const filteredAdmins = adminsData.filter(
(admin) =>
admin.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
admin.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
admin.role.toLowerCase().includes(searchTerm.toLowerCase()),
)
return (
<div className="space-y-6">
<div className="flex justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button asChild>
<Link href="/dashboard/admins/new">
<UserPlus className="mr-2 h-4 w-4" />
</Link>
</Button>
</div>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索管理员账号、姓名或角色..."
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredAdmins.length > 0 ? (
filteredAdmins.map((admin) => (
<TableRow key={admin.id}>
<TableCell className="font-medium">{admin.username}</TableCell>
<TableCell>{admin.name}</TableCell>
<TableCell>
<Badge variant={admin.role === "超级管理员" ? "default" : "outline"}>{admin.role}</Badge>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{admin.permissions.map((permission) => (
<Badge key={permission} variant="secondary">
{permission}
</Badge>
))}
</div>
</TableCell>
<TableCell>{admin.createdAt}</TableCell>
<TableCell>{admin.lastLogin}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/dashboard/admins/${admin.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="h-24 text-center">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -0,0 +1,202 @@
"use client"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { ArrowLeft, MessageSquare, Phone, UserPlus } from "lucide-react"
// Sample customer data
const customerData = {
id: "1",
name: "张三",
avatar: "/placeholder.svg?height=100&width=100",
wechatId: "zhangsan123",
gender: "男",
region: "北京",
source: "微信搜索",
tags: ["潜在客户", "高消费"],
projectName: "电商平台项目",
addedDate: "2023-06-10",
devices: [
{ id: "d1", name: "iPhone 13 Pro", addedDate: "2023-06-10" },
{ id: "d2", name: "MacBook Pro", addedDate: "2023-06-12" },
],
interactions: [
{
id: "i1",
type: "消息",
content: "您好,我对您的产品很感兴趣,能详细介绍一下吗?",
date: "2023-06-15 14:30",
},
{
id: "i2",
type: "电话",
content: "讨论产品细节和价格",
duration: "15分钟",
date: "2023-06-16 10:45",
},
{
id: "i3",
type: "消息",
content: "谢谢您的详细介绍,我考虑一下再联系您。",
date: "2023-06-16 16:20",
},
{
id: "i4",
type: "消息",
content: "我决定购买您的产品,请问如何下单?",
date: "2023-06-18 09:15",
},
],
transactions: [
{
id: "t1",
product: "高级会员套餐",
amount: "¥1,299",
status: "已完成",
date: "2023-06-20",
},
],
}
export default function CustomerDetailPage({ params }: { params: { id: string } }) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/dashboard/customers">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold"></h1>
</div>
<Button>
<UserPlus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="grid gap-6 md:grid-cols-3">
<Card className="md:col-span-1">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center text-center">
<Avatar className="h-24 w-24 mb-4">
<AvatarImage src={customerData.avatar} alt={customerData.name} />
<AvatarFallback>{customerData.name.slice(0, 2)}</AvatarFallback>
</Avatar>
<h3 className="text-xl font-bold">{customerData.name}</h3>
<p className="text-sm text-muted-foreground mb-4">{customerData.wechatId}</p>
<div className="flex flex-wrap justify-center gap-1 mb-6">
{customerData.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
<div className="w-full space-y-2 text-left">
<div className="flex justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{customerData.gender}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{customerData.region}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{customerData.source}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{customerData.projectName}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-muted-foreground"></span>
<span className="text-sm">{customerData.addedDate}</span>
</div>
</div>
</CardContent>
</Card>
<Card className="md:col-span-2">
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="interactions" className="space-y-4">
<TabsList>
<TabsTrigger value="interactions"></TabsTrigger>
<TabsTrigger value="devices"></TabsTrigger>
<TabsTrigger value="transactions"></TabsTrigger>
</TabsList>
<TabsContent value="interactions" className="space-y-4">
{customerData.interactions.map((interaction) => (
<div key={interaction.id} className="flex gap-4 pb-4 border-b last:border-0">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted">
{interaction.type === "消息" ? (
<MessageSquare className="h-5 w-5" />
) : (
<Phone className="h-5 w-5" />
)}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center justify-between">
<p className="text-sm font-medium">{interaction.type}</p>
<p className="text-xs text-muted-foreground">{interaction.date}</p>
</div>
<p className="text-sm">{interaction.content}</p>
{interaction.duration && (
<p className="text-xs text-muted-foreground">: {interaction.duration}</p>
)}
</div>
</div>
))}
</TabsContent>
<TabsContent value="devices">
{customerData.devices.map((device) => (
<div key={device.id} className="flex items-center justify-between py-2 border-b last:border-0">
<div>
<p className="font-medium">{device.name}</p>
<p className="text-xs text-muted-foreground">: {device.addedDate}</p>
</div>
</div>
))}
</TabsContent>
<TabsContent value="transactions">
{customerData.transactions.length > 0 ? (
customerData.transactions.map((transaction) => (
<div key={transaction.id} className="flex items-center justify-between py-2 border-b last:border-0">
<div>
<p className="font-medium">{transaction.product}</p>
<p className="text-xs text-muted-foreground">: {transaction.date}</p>
</div>
<div className="text-right">
<p className="font-medium">{transaction.amount}</p>
<Badge variant={transaction.status === "已完成" ? "success" : "outline"}>
{transaction.status}
</Badge>
</div>
</div>
))
) : (
<p className="text-center text-muted-foreground py-4"></p>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,4 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,310 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Search, MoreHorizontal, Eye, UserPlus, Filter } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
// Sample customer data
const customersData = [
{
id: "1",
name: "张三",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "zhangsan123",
gender: "男",
region: "北京",
source: "微信搜索",
tags: ["潜在客户", "高消费"],
projectName: "电商平台项目",
addedDate: "2023-06-10",
},
{
id: "2",
name: "李四",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "lisi456",
gender: "男",
region: "上海",
source: "朋友推荐",
tags: ["活跃用户"],
projectName: "社交媒体营销",
addedDate: "2023-06-12",
},
{
id: "3",
name: "王五",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "wangwu789",
gender: "男",
region: "广州",
source: "广告点击",
tags: ["新用户"],
projectName: "企业官网推广",
addedDate: "2023-06-15",
},
{
id: "4",
name: "赵六",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "zhaoliu321",
gender: "男",
region: "深圳",
source: "线下活动",
tags: ["高消费", "忠诚客户"],
projectName: "教育平台项目",
addedDate: "2023-06-18",
},
{
id: "5",
name: "钱七",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "qianqi654",
gender: "女",
region: "成都",
source: "微信群",
tags: ["潜在客户"],
projectName: "金融服务推广",
addedDate: "2023-06-20",
},
{
id: "6",
name: "孙八",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "sunba987",
gender: "女",
region: "武汉",
source: "微信搜索",
tags: ["活跃用户", "高消费"],
projectName: "电商平台项目",
addedDate: "2023-06-22",
},
{
id: "7",
name: "周九",
avatar: "/placeholder.svg?height=40&width=40",
wechatId: "zhoujiu135",
gender: "女",
region: "杭州",
source: "朋友推荐",
tags: ["新用户"],
projectName: "社交媒体营销",
addedDate: "2023-06-25",
},
]
export default function CustomersPage() {
const [searchTerm, setSearchTerm] = useState("")
const [selectedRegion, setSelectedRegion] = useState("")
const [selectedGender, setSelectedGender] = useState("")
const [selectedSource, setSelectedSource] = useState("")
const [selectedProject, setSelectedProject] = useState("")
// Filter customers based on search and filters
const filteredCustomers = customersData.filter((customer) => {
const matchesSearch =
customer.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
customer.wechatId.toLowerCase().includes(searchTerm.toLowerCase())
const matchesRegion = selectedRegion ? customer.region === selectedRegion : true
const matchesGender = selectedGender ? customer.gender === selectedGender : true
const matchesSource = selectedSource ? customer.source === selectedSource : true
const matchesProject = selectedProject ? customer.projectName === selectedProject : true
return matchesSearch && matchesRegion && matchesGender && matchesSource && matchesProject
})
// Get unique values for filters
const regions = [...new Set(customersData.map((c) => c.region))]
const sources = [...new Set(customersData.map((c) => c.source))]
const projects = [...new Set(customersData.map((c) => c.projectName))]
return (
<div className="space-y-6">
<div className="flex justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button>
<UserPlus className="mr-2 h-4 w-4" />
</Button>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索客户名称或微信ID..."
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Filter className="mr-2 h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<div className="p-2">
<p className="mb-2 text-sm font-medium"></p>
<Select value={selectedRegion} onValueChange={setSelectedRegion}>
<SelectTrigger>
<SelectValue placeholder="所有地区" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{regions.map((region) => (
<SelectItem key={region} value={region}>
{region}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DropdownMenuSeparator />
<div className="p-2">
<p className="mb-2 text-sm font-medium"></p>
<Select value={selectedGender} onValueChange={setSelectedGender}>
<SelectTrigger>
<SelectValue placeholder="所有性别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="男"></SelectItem>
<SelectItem value="女"></SelectItem>
</SelectContent>
</Select>
</div>
<DropdownMenuSeparator />
<div className="p-2">
<p className="mb-2 text-sm font-medium"></p>
<Select value={selectedSource} onValueChange={setSelectedSource}>
<SelectTrigger>
<SelectValue placeholder="所有来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{sources.map((source) => (
<SelectItem key={source} value={source}>
{source}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<DropdownMenuSeparator />
<div className="p-2">
<p className="mb-2 text-sm font-medium"></p>
<Select value={selectedProject} onValueChange={setSelectedProject}>
<SelectTrigger>
<SelectValue placeholder="所有项目" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{projects.map((project) => (
<SelectItem key={project} value={project}>
{project}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCustomers.length > 0 ? (
filteredCustomers.map((customer) => (
<TableRow key={customer.id}>
<TableCell>
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src={customer.avatar} alt={customer.name} />
<AvatarFallback>{customer.name.slice(0, 2)}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{customer.name}</div>
<div className="text-xs text-muted-foreground">{customer.gender}</div>
</div>
</div>
</TableCell>
<TableCell>{customer.wechatId}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{customer.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
</TableCell>
<TableCell>{customer.region}</TableCell>
<TableCell>{customer.source}</TableCell>
<TableCell>{customer.projectName}</TableCell>
<TableCell>{customer.addedDate}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/dashboard/customers/${customer.id}`}>
<Eye className="mr-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<UserPlus className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={8} className="h-24 text-center">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,101 @@
"use client"
import type React from "react"
import { useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Button } from "@/components/ui/button"
import { LayoutDashboard, Users, Settings, LogOut, Menu, X } from "lucide-react"
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(true)
const pathname = usePathname()
const navItems = [
{
title: "项目管理",
href: "/dashboard/projects",
icon: <LayoutDashboard className="h-5 w-5" />,
},
{
title: "客户池",
href: "/dashboard/customers",
icon: <Users className="h-5 w-5" />,
},
{
title: "管理员权限",
href: "/dashboard/admins",
icon: <Settings className="h-5 w-5" />,
},
]
return (
<div className="flex h-screen overflow-hidden">
{/* Mobile sidebar toggle */}
<div className="fixed top-4 left-4 z-50 md:hidden">
<Button variant="outline" size="icon" onClick={() => setSidebarOpen(!sidebarOpen)}>
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</Button>
</div>
{/* Sidebar */}
<div
className={`bg-primary text-primary-foreground w-64 flex-shrink-0 transition-all duration-300 ease-in-out ${
sidebarOpen ? "translate-x-0" : "-translate-x-full"
} md:translate-x-0 fixed md:relative z-40 h-full`}
>
<div className="flex flex-col h-full">
<div className="flex items-center justify-center h-16 border-b border-primary/10">
<h1 className="text-xl font-bold"></h1>
</div>
<nav className="flex-1 overflow-y-auto py-4">
<ul className="space-y-1 px-2">
{navItems.map((item) => (
<li key={item.href}>
<Link
href={item.href}
className={`flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors hover:bg-primary-foreground hover:text-primary ${
pathname.startsWith(item.href) ? "bg-primary-foreground text-primary" : ""
}`}
>
{item.icon}
{item.title}
</Link>
</li>
))}
</ul>
</nav>
<div className="border-t border-primary/10 p-4">
<Button
variant="outline"
className="w-full justify-start gap-2 bg-transparent text-primary-foreground hover:bg-primary-foreground hover:text-primary"
onClick={() => {
// Handle logout
window.location.href = "/login"
}}
>
<LogOut className="h-5 w-5" />
退
</Button>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 overflow-y-auto">
<header className="h-16 border-b flex items-center px-6 bg-background">
<h2 className="text-lg font-medium">
{navItems.find((item) => pathname.startsWith(item.href))?.title || "仪表盘"}
</h2>
</header>
<main className="p-6">{children}</main>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Users, FolderKanban, UserCog } from "lucide-react"
export default function DashboardPage() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold">使</h1>
<p className="text-muted-foreground"></p>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<FolderKanban className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24</div>
<p className="text-xs text-muted-foreground"> 12%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,284</div>
<p className="text-xs text-muted-foreground"> 8%</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<UserCog className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">8</div>
<p className="text-xs text-muted-foreground"> 2 </p>
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,159 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Plus, Trash } from "lucide-react"
import Link from "next/link"
// Sample project data for editing
const projectData = {
id: "1",
name: "电商平台项目",
phone: "13800138000",
account: "ecommerce_admin",
description: "这是一个电商平台推广项目,主要针对年轻用户群体,通过微信社交渠道进行产品推广和销售转化。",
devices: [
{ id: "d1", name: "iPhone 13 Pro" },
{ id: "d2", name: "Huawei P40" },
{ id: "d3", name: "Samsung S21" },
],
}
export default function EditProjectPage({ params }: { params: { id: string } }) {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const [projectName, setProjectName] = useState(projectData.name)
const [phone, setPhone] = useState(projectData.phone)
const [account, setAccount] = useState(projectData.account)
const [description, setDescription] = useState(projectData.description)
const [devices, setDevices] = useState(projectData.devices)
const handleAddDevice = () => {
setDevices([...devices, { id: Date.now().toString(), name: "" }])
}
const handleRemoveDevice = (id: string) => {
setDevices(devices.filter((device) => device.id !== id))
}
const handleDeviceChange = (id: string, value: string) => {
setDevices(devices.map((device) => (device.id === id ? { ...device, name: value } : device)))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
router.push(`/dashboard/projects/${params.id}`)
}, 1500)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href={`/dashboard/projects/${params.id}`}>
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold"></h1>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="projectName"></Label>
<Input
id="projectName"
value={projectName}
onChange={(e) => setProjectName(e.target.value)}
placeholder="请输入项目名称"
required
/>
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="请输入手机号"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="account"></Label>
<Input
id="account"
value={account}
onChange={(e) => setAccount(e.target.value)}
placeholder="请输入账号"
required
/>
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="space-y-3">
{devices.map((device, index) => (
<div key={device.id} className="flex items-center gap-2">
<Input
placeholder={`设备 ${index + 1} 名称`}
value={device.name}
onChange={(e) => handleDeviceChange(device.id, e.target.value)}
/>
<Button type="button" variant="outline" size="icon" onClick={() => handleRemoveDevice(device.id)}>
<Trash className="h-4 w-4" />
</Button>
</div>
))}
<Button type="button" variant="outline" onClick={handleAddDevice} className="flex items-center gap-1">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="请输入项目介绍(选填)"
rows={4}
/>
</div>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline" asChild>
<Link href={`/dashboard/projects/${params.id}`}></Link>
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "保存中..." : "保存修改"}
</Button>
</CardFooter>
</Card>
</form>
</div>
)
}

View File

@@ -0,0 +1,179 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ArrowLeft, Edit } from "lucide-react"
// Sample project data
const projectData = {
id: "1",
name: "电商平台项目",
phone: "13800138000",
account: "ecommerce_admin",
description: "这是一个电商平台推广项目,主要针对年轻用户群体,通过微信社交渠道进行产品推广和销售转化。",
createdAt: "2023-05-15",
devices: [
{ id: "d1", name: "iPhone 13 Pro", wechatFriends: 120 },
{ id: "d2", name: "Huawei P40", wechatFriends: 85 },
{ id: "d3", name: "Samsung S21", wechatFriends: 40 },
],
subAccounts: [
{ id: "a1", username: "sales_team1", createdAt: "2023-05-16" },
{ id: "a2", username: "sales_team2", createdAt: "2023-05-16" },
{ id: "a3", username: "marketing_team", createdAt: "2023-05-17" },
{ id: "a4", username: "support_team", createdAt: "2023-05-18" },
{ id: "a5", username: "admin_assistant", createdAt: "2023-05-20" },
],
}
export default function ProjectDetailPage({ params }: { params: { id: string } }) {
const [activeTab, setActiveTab] = useState("overview")
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/dashboard/projects">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold">{projectData.name}</h1>
</div>
<Button asChild>
<Link href={`/dashboard/projects/${params.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
</Link>
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="overview"></TabsTrigger>
<TabsTrigger value="devices"></TabsTrigger>
<TabsTrigger value="accounts"></TabsTrigger>
</TabsList>
<TabsContent value="overview" className="space-y-4">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm">{projectData.name}</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm">{projectData.phone}</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm">{projectData.account}</dd>
</div>
<div>
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm">{projectData.createdAt}</dd>
</div>
<div className="sm:col-span-2">
<dt className="text-sm font-medium text-muted-foreground"></dt>
<dd className="text-sm">{projectData.description}</dd>
</div>
</dl>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projectData.devices.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projectData.subAccounts.length}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{projectData.devices.reduce((sum, device) => sum + device.wechatFriends, 0)}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="devices">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectData.devices.map((device) => (
<TableRow key={device.id}>
<TableCell className="font-medium">{device.name}</TableCell>
<TableCell className="text-right">{device.wechatFriends}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="accounts">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectData.subAccounts.map((account) => (
<TableRow key={account.id}>
<TableCell className="font-medium">{account.username}</TableCell>
<TableCell className="text-right">{account.createdAt}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
)
}

View File

@@ -0,0 +1,4 @@
export default function Loading() {
return null
}

View File

@@ -0,0 +1,129 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Label } from "@/components/ui/label"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { ArrowLeft, Plus, Trash } from "lucide-react"
import Link from "next/link"
export default function NewProjectPage() {
const router = useRouter()
const [isSubmitting, setIsSubmitting] = useState(false)
const [devices, setDevices] = useState([{ id: "1", name: "" }])
const handleAddDevice = () => {
setDevices([...devices, { id: Date.now().toString(), name: "" }])
}
const handleRemoveDevice = (id: string) => {
setDevices(devices.filter((device) => device.id !== id))
}
const handleDeviceChange = (id: string, value: string) => {
setDevices(devices.map((device) => (device.id === id ? { ...device, name: value } : device)))
}
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
router.push("/dashboard/projects")
}, 1500)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" asChild>
<Link href="/dashboard/projects">
<ArrowLeft className="h-4 w-4" />
</Link>
</Button>
<h1 className="text-2xl font-bold"></h1>
</div>
<form onSubmit={handleSubmit}>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="projectName"></Label>
<Input id="projectName" placeholder="请输入项目名称" required />
</div>
<div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input id="phone" placeholder="请输入手机号" required />
</div>
<div className="space-y-2">
<Label htmlFor="account"></Label>
<Input id="account" placeholder="请输入账号" required />
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input id="password" type="password" placeholder="请设置初始密码" required />
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword"></Label>
<Input id="confirmPassword" type="password" placeholder="请再次输入密码" required />
</div>
</div>
<div className="space-y-2">
<Label></Label>
<div className="space-y-3">
{devices.map((device, index) => (
<div key={device.id} className="flex items-center gap-2">
<Input
placeholder={`设备 ${index + 1} 名称`}
value={device.name}
onChange={(e) => handleDeviceChange(device.id, e.target.value)}
/>
{devices.length > 1 && (
<Button type="button" variant="outline" size="icon" onClick={() => handleRemoveDevice(device.id)}>
<Trash className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button type="button" variant="outline" onClick={handleAddDevice} className="flex items-center gap-1">
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description"></Label>
<Textarea id="description" placeholder="请输入项目介绍(选填)" rows={4} />
</div>
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button variant="outline" asChild>
<Link href="/dashboard/projects"></Link>
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "创建中..." : "创建项目"}
</Button>
</CardFooter>
</Card>
</form>
</div>
)
}

View File

@@ -0,0 +1,147 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Plus, Search, MoreHorizontal, Edit, Eye, Trash } from "lucide-react"
// Sample project data
const projectsData = [
{
id: "1",
name: "电商平台项目",
phone: "13800138000",
accountCount: 5,
deviceCount: 12,
wechatFriends: 245,
},
{
id: "2",
name: "社交媒体营销",
phone: "13900139000",
accountCount: 8,
deviceCount: 20,
wechatFriends: 567,
},
{
id: "3",
name: "企业官网推广",
phone: "13700137000",
accountCount: 3,
deviceCount: 8,
wechatFriends: 120,
},
{
id: "4",
name: "教育平台项目",
phone: "13600136000",
accountCount: 10,
deviceCount: 25,
wechatFriends: 780,
},
{
id: "5",
name: "金融服务推广",
phone: "13500135000",
accountCount: 6,
deviceCount: 15,
wechatFriends: 320,
},
]
export default function ProjectsPage() {
const [searchTerm, setSearchTerm] = useState("")
const filteredProjects = projectsData.filter(
(project) => project.name.toLowerCase().includes(searchTerm.toLowerCase()) || project.phone.includes(searchTerm),
)
return (
<div className="space-y-6">
<div className="flex justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button asChild>
<Link href="/dashboard/projects/new">
<Plus className="mr-2 h-4 w-4" />
</Link>
</Button>
</div>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="搜索项目名称或手机号..."
className="pl-8"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<TableRow key={project.id}>
<TableCell className="font-medium">{project.name}</TableCell>
<TableCell>{project.phone}</TableCell>
<TableCell className="text-center">{project.deviceCount}</TableCell>
<TableCell className="text-center">{project.accountCount}</TableCell>
<TableCell className="text-center">{project.wechatFriends}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only"></span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/dashboard/projects/${project.id}`}>
<Eye className="mr-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/dashboard/projects/${project.id}/edit`}>
<Edit className="mr-2 h-4 w-4" />
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash className="mr-2 h-4 w-4" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="h-24 text-center">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -0,0 +1,87 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
--sidebar: 221.2 83.2% 53.3%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 0 0% 100%;
--sidebar-primary-foreground: 222.2 84% 4.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
--sidebar: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 217.2 91.2% 59.8%;
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

33
SuperAdmin/app/layout.tsx Normal file
View File

@@ -0,0 +1,33 @@
import type React from "react"
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ThemeProvider } from "@/components/theme-provider"
const inter = Inter({ subsets: ["latin"] })
export const metadata: Metadata = {
title: "超级管理员后台系统",
description: "管理项目、客户和管理员权限的综合平台",
generator: 'v0.dev'
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
)
}
import './globals.css'

View File

@@ -0,0 +1,70 @@
"use client"
import type React from "react"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label"
export default function LoginPage() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
// Simulate login API call
setTimeout(() => {
setIsLoading(false)
router.push("/dashboard")
}, 1500)
}
return (
<div className="flex h-screen w-full items-center justify-center bg-gray-50">
<Card className="w-[400px]">
<CardHeader className="space-y-1">
<CardTitle className="text-2xl text-center"></CardTitle>
<CardDescription className="text-center"></CardDescription>
</CardHeader>
<form onSubmit={handleLogin}>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username"></Label>
<Input
id="username"
placeholder="请输入账号"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
placeholder="请输入密码"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
</CardContent>
<CardFooter>
<Button className="w-full" type="submit" disabled={isLoading}>
{isLoading ? "登录中..." : "登录"}
</Button>
</CardFooter>
</form>
</Card>
</div>
)
}