454 lines
12 KiB
TypeScript
454 lines
12 KiB
TypeScript
/**
|
||
* 管理后台状态管理 Hook
|
||
* 封装管理后台的所有状态和数据获取逻辑
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||
import { useToast } from './useToast'
|
||
|
||
// ========== 类型定义 ==========
|
||
|
||
export interface User {
|
||
id: string
|
||
email: string
|
||
username: string
|
||
department: string
|
||
role: string
|
||
isActive: boolean
|
||
canDownload: boolean
|
||
managedDepartment?: string
|
||
createdAt: string
|
||
_count: {
|
||
materials: number
|
||
}
|
||
}
|
||
|
||
export interface Material {
|
||
id: string
|
||
previewUrl: string
|
||
productModel: string
|
||
categoryName: string
|
||
sceneName: string
|
||
materialType: string
|
||
renderer: string
|
||
authorName: string
|
||
createdAt: string
|
||
videoCoverUrl?: string
|
||
videoPreviewClipUrl?: string
|
||
videoPreview540Url?: string
|
||
user: {
|
||
username: string
|
||
}
|
||
}
|
||
|
||
export interface Category {
|
||
id: string
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface Scene {
|
||
id: string
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface Platform {
|
||
id: string
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface ProductCategory {
|
||
id: string
|
||
name: string
|
||
count: number
|
||
}
|
||
|
||
export interface Department {
|
||
id: string
|
||
name: string
|
||
// 阶段三:部门层级结构字段
|
||
code?: string | null
|
||
parentId?: string | null
|
||
level?: number
|
||
managerId?: string | null
|
||
status?: string
|
||
description?: string | null
|
||
sortOrder?: number
|
||
createdAt?: string
|
||
updatedAt?: string
|
||
// 关联数据(可选,通过includeHierarchy获取)
|
||
parent?: Department | null
|
||
children?: Department[]
|
||
manager?: {
|
||
id: string
|
||
email: string
|
||
username: string
|
||
realName?: string | null
|
||
} | null
|
||
}
|
||
|
||
export type AdminTab = 'materials' | 'users' | 'categories' | 'departments' | 'permissions' | 'superAdmins'
|
||
|
||
// ========== Hook 实现 ==========
|
||
|
||
export function useAdminState() {
|
||
// 当前标签页
|
||
const [activeTab, setActiveTab] = useState<AdminTab>('materials')
|
||
|
||
// 数据状态
|
||
const [materials, setMaterials] = useState<Material[]>([])
|
||
const [users, setUsers] = useState<User[]>([])
|
||
const [categories, setCategories] = useState<Category[]>([])
|
||
const [scenes, setScenes] = useState<Scene[]>([])
|
||
const [platforms, setPlatforms] = useState<Platform[]>([])
|
||
const [productCategories, setProductCategories] = useState<ProductCategory[]>([])
|
||
const [departments, setDepartments] = useState<Department[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
// 编辑状态
|
||
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
|
||
const [editingScene, setEditingScene] = useState<Scene | null>(null)
|
||
const [editingPlatform, setEditingPlatform] = useState<Platform | null>(null)
|
||
const [editingProductCategory, setEditingProductCategory] = useState<ProductCategory | null>(null)
|
||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||
|
||
// 添加弹窗状态
|
||
const [showAddCategoryModal, setShowAddCategoryModal] = useState(false)
|
||
const [showAddSceneModal, setShowAddSceneModal] = useState(false)
|
||
const [showAddPlatformModal, setShowAddPlatformModal] = useState(false)
|
||
const [showAddProductCategoryModal, setShowAddProductCategoryModal] = useState(false)
|
||
|
||
// 新建输入状态
|
||
const [newCategoryName, setNewCategoryName] = useState('')
|
||
const [newSceneName, setNewSceneName] = useState('')
|
||
const [newPlatformName, setNewPlatformName] = useState('')
|
||
const [newProductCategoryName, setNewProductCategoryName] = useState('')
|
||
const [newDepartmentName, setNewDepartmentName] = useState('')
|
||
const [addingDepartment, setAddingDepartment] = useState(false)
|
||
const [selectedDepartment, setSelectedDepartment] = useState('')
|
||
|
||
// 搜索状态
|
||
const [userSearchTerm, setUserSearchTerm] = useState('')
|
||
const [materialSearchTerm, setMaterialSearchTerm] = useState('')
|
||
|
||
// 素材批量操作
|
||
const [selectedMaterials, setSelectedMaterials] = useState<Set<string>>(new Set())
|
||
const [showRecycleBin, setShowRecycleBin] = useState(false)
|
||
|
||
// 折叠状态
|
||
const [collapsedDepartments, setCollapsedDepartments] = useState<Set<string>>(new Set())
|
||
const [collapsedAdmins, setCollapsedAdmins] = useState<Set<string>>(new Set())
|
||
|
||
// 素材分页
|
||
const [displayedMaterialsCount, setDisplayedMaterialsCount] = useState(24)
|
||
const [currentMaterialsPage, setCurrentMaterialsPage] = useState(1)
|
||
const [hasMoreMaterials, setHasMoreMaterials] = useState(true)
|
||
const [isLoadingMoreMaterials, setIsLoadingMoreMaterials] = useState(false)
|
||
const materialsPageSize = 20
|
||
const materialSearchDebounceRef = useRef<NodeJS.Timeout | null>(null)
|
||
|
||
// 使用统一的Toast Hook
|
||
const { toast, showToast } = useToast()
|
||
|
||
// ========== 数据获取函数 ==========
|
||
|
||
const fetchUsers = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/admin/users')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setUsers(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching users:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchCategories = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/categories')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setCategories(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching categories:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchScenes = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/scenes')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setScenes(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching scenes:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchPlatforms = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/platforms')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setPlatforms(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching platforms:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchProductCategories = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/product-categories')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setProductCategories(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching product categories:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchDepartments = useCallback(async () => {
|
||
try {
|
||
const response = await fetch('/api/departments')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setDepartments(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching departments:', error)
|
||
}
|
||
}, [])
|
||
|
||
const fetchMaterials = useCallback(async (page = 1, append = false) => {
|
||
try {
|
||
if (!append) {
|
||
setLoading(true)
|
||
}
|
||
const url = showRecycleBin
|
||
? `/api/materials?page=${page}&pageSize=${materialsPageSize}&deleted=true`
|
||
: `/api/materials?page=${page}&pageSize=${materialsPageSize}`
|
||
const response = await fetch(url)
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
const materialsData = data.materials || data
|
||
if (append) {
|
||
setMaterials(prev => [...prev, ...materialsData])
|
||
} else {
|
||
setMaterials(materialsData)
|
||
}
|
||
setHasMoreMaterials(materialsData.length === materialsPageSize)
|
||
setCurrentMaterialsPage(page)
|
||
setDisplayedMaterialsCount(append ? materials.length + materialsData.length : materialsData.length)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching materials:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
setIsLoadingMoreMaterials(false)
|
||
}
|
||
}, [showRecycleBin, materialsPageSize, materials.length])
|
||
|
||
const searchMaterials = useCallback(async (searchTerm: string) => {
|
||
try {
|
||
const hasSearch = searchTerm.trim().length > 0
|
||
const url = showRecycleBin
|
||
? `/api/materials?page=1&pageSize=${hasSearch ? 10000 : materialsPageSize}&deleted=true${hasSearch ? `&search=${encodeURIComponent(searchTerm.trim())}` : ''}`
|
||
: `/api/materials?page=1&pageSize=${hasSearch ? 10000 : materialsPageSize}${hasSearch ? `&search=${encodeURIComponent(searchTerm.trim())}` : ''}`
|
||
const response = await fetch(url)
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
const materialsData = data.materials || data
|
||
setMaterials(materialsData)
|
||
setDisplayedMaterialsCount(materialsData.length)
|
||
setCurrentMaterialsPage(1)
|
||
setHasMoreMaterials(!hasSearch && materialsData.length === materialsPageSize)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error searching materials:', error)
|
||
}
|
||
}, [showRecycleBin, materialsPageSize])
|
||
|
||
const loadMoreMaterials = useCallback(async () => {
|
||
if (isLoadingMoreMaterials || !hasMoreMaterials || materialSearchTerm.trim()) return
|
||
setIsLoadingMoreMaterials(true)
|
||
await fetchMaterials(currentMaterialsPage + 1, true)
|
||
}, [isLoadingMoreMaterials, hasMoreMaterials, materialSearchTerm, currentMaterialsPage, fetchMaterials])
|
||
|
||
// 初始化数据
|
||
const initializeData = useCallback(async () => {
|
||
setLoading(true)
|
||
await Promise.all([
|
||
fetchUsers(),
|
||
fetchCategories(),
|
||
fetchScenes(),
|
||
fetchPlatforms(),
|
||
fetchProductCategories(),
|
||
fetchDepartments(),
|
||
fetchMaterials(1, false),
|
||
])
|
||
setLoading(false)
|
||
}, [fetchUsers, fetchCategories, fetchScenes, fetchPlatforms, fetchProductCategories, fetchDepartments, fetchMaterials])
|
||
|
||
// ========== 折叠操作 ==========
|
||
|
||
const toggleDepartmentCollapse = useCallback((dept: string) => {
|
||
setCollapsedDepartments(prev => {
|
||
const newSet = new Set(prev)
|
||
if (newSet.has(dept)) {
|
||
newSet.delete(dept)
|
||
} else {
|
||
newSet.add(dept)
|
||
}
|
||
return newSet
|
||
})
|
||
}, [])
|
||
|
||
const toggleAdminCollapse = useCallback((adminId: string) => {
|
||
setCollapsedAdmins(prev => {
|
||
const newSet = new Set(prev)
|
||
if (newSet.has(adminId)) {
|
||
newSet.delete(adminId)
|
||
} else {
|
||
newSet.add(adminId)
|
||
}
|
||
return newSet
|
||
})
|
||
}, [])
|
||
|
||
// ========== 素材选择操作 ==========
|
||
|
||
const toggleMaterialSelection = useCallback((id: string) => {
|
||
setSelectedMaterials(prev => {
|
||
const newSet = new Set(prev)
|
||
if (newSet.has(id)) {
|
||
newSet.delete(id)
|
||
} else {
|
||
newSet.add(id)
|
||
}
|
||
return newSet
|
||
})
|
||
}, [])
|
||
|
||
const selectAllMaterials = useCallback(() => {
|
||
setSelectedMaterials(new Set(materials.map(m => m.id)))
|
||
}, [materials])
|
||
|
||
const clearMaterialSelection = useCallback(() => {
|
||
setSelectedMaterials(new Set())
|
||
}, [])
|
||
|
||
return {
|
||
// 标签页
|
||
activeTab,
|
||
setActiveTab,
|
||
|
||
// 数据
|
||
materials,
|
||
setMaterials,
|
||
users,
|
||
setUsers,
|
||
categories,
|
||
setCategories,
|
||
scenes,
|
||
setScenes,
|
||
platforms,
|
||
setPlatforms,
|
||
productCategories,
|
||
setProductCategories,
|
||
departments,
|
||
setDepartments,
|
||
loading,
|
||
setLoading,
|
||
|
||
// 编辑状态
|
||
editingCategory,
|
||
setEditingCategory,
|
||
editingScene,
|
||
setEditingScene,
|
||
editingPlatform,
|
||
setEditingPlatform,
|
||
editingProductCategory,
|
||
setEditingProductCategory,
|
||
editingUser,
|
||
setEditingUser,
|
||
|
||
// 添加弹窗
|
||
showAddCategoryModal,
|
||
setShowAddCategoryModal,
|
||
showAddSceneModal,
|
||
setShowAddSceneModal,
|
||
showAddPlatformModal,
|
||
setShowAddPlatformModal,
|
||
showAddProductCategoryModal,
|
||
setShowAddProductCategoryModal,
|
||
|
||
// 新建输入
|
||
newCategoryName,
|
||
setNewCategoryName,
|
||
newSceneName,
|
||
setNewSceneName,
|
||
newPlatformName,
|
||
setNewPlatformName,
|
||
newProductCategoryName,
|
||
setNewProductCategoryName,
|
||
newDepartmentName,
|
||
setNewDepartmentName,
|
||
addingDepartment,
|
||
setAddingDepartment,
|
||
selectedDepartment,
|
||
setSelectedDepartment,
|
||
|
||
// 搜索
|
||
userSearchTerm,
|
||
setUserSearchTerm,
|
||
materialSearchTerm,
|
||
setMaterialSearchTerm,
|
||
|
||
// 素材批量操作
|
||
selectedMaterials,
|
||
setSelectedMaterials,
|
||
showRecycleBin,
|
||
setShowRecycleBin,
|
||
toggleMaterialSelection,
|
||
selectAllMaterials,
|
||
clearMaterialSelection,
|
||
|
||
// 折叠状态
|
||
collapsedDepartments,
|
||
collapsedAdmins,
|
||
toggleDepartmentCollapse,
|
||
toggleAdminCollapse,
|
||
|
||
// 素材分页
|
||
displayedMaterialsCount,
|
||
currentMaterialsPage,
|
||
hasMoreMaterials,
|
||
isLoadingMoreMaterials,
|
||
materialsPageSize,
|
||
materialSearchDebounceRef,
|
||
|
||
// Toast
|
||
toast,
|
||
showToast,
|
||
|
||
// 数据获取函数
|
||
fetchUsers,
|
||
fetchCategories,
|
||
fetchScenes,
|
||
fetchPlatforms,
|
||
fetchProductCategories,
|
||
fetchDepartments,
|
||
fetchMaterials,
|
||
searchMaterials,
|
||
loadMoreMaterials,
|
||
initializeData,
|
||
}
|
||
}
|