超管后台 - 调整页面路由为动态路由请求,修改页面渲染为tabs标签形式
This commit is contained in:
@@ -1,14 +1,41 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect, createContext, useContext } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Menu, X } from "lucide-react"
|
import { Menu, X } from "lucide-react"
|
||||||
import { Sidebar } from "@/components/layout/sidebar"
|
import { Sidebar } from "@/components/layout/sidebar"
|
||||||
import { Header } from "@/components/layout/header"
|
import { Header } from "@/components/layout/header"
|
||||||
import { getAdminInfo } from "@/lib/utils"
|
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({
|
export default function DashboardLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
@@ -16,6 +43,122 @@ export default function DashboardLayout({
|
|||||||
}) {
|
}) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true)
|
const [sidebarOpen, setSidebarOpen] = useState(true)
|
||||||
const router = useRouter()
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -31,29 +174,72 @@ export default function DashboardLayout({
|
|||||||
}, [router])
|
}, [router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen overflow-hidden">
|
<TabContext.Provider value={{
|
||||||
{/* Mobile sidebar toggle */}
|
tabs,
|
||||||
<div className="fixed top-4 left-4 z-50 md:hidden">
|
activeTab,
|
||||||
<Button variant="outline" size="icon" onClick={() => setSidebarOpen(!sidebarOpen)}>
|
addTab,
|
||||||
{sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
closeTab,
|
||||||
</Button>
|
setActiveTab: setActiveTabAndNavigate,
|
||||||
</div>
|
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 */}
|
{/* Sidebar */}
|
||||||
<div
|
<div
|
||||||
className={`bg-background border-r w-64 flex-shrink-0 transition-all duration-300 ease-in-out ${
|
className={`bg-background flex-shrink-0 transition-all duration-300 ease-in-out ${
|
||||||
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
sidebarOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
} md:translate-x-0 fixed md:relative z-40 h-full`}
|
} md:translate-x-0 fixed md:relative z-40 h-full`}
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 overflow-y-auto p-6">{children}</main>
|
|
||||||
|
{/* 标签栏 */}
|
||||||
|
<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>
|
||||||
</div>
|
</TabContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import Link from "next/link"
|
|
||||||
import { useSearchParams, useRouter, usePathname } from "next/navigation"
|
import { useSearchParams, useRouter, usePathname } from "next/navigation"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
} from "@/components/ui/dialog"
|
} from "@/components/ui/dialog"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { PaginationControls } from "@/components/ui/pagination-controls"
|
import { PaginationControls } from "@/components/ui/pagination-controls"
|
||||||
|
import { useTabContext } from "@/app/dashboard/layout"
|
||||||
|
|
||||||
interface Project {
|
interface Project {
|
||||||
id: number
|
id: number
|
||||||
@@ -36,6 +36,8 @@ export default function ProjectsPage() {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
const { addTab } = useTabContext()
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState("")
|
const [searchTerm, setSearchTerm] = useState("")
|
||||||
const [projects, setProjects] = useState<Project[]>([])
|
const [projects, setProjects] = useState<Project[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
@@ -136,6 +138,7 @@ export default function ProjectsPage() {
|
|||||||
|
|
||||||
if (data.code === 200) {
|
if (data.code === 200) {
|
||||||
toast.success("删除成功")
|
toast.success("删除成功")
|
||||||
|
|
||||||
// Fetch projects again after delete
|
// Fetch projects again after delete
|
||||||
const fetchProjects = async () => {
|
const fetchProjects = async () => {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
@@ -168,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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<h1 className="text-2xl font-bold">项目列表</h1>
|
<h1 className="text-2xl font-bold">项目列表</h1>
|
||||||
<Button asChild>
|
<Button onClick={handleNewProject}>
|
||||||
<Link href="/dashboard/projects/new">
|
<Plus className="mr-2 h-4 w-4" /> 新建项目
|
||||||
<Plus className="mr-2 h-4 w-4" /> 新建项目
|
|
||||||
</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -207,7 +235,7 @@ export default function ProjectsPage() {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="h-24 text-center">
|
<TableCell colSpan={6} className="h-24 text-center">
|
||||||
加载中...
|
加载中...
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -232,15 +260,11 @@ export default function ProjectsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem onClick={() => handleViewProject(project)}>
|
||||||
<Link href={`/dashboard/projects/${project.id}`}>
|
<Eye className="mr-2 h-4 w-4" /> 查看详情
|
||||||
<Eye className="mr-2 h-4 w-4" /> 查看详情
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem onClick={() => handleEditProject(project)}>
|
||||||
<Link href={`/dashboard/projects/${project.id}/edit`}>
|
<Edit className="mr-2 h-4 w-4" /> 编辑项目
|
||||||
<Edit className="mr-2 h-4 w-4" /> 编辑项目
|
|
||||||
</Link>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-red-600"
|
className="text-red-600"
|
||||||
@@ -255,7 +279,7 @@ export default function ProjectsPage() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="h-24 text-center">
|
<TableCell colSpan={6} className="h-24 text-center">
|
||||||
未找到项目
|
未找到项目
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,37 +1,178 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import Link from "next/link"
|
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { getMenus, type MenuItem } from "@/lib/menu-api"
|
|
||||||
import * as LucideIcons from "lucide-react"
|
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() {
|
export function Sidebar() {
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const [menus, setMenus] = useState<MenuItem[]>([])
|
const [menus, setMenus] = useState<MenuItem[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
// 使用Set来存储已展开的菜单ID
|
|
||||||
const [expandedMenus, setExpandedMenus] = useState<Set<number>>(new Set())
|
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(() => {
|
useEffect(() => {
|
||||||
const fetchMenus = async () => {
|
const fetchMenus = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getMenus()
|
// 从后端API获取菜单数据
|
||||||
setMenus(data || [])
|
const menuData = await getMenus(true);
|
||||||
|
|
||||||
// 自动展开当前活动菜单的父菜单
|
// 适配数据格式
|
||||||
autoExpandActiveMenuParent(data || []);
|
const adaptedMenus = menuData.map(adaptMenuItem);
|
||||||
|
|
||||||
|
// 构建菜单树
|
||||||
|
const menuTree = buildMenuTree(adaptedMenus);
|
||||||
|
setMenus(menuTree);
|
||||||
|
|
||||||
|
// 初始自动展开当前活动菜单的父菜单
|
||||||
|
autoExpandActiveMenuParent(adaptedMenus);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("获取菜单失败:", error)
|
console.error("获取菜单数据失败:", error);
|
||||||
|
// 获取失败时使用空菜单
|
||||||
|
setMenus([]);
|
||||||
} finally {
|
} 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[]) => {
|
const autoExpandActiveMenuParent = (menuItems: MenuItem[]) => {
|
||||||
@@ -40,10 +181,23 @@ export function Sidebar() {
|
|||||||
// 递归查找当前路径匹配的菜单项
|
// 递归查找当前路径匹配的菜单项
|
||||||
const findActiveMenu = (items: MenuItem[], parentIds: number[] = []) => {
|
const findActiveMenu = (items: MenuItem[], parentIds: number[] = []) => {
|
||||||
for (const item of items) {
|
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添加到展开集合
|
// 将所有父菜单ID添加到展开集合
|
||||||
parentIds.forEach(id => newExpandedMenus.add(id));
|
parentIds.forEach(id => newExpandedMenus.add(id));
|
||||||
return true;
|
return true;
|
||||||
@@ -60,11 +214,16 @@ export function Sidebar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
findActiveMenu(menuItems);
|
findActiveMenu(menuItems);
|
||||||
|
|
||||||
|
// 将新的展开菜单集合设置到状态
|
||||||
setExpandedMenus(newExpandedMenus);
|
setExpandedMenus(newExpandedMenus);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换菜单展开状态
|
// 切换菜单展开状态
|
||||||
const toggleMenu = (menuId: number) => {
|
const toggleMenu = (menuId: number, e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
setExpandedMenus(prev => {
|
setExpandedMenus(prev => {
|
||||||
const newExpanded = new Set(prev);
|
const newExpanded = new Set(prev);
|
||||||
if (newExpanded.has(menuId)) {
|
if (newExpanded.has(menuId)) {
|
||||||
@@ -83,82 +242,207 @@ export function Sidebar() {
|
|||||||
return Icon ? <Icon className="h-4 w-4 mr-2" /> : null;
|
return Icon ? <Icon className="h-4 w-4 mr-2" /> : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 递归渲染菜单项
|
// 渲染菜单项
|
||||||
const renderMenuItem = (item: MenuItem) => {
|
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 hasChildren = item.children && item.children.length > 0;
|
||||||
const isExpanded = expandedMenus.has(item.id);
|
const isExpanded = expandedMenus.has(item.id);
|
||||||
const isActive = pathname === item.path;
|
const name = item.name || item.title || "";
|
||||||
const isChildActive = hasChildren && item.children!.some(child =>
|
|
||||||
pathname === child.path || pathname.startsWith(child.path + "/")
|
// 折叠状态下的菜单项
|
||||||
);
|
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 (
|
return (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<div className="flex flex-col">
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleMenu(item.id)}
|
onClick={(e) => toggleMenu(item.id, e)}
|
||||||
className={`flex items-center justify-between px-4 py-2 rounded-md text-sm w-full text-left ${
|
className={cn(
|
||||||
isActive || isChildActive
|
"flex items-center w-full py-2 px-3 rounded-md transition-colors",
|
||||||
? "text-white font-semibold"
|
isActive
|
||||||
: "hover:bg-blue-600"
|
? "text-white font-medium"
|
||||||
}`}
|
: "text-blue-100 hover:bg-blue-700/30"
|
||||||
>
|
|
||||||
<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" />
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
|
{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>
|
</button>
|
||||||
|
|
||||||
{isExpanded && hasChildren && (
|
{isExpanded && (
|
||||||
<ul className="ml-4 mt-1 space-y-1">
|
<ul className="ml-4 mt-1 space-y-1">
|
||||||
{item.children!.map(child => {
|
{item.children!.map((child) => (
|
||||||
const isChildItemActive = pathname === child.path;
|
<li key={child.id}>
|
||||||
return (
|
<a
|
||||||
<li key={child.id}>
|
href={child.path}
|
||||||
<Link
|
onClick={(e) => {
|
||||||
href={child.path}
|
e.preventDefault();
|
||||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${
|
e.stopPropagation();
|
||||||
isChildItemActive
|
|
||||||
? "text-white font-semibold bg-blue-700"
|
// 使用addTab返回的标签ID,在TabContext中处理,确保标签被激活并导航
|
||||||
: "hover:bg-blue-600"
|
const tabId = addTab({
|
||||||
}`}
|
label: child.name || child.title || "",
|
||||||
>
|
path: child.path,
|
||||||
{child.icon && getLucideIcon(child.icon)}
|
closable: true
|
||||||
{child.title}
|
});
|
||||||
</Link>
|
}}
|
||||||
</li>
|
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>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<a
|
||||||
href={item.path}
|
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
|
isActive
|
||||||
? "text-white font-semibold"
|
? "text-white font-medium"
|
||||||
: "hover:bg-blue-600"
|
: "text-blue-100 hover:bg-blue-700/30"
|
||||||
}`}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon && getLucideIcon(item.icon)}
|
{getLucideIcon(item.icon || "")}
|
||||||
{item.title}
|
<span>{name}</span>
|
||||||
</Link>
|
</a>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 border-r bg-[#2563eb] h-full flex flex-col text-white">
|
<div className={cn(
|
||||||
<div className="p-4 border-b border-blue-500">
|
"border-r bg-[#2563eb] h-full flex flex-col text-white transition-all duration-300 ease-in-out",
|
||||||
<h2 className="text-lg font-bold">超级管理员</h2>
|
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>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-1 overflow-auto p-2">
|
<nav className="flex-1 overflow-auto p-2">
|
||||||
@@ -176,8 +460,11 @@ export function Sidebar() {
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
// 无菜单数据
|
// 无菜单数据
|
||||||
<div className="text-center py-8 text-blue-200">
|
<div className={cn(
|
||||||
<p>暂无菜单数据</p>
|
"text-center py-8 text-blue-200",
|
||||||
|
collapsed && "text-xs px-0"
|
||||||
|
)}>
|
||||||
|
<p>{collapsed ? "无菜单" : "暂无菜单数据"}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</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