Merge branch 'develop' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into develop
This commit is contained in:
@@ -21,46 +21,6 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
|
||||
// 保留原始示例数据,作为加载失败时的备用数据
|
||||
const adminsData = [
|
||||
{
|
||||
id: "1",
|
||||
account: "admin_zhang",
|
||||
username: "张管理",
|
||||
role: "超级管理员",
|
||||
permissions: ["项目管理", "客户池", "管理员权限"],
|
||||
createdAt: "2023-05-01",
|
||||
lastLogin: "2023-06-28 09:15",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
account: "admin_li",
|
||||
username: "李管理",
|
||||
role: "项目管理员",
|
||||
permissions: ["项目管理", "客户池"],
|
||||
createdAt: "2023-05-10",
|
||||
lastLogin: "2023-06-27 14:30",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
account: "admin_wang",
|
||||
username: "王管理",
|
||||
role: "客户管理员",
|
||||
permissions: ["客户池"],
|
||||
createdAt: "2023-05-15",
|
||||
lastLogin: "2023-06-28 11:45",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
account: "admin_zhao",
|
||||
username: "赵管理",
|
||||
role: "项目管理员",
|
||||
permissions: ["项目管理"],
|
||||
createdAt: "2023-05-20",
|
||||
lastLogin: "2023-06-26 16:20",
|
||||
},
|
||||
]
|
||||
|
||||
export default function AdminsPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@@ -95,14 +55,8 @@ export default function AdminsPage() {
|
||||
description: response.msg || "请稍后重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
// 加载失败时显示示例数据
|
||||
setAdministrators(adminsData.map(admin => ({
|
||||
...admin,
|
||||
id: Number(admin.id),
|
||||
name: admin.username,
|
||||
status: 1
|
||||
})))
|
||||
setTotalCount(adminsData.length)
|
||||
setAdministrators([])
|
||||
setTotalCount(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取管理员列表出错:", error)
|
||||
@@ -111,14 +65,8 @@ export default function AdminsPage() {
|
||||
description: "请检查网络连接后重试",
|
||||
variant: "destructive",
|
||||
})
|
||||
// 加载失败时显示示例数据
|
||||
setAdministrators(adminsData.map(admin => ({
|
||||
...admin,
|
||||
id: Number(admin.id),
|
||||
name: admin.username,
|
||||
status: 1
|
||||
})))
|
||||
setTotalCount(adminsData.length)
|
||||
setAdministrators([])
|
||||
setTotalCount(0)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -20,94 +20,6 @@ import { getTrafficPoolList } from "@/lib/traffic-pool-api"
|
||||
import { Customer } from "@/lib/traffic-pool-api"
|
||||
import { PaginationControls } from "@/components/ui/pagination-controls"
|
||||
|
||||
// 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("")
|
||||
@@ -162,25 +74,6 @@ export default function CustomersPage() {
|
||||
setCurrentPage(1); // Reset to first page when page size changes
|
||||
};
|
||||
|
||||
// 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">
|
||||
@@ -217,11 +110,7 @@ export default function CustomersPage() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有地区</SelectItem>
|
||||
{regions.map((region) => (
|
||||
<SelectItem key={region} value={region}>
|
||||
{region}
|
||||
</SelectItem>
|
||||
))}
|
||||
{/* 从API获取到的regions数据应在此处映射 */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -248,28 +137,20 @@ export default function CustomersPage() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">所有来源</SelectItem>
|
||||
{sources.map((source) => (
|
||||
<SelectItem key={source} value={source}>
|
||||
{source}
|
||||
</SelectItem>
|
||||
))}
|
||||
{/* 从API获取到的sources数据应在此处映射 */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="p-2">
|
||||
<p className="mb-2 text-sm font-medium">所属项目</p>
|
||||
<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>
|
||||
))}
|
||||
{/* 从API获取到的projects数据应在此处映射 */}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -277,16 +158,16 @@ export default function CustomersPage() {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>客户昵称</TableHead>
|
||||
<TableHead>微信ID</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>性别</TableHead>
|
||||
<TableHead>地区</TableHead>
|
||||
<TableHead>来源</TableHead>
|
||||
<TableHead>公司名称</TableHead>
|
||||
<TableHead>标签</TableHead>
|
||||
<TableHead>所属项目</TableHead>
|
||||
<TableHead>添加时间</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
@@ -294,45 +175,45 @@ export default function CustomersPage() {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
加载中...
|
||||
</TableCell>
|
||||
<TableCell colSpan={8} className="text-center py-4">加载中...</TableCell>
|
||||
</TableRow>
|
||||
) : error ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center text-red-600">
|
||||
{error}
|
||||
</TableCell>
|
||||
<TableCell colSpan={8} className="text-center py-4 text-red-500">{error}</TableCell>
|
||||
</TableRow>
|
||||
) : customers.length > 0 ? (
|
||||
) : customers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-4">没有符合条件的客户</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
customers.map((customer) => (
|
||||
<TableRow key={customer.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src={customer.avatar || "/placeholder.svg?height=40&width=40"}
|
||||
alt={customer.nickname || "未知"}
|
||||
onError={(e) => {
|
||||
// 图片加载失败时使用默认图片
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = "/placeholder.svg?height=40&width=40";
|
||||
}}
|
||||
/>
|
||||
<AvatarFallback>{(customer.nickname || "未知").slice(0, 2)}</AvatarFallback>
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarImage src={customer.avatar || "/placeholder.svg?height=40&width=40"} alt={customer.nickname} />
|
||||
<AvatarFallback>{customer.nickname ? customer.nickname.substring(0, 1) : "?"}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-medium">{customer.nickname || "未知"}</div>
|
||||
<div className="text-xs text-muted-foreground">{customer.gender}</div>
|
||||
<div className="font-medium">{customer.nickname}</div>
|
||||
<div className="text-sm text-muted-foreground">{customer.wechatId}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{customer.wechatId}</TableCell>
|
||||
<TableCell>{customer.gender}</TableCell>
|
||||
<TableCell>{customer.region}</TableCell>
|
||||
<TableCell>{customer.source}</TableCell>
|
||||
<TableCell>{customer.projectName}</TableCell>
|
||||
<TableCell>{customer.addTime}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{customer.tags && customer.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{customer.companyName}</TableCell>
|
||||
<TableCell>{customer.createTime}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -342,39 +223,41 @@ export default function CustomersPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/customers/${customer.id}`}>
|
||||
<Eye className="mr-2 h-4 w-4" /> 查看详情
|
||||
<DropdownMenuItem>
|
||||
<Link href={`/dashboard/customers/${customer.id}`} className="flex items-center w-full">
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
查看详情
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<UserPlus className="mr-2 h-4 w-4" /> 分发客户
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>分配客服</DropdownMenuItem>
|
||||
<DropdownMenuItem>添加标签</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="h-24 text-center">
|
||||
未找到客户
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 使用新的分页组件 */}
|
||||
<PaginationControls
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
pageSize={pageSize}
|
||||
totalItems={totalItems}
|
||||
onPageChange={setCurrentPage} // 直接传递setCurrentPage
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
|
||||
{/* 分页控制 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
共 {totalItems} 条记录,第 {currentPage} 页,共 {totalPages} 页
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<PaginationControls
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
pageSize={pageSize}
|
||||
totalItems={totalItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState, useEffect, createContext, useContext } from "react"
|
||||
import { useRouter, usePathname } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Menu, X } from "lucide-react"
|
||||
import { Sidebar } from "@/components/layout/sidebar"
|
||||
import { Header } from "@/components/layout/header"
|
||||
import { getAdminInfo } from "@/lib/utils"
|
||||
|
||||
// 全局标签页管理上下文
|
||||
interface TabData {
|
||||
id: string
|
||||
label: string
|
||||
path: string
|
||||
closable: boolean
|
||||
}
|
||||
|
||||
interface TabContextType {
|
||||
tabs: TabData[]
|
||||
activeTab: string
|
||||
addTab: (tab: Omit<TabData, "id">) => string
|
||||
closeTab: (tabId: string) => void
|
||||
setActiveTab: (tabId: string) => void
|
||||
findTabByPath: (path: string) => TabData | undefined
|
||||
}
|
||||
|
||||
const TabContext = createContext<TabContextType | undefined>(undefined);
|
||||
|
||||
export function useTabContext() {
|
||||
const context = useContext(TabContext);
|
||||
if (!context) {
|
||||
throw new Error("useTabContext must be used within a TabProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
@@ -16,6 +43,122 @@ export default function DashboardLayout({
|
||||
}) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
// 标签页状态管理
|
||||
const [tabs, setTabs] = useState<TabData[]>([
|
||||
{ id: "dashboard", label: "仪表盘", path: "/dashboard", closable: false }
|
||||
])
|
||||
const [activeTab, setActiveTab] = useState("dashboard")
|
||||
|
||||
// 添加标签页
|
||||
const addTab = (tabData: Omit<TabData, "id">) => {
|
||||
const id = `tab-${Date.now()}`
|
||||
const newTab = { id, ...tabData }
|
||||
|
||||
// 检查是否已存在类似标签(基于路径)
|
||||
const existingTab = findTabByPath(tabData.path)
|
||||
|
||||
if (existingTab) {
|
||||
// 如果已存在,激活它
|
||||
setActiveTab(existingTab.id)
|
||||
// 确保导航到该路径(即使路径匹配也强制导航)
|
||||
router.push(tabData.path)
|
||||
return existingTab.id
|
||||
} else {
|
||||
// 如果不存在,添加新标签
|
||||
setTabs(prev => [...prev, newTab])
|
||||
setActiveTab(id)
|
||||
// 确保导航到该路径(即使路径匹配也强制导航)
|
||||
router.push(tabData.path)
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// 设置激活标签(并导航到对应路径)
|
||||
const setActiveTabAndNavigate = (tabId: string) => {
|
||||
setActiveTab(tabId)
|
||||
// 找到标签对应的路径并导航
|
||||
const tab = tabs.find(tab => tab.id === tabId)
|
||||
if (tab) {
|
||||
router.push(tab.path)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (tabId: string) => {
|
||||
// 找到要关闭的标签的索引
|
||||
const tabIndex = tabs.findIndex(tab => tab.id === tabId)
|
||||
|
||||
// 如果标签不存在或者是不可关闭的标签,直接返回
|
||||
if (tabIndex === -1 || !tabs[tabIndex].closable) return
|
||||
|
||||
// 创建新的标签数组,移除要关闭的标签
|
||||
const newTabs = tabs.filter(tab => tab.id !== tabId)
|
||||
setTabs(newTabs)
|
||||
|
||||
// 如果关闭的是当前活动标签,需要激活另一个标签
|
||||
if (activeTab === tabId) {
|
||||
// 优先激活关闭标签左侧的标签,如果没有则激活默认的仪表盘标签
|
||||
const newActiveTab = newTabs[tabIndex - 1]?.id || "dashboard"
|
||||
setActiveTab(newActiveTab)
|
||||
|
||||
// 路由跳转到新激活的标签对应的路径
|
||||
const newActivePath = newTabs.find(tab => tab.id === newActiveTab)?.path || "/dashboard"
|
||||
router.push(newActivePath)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据路径查找标签
|
||||
const findTabByPath = (path: string): TabData | undefined => {
|
||||
return tabs.find(tab => tab.path === path)
|
||||
}
|
||||
|
||||
// 监听路径变化,自动添加标签
|
||||
useEffect(() => {
|
||||
// 不触发/dashboard路径,已有默认标签
|
||||
if (pathname === "/dashboard") {
|
||||
setActiveTab("dashboard")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查当前路径是否已有对应标签
|
||||
const existingTab = findTabByPath(pathname)
|
||||
|
||||
if (existingTab) {
|
||||
// 如果存在,激活它
|
||||
setActiveTab(existingTab.id)
|
||||
} else {
|
||||
// 如果不存在,添加新标签
|
||||
// 生成标签标题
|
||||
let label = "新标签"
|
||||
|
||||
// 根据路径生成更友好的标签名
|
||||
if (pathname.includes("/projects")) {
|
||||
if (pathname === "/dashboard/projects") {
|
||||
label = "项目列表"
|
||||
} else if (pathname.includes("/new")) {
|
||||
label = "新建项目"
|
||||
} else if (pathname.includes("/edit")) {
|
||||
label = "编辑项目"
|
||||
} else {
|
||||
label = "项目详情"
|
||||
}
|
||||
} else if (pathname.includes("/admins")) {
|
||||
label = "管理员"
|
||||
} else if (pathname.includes("/customers")) {
|
||||
label = "客户池"
|
||||
} else if (pathname.includes("/settings")) {
|
||||
label = "系统设置"
|
||||
}
|
||||
|
||||
addTab({
|
||||
label,
|
||||
path: pathname,
|
||||
closable: true
|
||||
})
|
||||
}
|
||||
}, [pathname])
|
||||
|
||||
// 认证检查
|
||||
useEffect(() => {
|
||||
@@ -31,29 +174,72 @@ export default function DashboardLayout({
|
||||
}, [router])
|
||||
|
||||
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>
|
||||
<TabContext.Provider value={{
|
||||
tabs,
|
||||
activeTab,
|
||||
addTab,
|
||||
closeTab,
|
||||
setActiveTab: setActiveTabAndNavigate,
|
||||
findTabByPath
|
||||
}}>
|
||||
<div className="flex h-screen overflow-hidden bg-background">
|
||||
{/* 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-background border-r 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`}
|
||||
>
|
||||
<Sidebar />
|
||||
</div>
|
||||
{/* Sidebar */}
|
||||
<div
|
||||
className={`bg-background 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`}
|
||||
>
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Header />
|
||||
<main className="flex-1 overflow-y-auto p-6">{children}</main>
|
||||
{/* Main content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Header />
|
||||
|
||||
{/* 标签栏 */}
|
||||
<div className="border-b border-border">
|
||||
<div className="flex overflow-x-auto">
|
||||
{tabs.map(tab => (
|
||||
<div
|
||||
key={tab.id}
|
||||
className={`flex items-center px-4 py-2 border-r border-border cursor-pointer ${
|
||||
activeTab === tab.id ? "bg-muted font-medium" : "hover:bg-muted/50"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setActiveTabAndNavigate(tab.id)
|
||||
}}
|
||||
>
|
||||
<span className="truncate max-w-[200px]">{tab.label}</span>
|
||||
{tab.closable && (
|
||||
<button
|
||||
className="ml-2 p-1 rounded-full hover:bg-muted-foreground/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
closeTab(tab.id)
|
||||
}}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useState, useEffect, use } from "react"
|
||||
import * as React from "react"
|
||||
import { useState, useEffect } 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 { ArrowLeft, Plus, Trash, X } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { toast, Toaster } from "sonner"
|
||||
import Image from "next/image"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"
|
||||
|
||||
// 为React.use添加类型声明
|
||||
declare module 'react' {
|
||||
function use<T>(promise: Promise<T>): T;
|
||||
function use<T>(value: T): T;
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: number
|
||||
@@ -28,6 +36,10 @@ interface Project {
|
||||
deviceCount: number
|
||||
friendCount: number
|
||||
userCount: number
|
||||
username: string
|
||||
status: number
|
||||
s2_accountId?: number
|
||||
devices?: Device[]
|
||||
}
|
||||
|
||||
export default function EditProjectPage({ params }: { params: { id: string } }) {
|
||||
@@ -37,7 +49,10 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
const [project, setProject] = useState<Project | null>(null)
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
const { id } = use(params)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [qrCodeData, setQrCodeData] = useState("")
|
||||
const [isAddingDevice, setIsAddingDevice] = useState(false)
|
||||
const { id } = React.use(params)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProject = async () => {
|
||||
@@ -82,8 +97,8 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
account: project?.account,
|
||||
memo: project?.memo,
|
||||
phone: project?.phone,
|
||||
username: nickname,
|
||||
status: parseInt(status),
|
||||
username: project?.username,
|
||||
status: project?.status,
|
||||
...(password && { password })
|
||||
}),
|
||||
})
|
||||
@@ -103,8 +118,42 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddDevice = () => {
|
||||
router.push(`/dashboard/projects/${id}/devices/new`)
|
||||
const handleAddDevice = async () => {
|
||||
if (!project?.s2_accountId) {
|
||||
toast.error("无法添加设备,未找到账号ID")
|
||||
return
|
||||
}
|
||||
|
||||
setIsAddingDevice(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/v1/api/device/add`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accountId: project.s2_accountId
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200 && data.data?.qrCode) {
|
||||
setQrCodeData(data.data.qrCode)
|
||||
setIsModalOpen(true)
|
||||
} else {
|
||||
toast.error(data.msg || "获取设备二维码失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsAddingDevice(false)
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setIsModalOpen(false)
|
||||
setQrCodeData("")
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -215,7 +264,7 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
<div className="space-y-2">
|
||||
<Label>关联设备</Label>
|
||||
<div className="space-y-3">
|
||||
{project?.devices.length > 0 && project.devices.map((device) => (
|
||||
{project && project.devices && project.devices.length > 0 && project.devices.map((device) => (
|
||||
<div key={device.id} className="flex items-center gap-2">
|
||||
<Input
|
||||
value={device.memo}
|
||||
@@ -223,8 +272,18 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={handleAddDevice} className="flex items-center gap-1">
|
||||
<Plus className="h-4 w-4" /> 添加设备
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleAddDevice}
|
||||
className="flex items-center gap-1"
|
||||
disabled={isAddingDevice}
|
||||
>
|
||||
{isAddingDevice ? "添加中..." : (
|
||||
<>
|
||||
<Plus className="h-4 w-4" /> 添加设备
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,11 +304,43 @@ export default function EditProjectPage({ params }: { params: { id: string } })
|
||||
<Link href="/dashboard/projects">取消</Link>
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "保存中..." : "保存修改"}
|
||||
{isSubmitting ? "提交中..." : "保存修改"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</form>
|
||||
|
||||
{/* 使用Dialog组件替代自定义模态框 */}
|
||||
<Dialog open={isModalOpen} onOpenChange={(open) => !open && closeModal()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加设备</DialogTitle>
|
||||
<DialogDescription>
|
||||
请使用新设备进行扫码添加
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center p-6">
|
||||
<div className="border p-4 rounded-lg">
|
||||
{qrCodeData ? (
|
||||
<img
|
||||
src={qrCodeData}
|
||||
alt="设备二维码"
|
||||
className="w-64 h-64 object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-64 h-64 flex items-center justify-center bg-muted">
|
||||
<p className="text-muted-foreground">二维码加载中...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-center">
|
||||
<Button type="button" onClick={closeModal}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import React from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
@@ -11,6 +12,7 @@ import { ArrowLeft, Edit } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
import { use } from "react"
|
||||
import Image from "next/image"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
interface ProjectProfile {
|
||||
id: number
|
||||
@@ -50,7 +52,14 @@ interface SubUser {
|
||||
typeId: number
|
||||
}
|
||||
|
||||
export default function ProjectDetailPage({ params }: { params: { id: string } }) {
|
||||
interface ProjectDetailPageProps {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProjectDetailPage({ params }: ProjectDetailPageProps) {
|
||||
const { id } = use(params)
|
||||
const router = useRouter()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isDevicesLoading, setIsDevicesLoading] = useState(false)
|
||||
@@ -59,7 +68,6 @@ export default function ProjectDetailPage({ params }: { params: { id: string } }
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [subUsers, setSubUsers] = useState<SubUser[]>([])
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
const { id } = use(params)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjectProfile = async () => {
|
||||
@@ -112,15 +120,7 @@ export default function ProjectDetailPage({ params }: { params: { id: string } }
|
||||
if (activeTab === "accounts") {
|
||||
setIsSubUsersLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/subusers`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
companyId: parseInt(id)
|
||||
})
|
||||
})
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/subusers?companyId=${id}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
@@ -243,32 +243,53 @@ export default function ProjectDetailPage({ params }: { params: { id: string } }
|
||||
) : devices.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">暂无数据</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>设备名称</TableHead>
|
||||
<TableHead>设备型号</TableHead>
|
||||
<TableHead>品牌</TableHead>
|
||||
<TableHead>IMEI</TableHead>
|
||||
<TableHead>设备状态</TableHead>
|
||||
<TableHead>微信状态</TableHead>
|
||||
<TableHead className="text-right">微信好友数量</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.id}>
|
||||
<TableCell className="font-medium">{device.memo}</TableCell>
|
||||
<TableCell>{device.model}</TableCell>
|
||||
<TableCell>{device.brand}</TableCell>
|
||||
<TableCell>{device.imei}</TableCell>
|
||||
<TableCell>{device.alive === 1 ? "在线" : "离线"}</TableCell>
|
||||
<TableCell>{device.wAlive === 1 ? "在线" : device.wAlive === 0 ? "离线" : "未登录微信"}</TableCell>
|
||||
<TableCell className="text-right">{device.friendCount || 0}</TableCell>
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>设备名称</TableHead>
|
||||
<TableHead>设备型号</TableHead>
|
||||
<TableHead>品牌</TableHead>
|
||||
<TableHead>IMEI</TableHead>
|
||||
<TableHead>设备状态</TableHead>
|
||||
<TableHead>微信状态</TableHead>
|
||||
<TableHead className="text-right">微信好友数量</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.id}>
|
||||
<TableCell className="font-medium">{device.memo}</TableCell>
|
||||
<TableCell>{device.model}</TableCell>
|
||||
<TableCell>{device.brand}</TableCell>
|
||||
<TableCell>{device.imei}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={device.alive === 1 ? "success" : "destructive"}>
|
||||
{device.alive === 1 ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
device.wAlive === 1
|
||||
? "success"
|
||||
: device.wAlive === 0
|
||||
? "destructive"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{device.wAlive === 1 ? "已登录" : device.wAlive === 0 ? "已登出" : "未登录微信"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{device.friendCount || 0}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
共 {devices.length} 条数据
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -286,42 +307,51 @@ export default function ProjectDetailPage({ params }: { params: { id: string } }
|
||||
) : subUsers.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">暂无数据</div>
|
||||
) : (
|
||||
<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>
|
||||
{subUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt={user.username}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.account}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
<TableCell>{user.status === 1 ? "登录" : "禁用"}</TableCell>
|
||||
<TableCell>{user.typeId === 1 ? "操盘手" : "门店顾问"}</TableCell>
|
||||
<TableCell className="text-right">{user.createTime}</TableCell>
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>头像</TableHead>
|
||||
<TableHead>账号ID</TableHead>
|
||||
<TableHead>登录账号</TableHead>
|
||||
<TableHead>昵称</TableHead>
|
||||
<TableHead>手机号</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>账号类型</TableHead>
|
||||
<TableHead className="text-right">创建时间</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{subUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt={user.username}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.account}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.status === 1 ? "success" : "destructive"}>
|
||||
{user.status === 1 ? "启用" : "禁用"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{user.typeId === 1 ? "操盘手" : "门店顾问"}</TableCell>
|
||||
<TableCell className="text-right">{user.createTime}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
共 {subUsers.length} 条数据
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Link from "next/link"
|
||||
import { useSearchParams, useRouter, usePathname } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from "@/components/ui/dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { PaginationControls } from "@/components/ui/pagination-controls"
|
||||
import { useTabContext } from "@/app/dashboard/layout"
|
||||
|
||||
interface Project {
|
||||
id: number
|
||||
@@ -32,17 +33,41 @@ interface Project {
|
||||
}
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { addTab } = useTabContext()
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("")
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [currentPage, setCurrentPage] = useState(parseInt(searchParams.get("page") || "1"))
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const [pageSize, setPageSize] = useState(parseInt(searchParams.get("pageSize") || "10"))
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||
const [deletingProjectId, setDeletingProjectId] = useState<number | null>(null)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
// 从URL更新状态
|
||||
useEffect(() => {
|
||||
const page = parseInt(searchParams.get("page") || "1")
|
||||
const size = parseInt(searchParams.get("pageSize") || "10")
|
||||
setCurrentPage(page)
|
||||
setPageSize(size)
|
||||
}, [searchParams])
|
||||
|
||||
// 更新URL查询参数
|
||||
const updateUrlParams = (page: number, size: number) => {
|
||||
const params = new URLSearchParams()
|
||||
params.set("page", page.toString())
|
||||
params.set("pageSize", size.toString())
|
||||
if (searchTerm) {
|
||||
params.set("search", searchTerm)
|
||||
}
|
||||
router.replace(`${pathname}?${params.toString()}`)
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
useEffect(() => {
|
||||
const fetchProjects = async () => {
|
||||
@@ -72,12 +97,21 @@ export default function ProjectsPage() {
|
||||
}
|
||||
|
||||
fetchProjects()
|
||||
}, [currentPage, pageSize])
|
||||
// 更新URL参数
|
||||
updateUrlParams(currentPage, pageSize)
|
||||
}, [currentPage, pageSize, pathname])
|
||||
|
||||
// 处理页面大小变化
|
||||
const handlePageSizeChange = (newSize: number) => {
|
||||
setPageSize(newSize)
|
||||
setCurrentPage(1)
|
||||
updateUrlParams(1, newSize)
|
||||
}
|
||||
|
||||
// 处理页面变化
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setCurrentPage(newPage)
|
||||
updateUrlParams(newPage, pageSize)
|
||||
}
|
||||
|
||||
const handleDeleteClick = (projectId: number) => {
|
||||
@@ -104,6 +138,7 @@ export default function ProjectsPage() {
|
||||
|
||||
if (data.code === 200) {
|
||||
toast.success("删除成功")
|
||||
|
||||
// Fetch projects again after delete
|
||||
const fetchProjects = async () => {
|
||||
setIsLoading(true)
|
||||
@@ -136,14 +171,39 @@ export default function ProjectsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 打开项目详情
|
||||
const handleViewProject = (project: Project) => {
|
||||
addTab({
|
||||
label: `项目 #${project.id} - 详情`,
|
||||
path: `/dashboard/projects/${project.id}`,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 打开编辑项目
|
||||
const handleEditProject = (project: Project) => {
|
||||
addTab({
|
||||
label: `项目 #${project.id} - 编辑`,
|
||||
path: `/dashboard/projects/${project.id}/edit`,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
// 打开新建项目
|
||||
const handleNewProject = () => {
|
||||
addTab({
|
||||
label: "新建项目",
|
||||
path: "/dashboard/projects/new",
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
|
||||
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 onClick={handleNewProject}>
|
||||
<Plus className="mr-2 h-4 w-4" /> 新建项目
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -175,7 +235,7 @@ export default function ProjectsPage() {
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
<TableCell colSpan={6} className="h-24 text-center">
|
||||
加载中...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -200,15 +260,11 @@ export default function ProjectsPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/projects/${project.id}`}>
|
||||
<Eye className="mr-2 h-4 w-4" /> 查看详情
|
||||
</Link>
|
||||
<DropdownMenuItem onClick={() => handleViewProject(project)}>
|
||||
<Eye className="mr-2 h-4 w-4" /> 查看详情
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={`/dashboard/projects/${project.id}/edit`}>
|
||||
<Edit className="mr-2 h-4 w-4" /> 编辑项目
|
||||
</Link>
|
||||
<DropdownMenuItem onClick={() => handleEditProject(project)}>
|
||||
<Edit className="mr-2 h-4 w-4" /> 编辑项目
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
@@ -223,7 +279,7 @@ export default function ProjectsPage() {
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="h-24 text-center">
|
||||
<TableCell colSpan={6} className="h-24 text-center">
|
||||
未找到项目
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -237,7 +293,7 @@ export default function ProjectsPage() {
|
||||
totalPages={totalPages}
|
||||
pageSize={pageSize}
|
||||
totalItems={totalItems}
|
||||
onPageChange={setCurrentPage}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
|
||||
|
||||
5
SuperAdmin/app/page.tsx
Normal file
5
SuperAdmin/app/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
export default function Home() {
|
||||
redirect("/dashboard")
|
||||
}
|
||||
@@ -1,37 +1,178 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { useState, useEffect } from "react"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { getMenus, type MenuItem } from "@/lib/menu-api"
|
||||
import * as LucideIcons from "lucide-react"
|
||||
import { ChevronDown, ChevronRight } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useTabContext } from "@/app/dashboard/layout"
|
||||
import { getMenus } from "@/lib/menu-api"
|
||||
|
||||
// 适配后端返回的菜单项格式
|
||||
interface MenuItem {
|
||||
id: number
|
||||
parentId?: number | null
|
||||
parent_id?: number // 后端返回的字段
|
||||
name?: string
|
||||
title?: string // 后端返回的字段
|
||||
path: string
|
||||
icon?: string
|
||||
order?: number
|
||||
sort?: number // 后端返回的字段
|
||||
status?: number
|
||||
children?: MenuItem[]
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname()
|
||||
const [menus, setMenus] = useState<MenuItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
// 使用Set来存储已展开的菜单ID
|
||||
const [expandedMenus, setExpandedMenus] = useState<Set<number>>(new Set())
|
||||
const [collapsed, setCollapsed] = useState(false) // 添加折叠状态
|
||||
const { addTab } = useTabContext()
|
||||
|
||||
// 字段适配:将后端返回的菜单数据格式转换为前端需要的格式
|
||||
const adaptMenuItem = (item: MenuItem): MenuItem => {
|
||||
return {
|
||||
id: item.id,
|
||||
parentId: item.parent_id || null,
|
||||
name: item.title || item.name,
|
||||
path: item.path,
|
||||
icon: item.icon,
|
||||
order: item.sort || item.order || 0,
|
||||
children: item.children ? item.children.map(adaptMenuItem) : []
|
||||
};
|
||||
};
|
||||
|
||||
// 切换折叠状态
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed(prev => !prev);
|
||||
};
|
||||
|
||||
// 获取菜单数据
|
||||
useEffect(() => {
|
||||
const fetchMenus = async () => {
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const data = await getMenus()
|
||||
setMenus(data || [])
|
||||
// 从后端API获取菜单数据
|
||||
const menuData = await getMenus(true);
|
||||
|
||||
// 自动展开当前活动菜单的父菜单
|
||||
autoExpandActiveMenuParent(data || []);
|
||||
// 适配数据格式
|
||||
const adaptedMenus = menuData.map(adaptMenuItem);
|
||||
|
||||
// 构建菜单树
|
||||
const menuTree = buildMenuTree(adaptedMenus);
|
||||
setMenus(menuTree);
|
||||
|
||||
// 初始自动展开当前活动菜单的父菜单
|
||||
autoExpandActiveMenuParent(adaptedMenus);
|
||||
} catch (error) {
|
||||
console.error("获取菜单失败:", error)
|
||||
console.error("获取菜单数据失败:", error);
|
||||
// 获取失败时使用空菜单
|
||||
setMenus([]);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMenus();
|
||||
}, []); // 仅在组件挂载时执行一次,移除pathname依赖
|
||||
|
||||
// 监听路径变化以更新菜单展开状态
|
||||
useEffect(() => {
|
||||
if (menus.length > 0) {
|
||||
// 只在菜单数据存在且路径变化时更新展开状态
|
||||
// 获取当前路径所需展开的所有父菜单ID
|
||||
const pathMenuItems = menus.reduce((allItems, item) => {
|
||||
const flattenMenu = (menuItem: MenuItem, items: MenuItem[] = []) => {
|
||||
items.push(menuItem);
|
||||
if (menuItem.children && menuItem.children.length > 0) {
|
||||
menuItem.children.forEach(child => flattenMenu(child, items));
|
||||
}
|
||||
return items;
|
||||
};
|
||||
return [...allItems, ...flattenMenu(item)];
|
||||
}, [] as MenuItem[]);
|
||||
|
||||
// 保存当前展开状态
|
||||
setExpandedMenus(prev => {
|
||||
// 创建新集合,保留所有已展开的菜单
|
||||
const newExpanded = new Set(prev);
|
||||
|
||||
// 将需要展开的菜单添加到集合中
|
||||
const currentPath = pathname === "/" ? "/dashboard" : pathname;
|
||||
|
||||
// 查找当前路径对应的菜单项和所有父菜单
|
||||
const findActiveMenuParents = (items: MenuItem[], parentIds: number[] = []): number[] => {
|
||||
for (const item of items) {
|
||||
// 如果是"#"路径的菜单,检查其子菜单
|
||||
if (item.path === "#" && item.children && item.children.length > 0) {
|
||||
const found = findActiveMenuParents(item.children, [...parentIds, item.id]);
|
||||
if (found.length > 0) {
|
||||
return [...found, item.id];
|
||||
}
|
||||
}
|
||||
// 检查菜单路径是否匹配当前路径
|
||||
else if (currentPath === item.path || currentPath.startsWith(item.path + "/")) {
|
||||
return [...parentIds];
|
||||
}
|
||||
|
||||
// 递归检查子菜单
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findActiveMenuParents(item.children, [...parentIds, item.id]);
|
||||
if (found.length > 0) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// 获取需要自动展开的菜单ID
|
||||
const parentsToExpand = findActiveMenuParents(menus);
|
||||
|
||||
// 添加到展开集合中
|
||||
parentsToExpand.forEach(id => newExpanded.add(id));
|
||||
|
||||
return newExpanded;
|
||||
});
|
||||
}
|
||||
}, [pathname, menus]);
|
||||
|
||||
fetchMenus()
|
||||
}, [])
|
||||
// 构建菜单树结构
|
||||
const buildMenuTree = (items: MenuItem[]) => {
|
||||
const map = new Map<number, MenuItem>();
|
||||
const roots: MenuItem[] = [];
|
||||
|
||||
// 先创建所有菜单项的映射
|
||||
items.forEach(item => {
|
||||
map.set(item.id, { ...item, children: item.children || [] });
|
||||
});
|
||||
|
||||
// 构建树结构
|
||||
items.forEach(item => {
|
||||
if (!item.parentId || item.parentId === 0) {
|
||||
// 根菜单
|
||||
roots.push(map.get(item.id)!);
|
||||
} else {
|
||||
// 子菜单
|
||||
const parent = map.get(item.parentId);
|
||||
if (parent && parent.children) {
|
||||
parent.children.push(map.get(item.id)!);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 排序
|
||||
roots.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
roots.forEach(root => {
|
||||
if (root.children) {
|
||||
root.children.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
};
|
||||
|
||||
// 自动展开当前活动菜单的父菜单
|
||||
const autoExpandActiveMenuParent = (menuItems: MenuItem[]) => {
|
||||
@@ -40,10 +181,23 @@ export function Sidebar() {
|
||||
// 递归查找当前路径匹配的菜单项
|
||||
const findActiveMenu = (items: MenuItem[], parentIds: number[] = []) => {
|
||||
for (const item of items) {
|
||||
const currentPath = pathname === "/" ? "/dashboard" : pathname;
|
||||
const itemPath = item.path;
|
||||
// 如果是"#"路径的菜单,跳过路径检查
|
||||
if (item.path === "#") {
|
||||
if (item.children && item.children.length > 0) {
|
||||
const found = findActiveMenu(item.children, [...parentIds, item.id]);
|
||||
if (found) {
|
||||
// 将所有父菜单ID添加到展开集合
|
||||
parentIds.forEach(id => newExpandedMenus.add(id));
|
||||
newExpandedMenus.add(item.id); // 确保当前菜单也被展开
|
||||
return true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentPath === itemPath || currentPath.startsWith(itemPath + "/")) {
|
||||
const currentPath = pathname === "/" ? "/dashboard" : pathname;
|
||||
|
||||
if (currentPath === item.path || currentPath.startsWith(item.path + "/")) {
|
||||
// 将所有父菜单ID添加到展开集合
|
||||
parentIds.forEach(id => newExpandedMenus.add(id));
|
||||
return true;
|
||||
@@ -60,11 +214,16 @@ export function Sidebar() {
|
||||
};
|
||||
|
||||
findActiveMenu(menuItems);
|
||||
|
||||
// 将新的展开菜单集合设置到状态
|
||||
setExpandedMenus(newExpandedMenus);
|
||||
};
|
||||
|
||||
// 切换菜单展开状态
|
||||
const toggleMenu = (menuId: number) => {
|
||||
const toggleMenu = (menuId: number, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setExpandedMenus(prev => {
|
||||
const newExpanded = new Set(prev);
|
||||
if (newExpanded.has(menuId)) {
|
||||
@@ -83,82 +242,207 @@ export function Sidebar() {
|
||||
return Icon ? <Icon className="h-4 w-4 mr-2" /> : null;
|
||||
};
|
||||
|
||||
// 递归渲染菜单项
|
||||
// 渲染菜单项
|
||||
const renderMenuItem = (item: MenuItem) => {
|
||||
// 修改子菜单项活动状态判断逻辑
|
||||
const isMenuPathActive = (menuPath: string, currentPath: string) => {
|
||||
// 对于精确匹配的情况,直接返回true
|
||||
if (currentPath === menuPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 特殊处理项目列表路径
|
||||
if (menuPath === "/dashboard/projects" && currentPath !== "/dashboard/projects") {
|
||||
// 如果当前路径不是精确匹配项目列表,则项目列表不高亮
|
||||
return false;
|
||||
}
|
||||
|
||||
// 对于其他情况,保持原来的前缀匹配逻辑
|
||||
// 但要确保父级路径后有斜杠再做前缀匹配
|
||||
return currentPath.startsWith(menuPath + "/");
|
||||
};
|
||||
|
||||
const currentPath = pathname === "/" ? "/dashboard" : pathname;
|
||||
const isActive = isMenuPathActive(item.path, currentPath);
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isExpanded = expandedMenus.has(item.id);
|
||||
const isActive = pathname === item.path;
|
||||
const isChildActive = hasChildren && item.children!.some(child =>
|
||||
pathname === child.path || pathname.startsWith(child.path + "/")
|
||||
);
|
||||
|
||||
const name = item.name || item.title || "";
|
||||
|
||||
// 折叠状态下的菜单项
|
||||
if (collapsed) {
|
||||
return (
|
||||
<li key={item.id} className="relative group">
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center py-2 rounded-md transition-colors cursor-pointer",
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-blue-100 hover:bg-blue-700/30"
|
||||
)}
|
||||
title={name}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!hasChildren) {
|
||||
const tabId = addTab({
|
||||
label: name,
|
||||
path: item.path,
|
||||
closable: item.path !== "/dashboard"
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{getLucideIcon(item.icon || "")}
|
||||
|
||||
{/* 悬浮提示 */}
|
||||
{hasChildren ? (
|
||||
<div className="absolute left-full ml-2 hidden group-hover:block z-50 bg-blue-800 rounded-md shadow-lg py-1 min-w-40">
|
||||
<div className="font-medium px-3 py-1 border-b border-blue-700">{name}</div>
|
||||
<ul className="py-1">
|
||||
{item.children!.map((child) => (
|
||||
<li key={child.id}>
|
||||
<a
|
||||
href={child.path}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const tabId = addTab({
|
||||
label: child.name || child.title || "",
|
||||
path: child.path,
|
||||
closable: true
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center px-3 py-1 transition-colors",
|
||||
isMenuPathActive(child.path, currentPath)
|
||||
? "bg-blue-700 text-white font-medium"
|
||||
: "text-blue-100 hover:bg-blue-700/30"
|
||||
)}
|
||||
>
|
||||
{getLucideIcon(child.icon || "")}
|
||||
<span>{child.name || child.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="absolute left-full ml-2 hidden group-hover:block z-50 bg-blue-800 rounded-md shadow-lg px-3 py-1 whitespace-nowrap">
|
||||
{name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
// 展开状态下的菜单项
|
||||
return (
|
||||
<li key={item.id}>
|
||||
{hasChildren ? (
|
||||
<div className="flex flex-col">
|
||||
<>
|
||||
<button
|
||||
onClick={() => toggleMenu(item.id)}
|
||||
className={`flex items-center justify-between px-4 py-2 rounded-md text-sm w-full text-left ${
|
||||
isActive || isChildActive
|
||||
? "text-white font-semibold"
|
||||
: "hover:bg-blue-600"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{item.icon && getLucideIcon(item.icon)}
|
||||
{item.title}
|
||||
</div>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
onClick={(e) => toggleMenu(item.id, e)}
|
||||
className={cn(
|
||||
"flex items-center w-full py-2 px-3 rounded-md transition-colors",
|
||||
isActive
|
||||
? "text-white font-medium"
|
||||
: "text-blue-100 hover:bg-blue-700/30"
|
||||
)}
|
||||
>
|
||||
{getLucideIcon(item.icon || "")}
|
||||
<span>{name}</span>
|
||||
<span className="ml-auto">
|
||||
{isExpanded ? (
|
||||
<LucideIcons.ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<LucideIcons.ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && hasChildren && (
|
||||
{isExpanded && (
|
||||
<ul className="ml-4 mt-1 space-y-1">
|
||||
{item.children!.map(child => {
|
||||
const isChildItemActive = pathname === child.path;
|
||||
return (
|
||||
<li key={child.id}>
|
||||
<Link
|
||||
href={child.path}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
||||
isChildItemActive
|
||||
? "text-white font-semibold bg-blue-700"
|
||||
: "hover:bg-blue-600"
|
||||
}`}
|
||||
>
|
||||
{child.icon && getLucideIcon(child.icon)}
|
||||
{child.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{item.children!.map((child) => (
|
||||
<li key={child.id}>
|
||||
<a
|
||||
href={child.path}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 使用addTab返回的标签ID,在TabContext中处理,确保标签被激活并导航
|
||||
const tabId = addTab({
|
||||
label: child.name || child.title || "",
|
||||
path: child.path,
|
||||
closable: true
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center py-2 px-3 rounded-md transition-colors",
|
||||
isMenuPathActive(child.path, currentPath)
|
||||
? "bg-blue-700 text-white font-medium"
|
||||
: "text-blue-100 hover:bg-blue-700/30"
|
||||
)}
|
||||
>
|
||||
{getLucideIcon(child.icon || "")}
|
||||
<span>{child.name || child.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Link
|
||||
<a
|
||||
href={item.path}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// 使用addTab返回的标签ID,在TabContext中处理,确保标签被激活并导航
|
||||
const tabId = addTab({
|
||||
label: name,
|
||||
path: item.path,
|
||||
closable: item.path !== "/dashboard" // 仪表盘不可关闭
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center py-2 px-3 rounded-md transition-colors",
|
||||
isActive
|
||||
? "text-white font-semibold"
|
||||
: "hover:bg-blue-600"
|
||||
}`}
|
||||
? "text-white font-medium"
|
||||
: "text-blue-100 hover:bg-blue-700/30"
|
||||
)}
|
||||
>
|
||||
{item.icon && getLucideIcon(item.icon)}
|
||||
{item.title}
|
||||
</Link>
|
||||
{getLucideIcon(item.icon || "")}
|
||||
<span>{name}</span>
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-r bg-[#2563eb] h-full flex flex-col text-white">
|
||||
<div className="p-4 border-b border-blue-500">
|
||||
<h2 className="text-lg font-bold">超级管理员</h2>
|
||||
<div className={cn(
|
||||
"border-r bg-[#2563eb] h-full flex flex-col text-white transition-all duration-300 ease-in-out",
|
||||
collapsed ? "w-16" : "w-64"
|
||||
)}>
|
||||
<div className={cn(
|
||||
"border-b border-blue-500 flex items-center justify-between",
|
||||
collapsed ? "p-2" : "p-4"
|
||||
)}>
|
||||
{!collapsed && <h2 className="text-lg font-bold">超级管理员</h2>}
|
||||
<button
|
||||
onClick={toggleCollapsed}
|
||||
className="p-1 rounded-md hover:bg-blue-700 transition-colors"
|
||||
title={collapsed ? "展开菜单" : "折叠菜单"}
|
||||
>
|
||||
{collapsed ? (
|
||||
<LucideIcons.ChevronRight className="h-5 w-5" />
|
||||
) : (
|
||||
<LucideIcons.ChevronLeft className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-auto p-2">
|
||||
@@ -176,8 +460,11 @@ export function Sidebar() {
|
||||
</ul>
|
||||
) : (
|
||||
// 无菜单数据
|
||||
<div className="text-center py-8 text-blue-200">
|
||||
<p>暂无菜单数据</p>
|
||||
<div className={cn(
|
||||
"text-center py-8 text-blue-200",
|
||||
collapsed && "text-xs px-0"
|
||||
)}>
|
||||
<p>{collapsed ? "无菜单" : "暂无菜单数据"}</p>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
244
SuperAdmin/components/projects/project-create.tsx
Normal file
244
SuperAdmin/components/projects/project-create.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, "项目名称至少需要2个字符"),
|
||||
account: z.string().min(3, "账号至少需要3个字符"),
|
||||
password: z.string().min(6, "密码至少需要6个字符"),
|
||||
confirmPassword: z.string().min(6, "确认密码至少需要6个字符"),
|
||||
phone: z.string().optional(),
|
||||
realname: z.string().optional(),
|
||||
nickname: z.string().optional(),
|
||||
memo: z.string().optional(),
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "两次输入的密码不一致",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
interface ProjectCreateProps {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export default function ProjectCreate({ onSuccess }: ProjectCreateProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
phone: "",
|
||||
realname: "",
|
||||
nickname: "",
|
||||
memo: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: values.name,
|
||||
account: values.account,
|
||||
password: values.password,
|
||||
phone: values.phone || null,
|
||||
realname: values.realname || null,
|
||||
nickname: values.nickname || null,
|
||||
memo: values.memo || null,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
toast.success("项目创建成功")
|
||||
form.reset()
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
} else {
|
||||
toast.error(data.msg || "创建项目失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>新建项目</CardTitle>
|
||||
<CardDescription>创建一个新的项目并设置基本信息</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form id="create-project-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入项目名称" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="account"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>账号</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入账号" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>手机号</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入手机号" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="请输入密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>确认密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="请再次输入密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="realname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>真实姓名</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入真实姓名(可选)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="nickname"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>昵称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入昵称(可选)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="请输入项目描述(可选)" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="create-project-form"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "创建中..." : "创建项目"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
349
SuperAdmin/components/projects/project-detail.tsx
Normal file
349
SuperAdmin/components/projects/project-detail.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } 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"
|
||||
import { toast } from "sonner"
|
||||
import Image from "next/image"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
interface ProjectProfile {
|
||||
id: number
|
||||
name: string
|
||||
memo: string
|
||||
companyId: number
|
||||
createTime: string
|
||||
account: string
|
||||
phone: string | null
|
||||
deviceCount: number
|
||||
friendCount: number
|
||||
userCount: number
|
||||
}
|
||||
|
||||
interface Device {
|
||||
id: number
|
||||
memo: string
|
||||
phone: string
|
||||
model: string
|
||||
brand: string
|
||||
alive: number
|
||||
deviceId: number
|
||||
wechatId: string
|
||||
friendCount: number
|
||||
wAlive: number
|
||||
imei: string
|
||||
}
|
||||
|
||||
interface SubUser {
|
||||
id: number
|
||||
account: string
|
||||
username: string
|
||||
phone: string
|
||||
avatar: string
|
||||
status: number
|
||||
createTime: string
|
||||
typeId: number
|
||||
}
|
||||
|
||||
interface ProjectDetailProps {
|
||||
projectId: string
|
||||
onEdit?: (projectId: string) => void
|
||||
}
|
||||
|
||||
export default function ProjectDetail({ projectId, onEdit }: ProjectDetailProps) {
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isDevicesLoading, setIsDevicesLoading] = useState(false)
|
||||
const [isSubUsersLoading, setIsSubUsersLoading] = useState(false)
|
||||
const [profile, setProfile] = useState<ProjectProfile | null>(null)
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [subUsers, setSubUsers] = useState<SubUser[]>([])
|
||||
const [activeTab, setActiveTab] = useState("overview")
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProjectProfile = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/profile/${projectId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
setProfile(data.data)
|
||||
} else {
|
||||
toast.error(data.msg || "获取项目信息失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchProjectProfile()
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDevices = async () => {
|
||||
if (activeTab === "devices") {
|
||||
setIsDevicesLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/devices?companyId=${projectId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
setDevices(data.data)
|
||||
} else {
|
||||
toast.error(data.msg || "获取设备列表失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsDevicesLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchDevices()
|
||||
}, [activeTab, projectId])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSubUsers = async () => {
|
||||
if (activeTab === "accounts") {
|
||||
setIsSubUsersLoading(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/subusers?companyId=${projectId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
setSubUsers(data.data)
|
||||
} else {
|
||||
toast.error(data.msg || "获取子账号列表失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsSubUsersLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchSubUsers()
|
||||
}, [activeTab, projectId])
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="flex items-center justify-center min-h-64">加载中...</div>
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return <div className="flex items-center justify-center min-h-64">未找到项目信息</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">{profile.name}</h1>
|
||||
{onEdit && (
|
||||
<Button onClick={() => onEdit(projectId)}>
|
||||
<Edit className="mr-2 h-4 w-4" /> 编辑项目
|
||||
</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">{profile.name}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">手机号</dt>
|
||||
<dd className="text-sm">{profile.phone || "未设置"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">账号</dt>
|
||||
<dd className="text-sm">{profile.account}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm font-medium text-muted-foreground">创建时间</dt>
|
||||
<dd className="text-sm">{profile.createTime}</dd>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<dt className="text-sm font-medium text-muted-foreground">项目介绍</dt>
|
||||
<dd className="text-sm">{profile.memo}</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">{profile.deviceCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">子账号数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{profile.userCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium">微信好友总数</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{profile.friendCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="devices">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>关联设备列表</CardTitle>
|
||||
<CardDescription>项目关联的所有设备及其微信好友数量</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isDevicesLoading ? (
|
||||
<div className="flex items-center justify-center py-8">加载中...</div>
|
||||
) : devices.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">暂无数据</div>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>设备名称</TableHead>
|
||||
<TableHead>设备型号</TableHead>
|
||||
<TableHead>品牌</TableHead>
|
||||
<TableHead>IMEI</TableHead>
|
||||
<TableHead>设备状态</TableHead>
|
||||
<TableHead>微信状态</TableHead>
|
||||
<TableHead className="text-right">微信好友数量</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{devices.map((device) => (
|
||||
<TableRow key={device.id}>
|
||||
<TableCell className="font-medium">{device.memo}</TableCell>
|
||||
<TableCell>{device.model}</TableCell>
|
||||
<TableCell>{device.brand}</TableCell>
|
||||
<TableCell>{device.imei}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={device.alive === 1 ? "success" : "destructive"}>
|
||||
{device.alive === 1 ? "在线" : "离线"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
device.wAlive === 1
|
||||
? "success"
|
||||
: device.wAlive === 0
|
||||
? "destructive"
|
||||
: "secondary"
|
||||
}
|
||||
>
|
||||
{device.wAlive === 1 ? "已登录" : device.wAlive === 0 ? "已登出" : "未登录微信"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">{device.friendCount || 0}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
共 {devices.length} 条数据
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="accounts">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>子账号列表</CardTitle>
|
||||
<CardDescription>项目下的所有子账号</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isSubUsersLoading ? (
|
||||
<div className="flex items-center justify-center py-8">加载中...</div>
|
||||
) : subUsers.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8 text-muted-foreground">暂无数据</div>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
{subUsers.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Image
|
||||
src={user.avatar}
|
||||
alt={user.username}
|
||||
width={32}
|
||||
height={32}
|
||||
className="rounded-full"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.account}</TableCell>
|
||||
<TableCell>{user.username}</TableCell>
|
||||
<TableCell>{user.phone}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={user.status === 1 ? "success" : "destructive"}>
|
||||
{user.status === 1 ? "启用" : "禁用"}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{user.typeId === 1 ? "操盘手" : "门店顾问"}</TableCell>
|
||||
<TableCell className="text-right">{user.createTime}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
共 {subUsers.length} 条数据
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
264
SuperAdmin/components/projects/project-edit.tsx
Normal file
264
SuperAdmin/components/projects/project-edit.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, "项目名称至少需要2个字符"),
|
||||
account: z.string().min(3, "账号至少需要3个字符"),
|
||||
password: z.string().optional(),
|
||||
confirmPassword: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
memo: z.string().optional(),
|
||||
})
|
||||
|
||||
interface ProjectEditProps {
|
||||
projectId: string
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
interface ProjectData {
|
||||
id: number
|
||||
name: string
|
||||
account: string
|
||||
memo?: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
export default function ProjectEdit({ projectId, onSuccess }: ProjectEditProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isFetching, setIsFetching] = useState(true)
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
phone: "",
|
||||
memo: "",
|
||||
},
|
||||
})
|
||||
|
||||
// 获取项目数据
|
||||
useEffect(() => {
|
||||
const fetchProject = async () => {
|
||||
setIsFetching(true)
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/profile/${projectId}`)
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
const project = data.data
|
||||
form.reset({
|
||||
name: project.name || "",
|
||||
account: project.account || "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
phone: project.phone || "",
|
||||
memo: project.memo || "",
|
||||
})
|
||||
} else {
|
||||
toast.error(data.msg || "获取项目信息失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchProject()
|
||||
}, [projectId, form])
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
// 检查密码是否匹配
|
||||
if (values.password && values.password !== values.confirmPassword) {
|
||||
toast.error("两次输入的密码不一致")
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// 准备请求数据,根据需要添加或移除字段
|
||||
const updateData: Record<string, any> = {
|
||||
id: parseInt(projectId),
|
||||
name: values.name,
|
||||
account: values.account,
|
||||
memo: values.memo,
|
||||
phone: values.phone,
|
||||
}
|
||||
|
||||
// 如果提供了密码,则包含密码字段
|
||||
if (values.password) {
|
||||
updateData.password = values.password
|
||||
}
|
||||
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/company/update`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data.code === 200) {
|
||||
toast.success("项目更新成功")
|
||||
if (onSuccess) {
|
||||
onSuccess()
|
||||
}
|
||||
} else {
|
||||
toast.error(data.msg || "更新项目失败")
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("网络错误,请稍后重试")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isFetching) {
|
||||
return <div className="flex items-center justify-center min-h-64">加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>编辑项目</CardTitle>
|
||||
<CardDescription>更新项目信息和设置</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form id="edit-project-form" onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入项目名称" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="account"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>账号</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入账号" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>手机号</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="请输入手机号" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="不修改请留空" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>确认密码</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" placeholder="请再次输入密码" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="memo"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>项目描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="请输入项目描述" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => form.reset()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
form="edit-project-form"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "保存中..." : "保存更改"}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user