diff --git a/SuperAdmin/app/dashboard/admins/page.tsx b/SuperAdmin/app/dashboard/admins/page.tsx index af272b9a..51e80f71 100644 --- a/SuperAdmin/app/dashboard/admins/page.tsx +++ b/SuperAdmin/app/dashboard/admins/page.tsx @@ -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) } diff --git a/SuperAdmin/app/dashboard/customers/page.tsx b/SuperAdmin/app/dashboard/customers/page.tsx index ee613e8f..0fe327bd 100644 --- a/SuperAdmin/app/dashboard/customers/page.tsx +++ b/SuperAdmin/app/dashboard/customers/page.tsx @@ -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 (
@@ -217,11 +110,7 @@ export default function CustomersPage() { 所有地区 - {regions.map((region) => ( - - {region} - - ))} + {/* 从API获取到的regions数据应在此处映射 */}
@@ -248,28 +137,20 @@ export default function CustomersPage() { 所有来源 - {sources.map((source) => ( - - {source} - - ))} + {/* 从API获取到的sources数据应在此处映射 */}
-

所属项目

+

项目

@@ -277,16 +158,16 @@ export default function CustomersPage() { -
+
- 客户昵称 - 微信ID + 姓名 性别 地区 来源 - 公司名称 + 标签 + 所属项目 添加时间 操作 @@ -294,45 +175,45 @@ export default function CustomersPage() { {isLoading ? ( - - 加载中... - + 加载中... ) : error ? ( - - {error} - + {error} - ) : customers.length > 0 ? ( + ) : customers.length === 0 ? ( + + 没有符合条件的客户 + + ) : ( customers.map((customer) => ( -
- - { - // 图片加载失败时使用默认图片 - const target = e.target as HTMLImageElement; - target.src = "/placeholder.svg?height=40&width=40"; - }} - /> - {(customer.nickname || "未知").slice(0, 2)} +
+ + + {customer.nickname ? customer.nickname.substring(0, 1) : "?"}
-
{customer.nickname || "未知"}
-
{customer.gender}
+
{customer.nickname}
+
{customer.wechatId}
- {customer.wechatId} {customer.gender} {customer.region} {customer.source} - {customer.projectName} - {customer.addTime} + +
+ {customer.tags && customer.tags.map((tag) => ( + + {tag} + + ))} +
+
+ {customer.companyName} + {customer.createTime} @@ -342,39 +223,41 @@ export default function CustomersPage() { - - - 查看详情 + + + + 查看详情 - - 分发客户 - + + 分配客服 + 添加标签 )) - ) : ( - - - 未找到客户 - - )}
- - {/* 使用新的分页组件 */} - + + {/* 分页控制 */} +
+
+ 共 {totalItems} 条记录,第 {currentPage} 页,共 {totalPages} 页 +
+
+ +
+
) diff --git a/SuperAdmin/app/dashboard/layout.tsx b/SuperAdmin/app/dashboard/layout.tsx index aa73fed2..af35df0a 100644 --- a/SuperAdmin/app/dashboard/layout.tsx +++ b/SuperAdmin/app/dashboard/layout.tsx @@ -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) => string + closeTab: (tabId: string) => void + setActiveTab: (tabId: string) => void + findTabByPath: (path: string) => TabData | undefined +} + +const TabContext = createContext(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([ + { id: "dashboard", label: "仪表盘", path: "/dashboard", closable: false } + ]) + const [activeTab, setActiveTab] = useState("dashboard") + + // 添加标签页 + const addTab = (tabData: Omit) => { + 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 ( -
- {/* Mobile sidebar toggle */} -
- -
+ +
+ {/* Mobile sidebar toggle */} +
+ +
- {/* Sidebar */} -
- -
+ {/* Sidebar */} +
+ +
- {/* Main content */} -
-
-
{children}
+ {/* Main content */} +
+
+ + {/* 标签栏 */} +
+
+ {tabs.map(tab => ( +
{ + setActiveTabAndNavigate(tab.id) + }} + > + {tab.label} + {tab.closable && ( + + )} +
+ ))} +
+
+ + {/* 内容区域 */} +
+ {children} +
+
-
+
) } diff --git a/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx b/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx index 051e2ec7..9c7f266e 100644 --- a/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx +++ b/SuperAdmin/app/dashboard/projects/[id]/edit/page.tsx @@ -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(promise: Promise): T; + function use(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(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 } })
- {project?.devices.length > 0 && project.devices.map((device) => ( + {project && project.devices && project.devices.length > 0 && project.devices.map((device) => (
))} -
@@ -245,11 +304,43 @@ export default function EditProjectPage({ params }: { params: { id: string } }) 取消 + + {/* 使用Dialog组件替代自定义模态框 */} + !open && closeModal()}> + + + 添加设备 + + 请使用新设备进行扫码添加 + + +
+
+ {qrCodeData ? ( + 设备二维码 + ) : ( +
+

二维码加载中...

+
+ )} +
+
+ + + +
+
) } diff --git a/SuperAdmin/app/dashboard/projects/[id]/page.tsx b/SuperAdmin/app/dashboard/projects/[id]/page.tsx index 0fe60923..d0ac9f7c 100644 --- a/SuperAdmin/app/dashboard/projects/[id]/page.tsx +++ b/SuperAdmin/app/dashboard/projects/[id]/page.tsx @@ -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([]) const [subUsers, setSubUsers] = useState([]) 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 ? (
暂无数据
) : ( - - - - 设备名称 - 设备型号 - 品牌 - IMEI - 设备状态 - 微信状态 - 微信好友数量 - - - - {devices.map((device) => ( - - {device.memo} - {device.model} - {device.brand} - {device.imei} - {device.alive === 1 ? "在线" : "离线"} - {device.wAlive === 1 ? "在线" : device.wAlive === 0 ? "离线" : "未登录微信"} - {device.friendCount || 0} + <> +
+ + + 设备名称 + 设备型号 + 品牌 + IMEI + 设备状态 + 微信状态 + 微信好友数量 - ))} - -
+ + + {devices.map((device) => ( + + {device.memo} + {device.model} + {device.brand} + {device.imei} + + + {device.alive === 1 ? "在线" : "离线"} + + + + + {device.wAlive === 1 ? "已登录" : device.wAlive === 0 ? "已登出" : "未登录微信"} + + + {device.friendCount || 0} + + ))} + + +
+ 共 {devices.length} 条数据 +
+ )} @@ -286,42 +307,51 @@ export default function ProjectDetailPage({ params }: { params: { id: string } } ) : subUsers.length === 0 ? (
暂无数据
) : ( - - - - 头像 - 账号ID - 登录账号 - 昵称 - 手机号 - 状态 - 账号类型 - 创建时间 - - - - {subUsers.map((user) => ( - - - - - {user.id} - {user.account} - {user.username} - {user.phone} - {user.status === 1 ? "登录" : "禁用"} - {user.typeId === 1 ? "操盘手" : "门店顾问"} - {user.createTime} + <> +
+ + + 头像 + 账号ID + 登录账号 + 昵称 + 手机号 + 状态 + 账号类型 + 创建时间 - ))} - -
+ + + {subUsers.map((user) => ( + + + {user.username} + + {user.id} + {user.account} + {user.username} + {user.phone} + + + {user.status === 1 ? "启用" : "禁用"} + + + {user.typeId === 1 ? "操盘手" : "门店顾问"} + {user.createTime} + + ))} + + +
+ 共 {subUsers.length} 条数据 +
+ )} diff --git a/SuperAdmin/app/dashboard/projects/page.tsx b/SuperAdmin/app/dashboard/projects/page.tsx index 609c3680..4b93d9eb 100644 --- a/SuperAdmin/app/dashboard/projects/page.tsx +++ b/SuperAdmin/app/dashboard/projects/page.tsx @@ -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([]) 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(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 (

项目列表

-
@@ -175,7 +235,7 @@ export default function ProjectsPage() { {isLoading ? ( - + 加载中... @@ -200,15 +260,11 @@ export default function ProjectsPage() { - - - 查看详情 - + handleViewProject(project)}> + 查看详情 - - - 编辑项目 - + handleEditProject(project)}> + 编辑项目 - + 未找到项目 @@ -237,7 +293,7 @@ export default function ProjectsPage() { totalPages={totalPages} pageSize={pageSize} totalItems={totalItems} - onPageChange={setCurrentPage} + onPageChange={handlePageChange} onPageSizeChange={handlePageSizeChange} /> diff --git a/SuperAdmin/app/page.tsx b/SuperAdmin/app/page.tsx new file mode 100644 index 00000000..e8fb4476 --- /dev/null +++ b/SuperAdmin/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function Home() { + redirect("/dashboard") +} \ No newline at end of file diff --git a/SuperAdmin/components/layout/sidebar.tsx b/SuperAdmin/components/layout/sidebar.tsx index 73993682..1297ff6b 100644 --- a/SuperAdmin/components/layout/sidebar.tsx +++ b/SuperAdmin/components/layout/sidebar.tsx @@ -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([]) const [loading, setLoading] = useState(true) - // 使用Set来存储已展开的菜单ID const [expandedMenus, setExpandedMenus] = useState>(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(); + 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 ? : 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 ( +
  • +
    { + e.preventDefault(); + e.stopPropagation(); + if (!hasChildren) { + const tabId = addTab({ + label: name, + path: item.path, + closable: item.path !== "/dashboard" + }); + } + }} + > + {getLucideIcon(item.icon || "")} + + {/* 悬浮提示 */} + {hasChildren ? ( + + ) : ( +
    + {name} +
    + )} +
    +
  • + ); + } + + // 展开状态下的菜单项 return (
  • {hasChildren ? ( -
    + <> - {isExpanded && hasChildren && ( + {isExpanded && ( )} -
    + ) : ( - { + 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} - + {getLucideIcon(item.icon || "")} + {name} + )}
  • ); }; return ( -
    -
    -

    超级管理员

    +
    +
    + {!collapsed &&

    超级管理员

    } +
    diff --git a/SuperAdmin/components/projects/project-create.tsx b/SuperAdmin/components/projects/project-create.tsx new file mode 100644 index 00000000..85cb575d --- /dev/null +++ b/SuperAdmin/components/projects/project-create.tsx @@ -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>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + account: "", + password: "", + confirmPassword: "", + phone: "", + realname: "", + nickname: "", + memo: "", + }, + }) + + const onSubmit = async (values: z.infer) => { + 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 ( + + + 新建项目 + 创建一个新的项目并设置基本信息 + + +
    + + ( + + 项目名称 + + + + + + )} + /> + +
    + ( + + 账号 + + + + + + )} + /> + ( + + 手机号 + + + + + + )} + /> +
    + +
    + ( + + 密码 + + + + + + )} + /> + ( + + 确认密码 + + + + + + )} + /> +
    + +
    + ( + + 真实姓名 + + + + + + )} + /> + ( + + 昵称 + + + + + + )} + /> +
    + + ( + + 项目描述 + +