This commit is contained in:
menxipeng
2025-10-12 11:17:41 +08:00
commit c242069658
66 changed files with 17940 additions and 0 deletions

109
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,109 @@
// 系统常量定义
export const USER_ROLES = {
HEADQUARTERS: "headquarters",
DEALER: "dealer",
MALL: "mall",
MERCHANT: "merchant",
WORKER: "worker",
} as const
export const EQUIPMENT_STATUS = {
NORMAL: "normal",
WARNING: "warning",
EXPIRED: "expired",
FAULT: "fault",
} as const
export const WORK_ORDER_STATUS = {
PENDING: "pending",
ASSIGNED: "assigned",
IN_PROGRESS: "in_progress",
COMPLETED: "completed",
CANCELLED: "cancelled",
} as const
export const WORK_ORDER_TYPE = {
MAINTENANCE: "maintenance",
REPAIR: "repair",
INSPECTION: "inspection",
} as const
export const PRIORITY_LEVELS = {
LOW: "low",
NORMAL: "normal",
HIGH: "high",
URGENT: "urgent",
} as const
// 权限配置
export const ROLE_PERMISSIONS = {
[USER_ROLES.HEADQUARTERS]: {
canManageAllDealers: true,
canManageAllMalls: true,
canViewAllEquipment: true,
canViewAllWorkOrders: true,
canManageUsers: true,
canViewReports: true,
},
[USER_ROLES.DEALER]: {
canManageWorkers: true,
canManageRegionEquipment: true,
canManageWorkOrders: true,
canManageInventory: true,
canViewRegionReports: true,
},
[USER_ROLES.MALL]: {
canManageMerchants: true,
canViewMallEquipment: true,
canCreateWorkOrders: true,
},
[USER_ROLES.MERCHANT]: {
canViewOwnEquipment: true,
canCreateRepairOrders: true,
canViewOwnWorkOrders: true,
},
[USER_ROLES.WORKER]: {
canViewAssignedOrders: true,
canUpdateOrderStatus: true,
canUploadFiles: true,
},
}
// 设备状态颜色映射
export const EQUIPMENT_STATUS_COLORS = {
[EQUIPMENT_STATUS.NORMAL]: "text-green-600 bg-green-50",
[EQUIPMENT_STATUS.WARNING]: "text-yellow-600 bg-yellow-50",
[EQUIPMENT_STATUS.EXPIRED]: "text-red-600 bg-red-50",
[EQUIPMENT_STATUS.FAULT]: "text-red-600 bg-red-100",
}
// 工单状态颜色映射
export const WORK_ORDER_STATUS_COLORS = {
[WORK_ORDER_STATUS.PENDING]: "text-gray-600 bg-gray-50",
[WORK_ORDER_STATUS.ASSIGNED]: "text-blue-600 bg-blue-50",
[WORK_ORDER_STATUS.IN_PROGRESS]: "text-yellow-600 bg-yellow-50",
[WORK_ORDER_STATUS.COMPLETED]: "text-green-600 bg-green-50",
[WORK_ORDER_STATUS.CANCELLED]: "text-red-600 bg-red-50",
}
// 优先级颜色映射
export const PRIORITY_COLORS = {
[PRIORITY_LEVELS.LOW]: "text-gray-600 bg-gray-50",
[PRIORITY_LEVELS.NORMAL]: "text-blue-600 bg-blue-50",
[PRIORITY_LEVELS.HIGH]: "text-orange-600 bg-orange-50",
[PRIORITY_LEVELS.URGENT]: "text-red-600 bg-red-50",
}
// 系统配置
export const SYSTEM_CONFIG = {
// 设备到期提醒天数
EQUIPMENT_WARNING_DAYS: [30, 7],
// 分页默认大小
DEFAULT_PAGE_SIZE: 20,
// 文件上传限制
MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB
ALLOWED_FILE_TYPES: ["image/jpeg", "image/png", "image/gif", "video/mp4", "application/pdf"],
// 库存预警阈值
LOW_STOCK_THRESHOLD: 10,
}

View File

@@ -0,0 +1,78 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from 'react'
import { RouteItem, SidebarMenuItem } from '@/lib/types/route'
import { getRouters } from '@/lib/services/route'
import { transformRoutes, generateSidebarMenu } from '@/lib/utils/route'
interface RouteContextType {
routes: RouteItem[]
sidebarRoutes: SidebarMenuItem[]
loading: boolean
error: string | null
refreshRoutes: () => Promise<void>
}
const RouteContext = createContext<RouteContextType | undefined>(undefined)
export function RouteProvider({ children }: { children: React.ReactNode }) {
const [routes, setRoutes] = useState<RouteItem[]>([])
const [sidebarRoutes, setSidebarRoutes] = useState<SidebarMenuItem[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchRoutes = async () => {
try {
setLoading(true)
setError(null)
const response = await getRouters()
if (response.code === 200) {
const transformedRoutes = transformRoutes(response.data)
const sidebarMenu = generateSidebarMenu(transformedRoutes)
setRoutes(transformedRoutes)
setSidebarRoutes(sidebarMenu)
} else {
throw new Error(response.msg || '获取路由失败')
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '未知错误'
setError(errorMessage)
console.error('Failed to fetch routes:', err)
} finally {
setLoading(false)
}
}
const refreshRoutes = async () => {
await fetchRoutes()
}
useEffect(() => {
fetchRoutes()
}, [])
const value: RouteContextType = {
routes,
sidebarRoutes,
loading,
error,
refreshRoutes,
}
return (
<RouteContext.Provider value={value}>
{children}
</RouteContext.Provider>
)
}
export function useRoutes() {
const context = useContext(RouteContext)
if (context === undefined) {
throw new Error('useRoutes must be used within a RouteProvider')
}
return context
}

137
src/lib/services/api.ts Normal file
View File

@@ -0,0 +1,137 @@
import { getUserToken, removeStorageItem, runOnClient } from '@/lib/utils/storage'
// API 客户端工具,统一处理请求和认证
const API_BASE_URL = 'http://116.204.124.80:8080/api'
// 获取存储的 token
function getToken(): string | null {
return getUserToken()
}
// 统一的 API 请求函数
export async function apiRequest<T = any>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
// 确保只在客户端执行
if (typeof window === 'undefined') {
throw new Error('API calls can only be made on the client side')
}
const token = getToken()
// 构建完整的 URL
const url = endpoint.startsWith('http') ? endpoint : `${API_BASE_URL}${endpoint}`
// 默认请求头
const defaultHeaders: HeadersInit = {
'Content-Type': 'application/json',
}
// 如果有 token添加到请求头
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`
}
// 合并请求头
const headers = {
...defaultHeaders,
...options.headers,
}
// 构建请求配置
const requestConfig = {
...options,
headers,
}
try {
const response = await fetch(url, requestConfig)
const data = await response.json()
// 检查响应状态
if (!response.ok) {
// 构建调试信息对象
const debugInfo = {
url: url,
endpoint: endpoint,
method: requestConfig.method || options.method || 'GET',
headers: headers,
status: response.status,
statusText: response.statusText,
timestamp: new Date().toISOString()
}
// 如果是 401 未授权,清除本地存储的用户信息
if (response.status === 401) {
console.error('API 401 未授权错误:', debugInfo)
removeStorageItem('user')
// 安全地执行客户端跳转
runOnClient(() => {
window.location.href = '/login'
})
}
// 对于 404 错误,提供更详细的调试信息
if (response.status === 404) {
console.error('API 404 错误详情:', debugInfo)
throw new Error(data.msg || `API 端点未找到 (404): ${endpoint}. 请检查端点是否正确或服务器是否正在运行。完整URL: ${url}`)
}
// 记录其他HTTP错误
console.error('API HTTP 错误:', debugInfo)
throw new Error(data.msg || `HTTP error! status: ${response.status}`)
}
return data
} catch (error) {
// 如果是网络错误或其他fetch错误也记录调试信息
if (error instanceof TypeError && error.message.includes('fetch')) {
console.error('API 网络错误:', {
url: url,
endpoint: endpoint,
method: requestConfig.method || options.method || 'GET',
error: error.message,
timestamp: new Date().toISOString()
})
}
console.error('API 请求失败:', error)
throw error
}
}
// GET 请求
export async function apiGet<T = any>(endpoint: string): Promise<T> {
return apiRequest<T>(endpoint, { method: 'GET' })
}
// POST 请求
export async function apiPost<T = any>(
endpoint: string,
data?: any
): Promise<T> {
return apiRequest<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
})
}
// PUT 请求
export async function apiPut<T = any>(
endpoint: string,
data?: any
): Promise<T> {
return apiRequest<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
})
}
// DELETE 请求
export async function apiDelete<T = any>(endpoint: string): Promise<T> {
return apiRequest<T>(endpoint, { method: 'DELETE' })
}
// 导出 API_BASE_URL 供其他地方使用
export { API_BASE_URL }

View File

@@ -0,0 +1,11 @@
import { DealerResponse } from '@/lib/types/dealer';
import { apiGet } from './api';
export async function getDealers(): Promise<DealerResponse> {
try {
return await apiGet<DealerResponse>('/back/findNextDealer');
} catch (error) {
console.error('Failed to fetch dealers:', error);
throw error;
}
}

View File

@@ -0,0 +1,6 @@
import { apiGet } from './api';
import { MerchantResponse } from '@/lib/types/merchant';
export const getMerchants = async (): Promise<MerchantResponse> => {
return await apiGet('/back/findNextInfo');
};

View File

@@ -0,0 +1,11 @@
import { ProvinceResponse } from '@/lib/types/region';
import { apiGet } from './api';
export async function getProvinces(): Promise<ProvinceResponse> {
try {
return await apiGet<ProvinceResponse>('/back/region/provinces');
} catch (error) {
console.error('Failed to fetch provinces:', error);
throw error;
}
}

37
src/lib/services/route.ts Normal file
View File

@@ -0,0 +1,37 @@
import { RouteResponse } from '@/lib/types/route'
import { apiGet } from './api'
import { getUserRoleId as getStorageRoleId } from '@/lib/utils/storage'
// 获取用户的 roleId
function getUserRoleId(): string | null {
return getStorageRoleId()
}
// 获取路由配置根据角色ID
export async function getRouters(): Promise<RouteResponse> {
try {
const roleId = getUserRoleId()
if (!roleId) {
throw new Error('未找到用户角色信息,请重新登录')
}
// 使用 GET 请求传递 roleId 参数
const data = await apiGet<RouteResponse>(`/getRoutersByRoleId?roleId=${roleId}`)
return data
} catch (error) {
console.error('Failed to fetch routes:', error)
throw error
}
}
// 根据指定的 roleId 获取路由配置
export async function getRoutersByRoleId(roleId: string): Promise<RouteResponse> {
try {
const data = await apiGet<RouteResponse>(`/getRoutersByRoleId?roleId=${roleId}`)
return data
} catch (error) {
console.error('Failed to fetch routes by roleId:', error)
throw error
}
}

11
src/lib/services/skill.ts Normal file
View File

@@ -0,0 +1,11 @@
import { SkillLevelResponse } from '@/lib/types/skill';
import { apiGet } from './api';
export async function getSkillLevels(): Promise<SkillLevelResponse> {
try {
return await apiGet<SkillLevelResponse>('/back/skillLevel');
} catch (error) {
console.error('Failed to fetch skill levels:', error);
throw error;
}
}

156
src/lib/types.ts Normal file
View File

@@ -0,0 +1,156 @@
// 系统类型定义
export type UserRole = "headquarters" | "dealer" | "mall" | "merchant" | "worker"
export type EquipmentStatus = "normal" | "warning" | "expired" | "fault"
export type WorkOrderStatus = "pending" | "assigned" | "in_progress" | "completed" | "cancelled"
export type WorkOrderType = "maintenance" | "repair" | "inspection"
export type Priority = "low" | "normal" | "high" | "urgent"
export interface User {
id: number
username: string
email: string
phone?: string
role: UserRole
status: "active" | "inactive"
created_at: string
updated_at: string
}
export interface Equipment {
id: number
qr_code: string
model: string
serial_number?: string
install_date: string
last_maintenance_date?: string
next_maintenance_date: string
status: EquipmentStatus
location?: string
merchant_id?: number
dealer_id?: number
created_at: string
updated_at: string
}
export interface WorkOrder {
id: number
order_number: string
type: WorkOrderType
title: string
description?: string
priority: Priority
status: WorkOrderStatus
equipment_id?: number
merchant_id?: number
dealer_id?: number
worker_id?: number
created_by: number
assigned_at?: string
started_at?: string
completed_at?: string
created_at: string
updated_at: string
}
export interface Dealer {
id: number
name: string
contact_person?: string
phone?: string
address?: string
region?: string
user_id?: number
created_at: string
}
export interface Mall {
id: number
name: string
address?: string
contact_person?: string
phone?: string
dealer_id?: number
user_id?: number
created_at: string
}
export interface Merchant {
id: number
name: string
shop_number?: string
contact_person?: string
phone?: string
mall_id?: number
user_id?: number
created_at: string
}
export interface Worker {
id: number
name: string
phone?: string
skills?: string[]
region?: string
dealer_id?: number
user_id?: number
status: "available" | "busy" | "offline"
created_at: string
}
export interface InventoryItem {
id: number
dealer_id?: number
item_name: string
item_code?: string
category?: string
current_stock: number
min_stock_level: number
unit?: string
unit_price?: number
created_at: string
updated_at: string
}
export interface Notification {
id: number
user_id: number
title: string
content?: string
type: string
is_read: boolean
created_at: string
}
// API 响应类型
export interface ApiResponse<T> {
success: boolean
data?: T
message?: string
error?: string
}
// 分页类型
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
totalPages: number
}
// 统计数据类型
export interface DashboardStats {
totalEquipment: number
normalEquipment: number
warningEquipment: number
expiredEquipment: number
faultEquipment: number
pendingOrders: number
completedOrders: number
activeWorkers: number
}

58
src/lib/types/dealer.ts Normal file
View File

@@ -0,0 +1,58 @@
export interface Role {
createBy: string | null;
createTime: string | null;
updateBy: string | null;
updateTime: string | null;
remark: string | null;
roleId: string;
roleName: string;
roleKey: string;
roleSort: number;
dataScope: string;
menuCheckStrictly: boolean;
deptCheckStrictly: boolean;
status: string;
delFlag: string | null;
flag: boolean;
menuIds: any;
deptIds: any;
permissions: any;
parentRoleKey: string | null;
admin: boolean;
}
export interface Dealer {
createBy: string;
createTime: string;
updateBy: string | null;
updateTime: string | null;
remark: string | null;
userId: string;
deptId: string | null;
userName: string;
nickName: string;
email: string;
phonenumber: string;
sex: string;
avatar: string;
password: string;
status: string;
delFlag: string;
loginIp: string;
loginDate: string;
pwdUpdateDate: string | null;
dept: any;
roles: Role[];
roleIds: any;
postIds: any;
roleId: string | null;
roleName: string;
provinceCode: string;
admin: boolean;
}
export interface DealerResponse {
msg: string;
code: number;
data: Dealer[];
}

35
src/lib/types/merchant.ts Normal file
View File

@@ -0,0 +1,35 @@
export interface Merchant {
createBy: string;
createTime: string;
updateBy: string;
updateTime: string;
remark: string | null;
userId: string;
deptId: string | null;
userName: string;
nickName: string;
email: string;
phonenumber: string;
sex: string;
avatar: string;
password: string;
status: string;
delFlag: string;
loginIp: string;
loginDate: string;
pwdUpdateDate: string | null;
dept: any;
roles: any[];
roleIds: string | null;
postIds: string | null;
roleId: string | null;
roleName: string | null;
provinceCode: string;
admin: boolean;
}
export interface MerchantResponse {
msg: string;
code: number;
data: Merchant[];
}

14
src/lib/types/region.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface Province {
code: string;
name: string;
parentCode: string | null;
level: number;
roleId: string | null;
children: any;
}
export interface ProvinceResponse {
msg: string;
code: number;
data: Province[];
}

35
src/lib/types/route.ts Normal file
View File

@@ -0,0 +1,35 @@
// 路由元信息接口
export interface RouteMeta {
title: string
icon: string
noCache: boolean
link: string | null
}
// 路由项接口
export interface RouteItem {
name: string
path: string
hidden: boolean
component: string
redirect?: string
alwaysShow?: boolean
meta?: RouteMeta
children?: RouteItem[]
}
// API 响应接口
export interface RouteResponse {
msg: string
code: number
data: RouteItem[]
}
// 侧边栏菜单项接口
export interface SidebarMenuItem {
name: string
path: string
title: string
icon: string
children?: SidebarMenuItem[]
}

13
src/lib/types/skill.ts Normal file
View File

@@ -0,0 +1,13 @@
export type SkillLevel = 'PRIMARY' | 'INTERMEDIATE' | 'ADVANCED';
export interface SkillLevelResponse {
msg: string;
code: number;
data: SkillLevel[];
}
export const SKILL_LEVEL_MAP: Record<SkillLevel, string> = {
PRIMARY: '初级技师',
INTERMEDIATE: '中级技师',
ADVANCED: '高级技师'
};

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

114
src/lib/utils/icon-map.ts Normal file
View File

@@ -0,0 +1,114 @@
import {
Home,
User,
Users,
Settings,
Shield,
Wrench,
Package,
Bell,
Building2,
Store,
ShoppingBag,
Archive,
UserCheck,
BarChart3,
MessageSquare,
Music,
Monitor,
Tool,
FileText,
Star,
Tag,
Image,
Search,
GraduationCap,
Button as ButtonIcon,
Hash,
TreePine,
Edit,
MessageCircle,
Activity,
Server,
Database,
Code,
Globe,
Calendar,
Folder,
FolderOpen,
ChevronRight,
ChevronDown,
} from 'lucide-react'
// 图标映射表
export const iconMap: Record<string, any> = {
// 系统图标
'user': User,
'users': Users,
'peoples': Users,
'people': Users,
'settings': Settings,
'system': Settings,
'shield': Shield,
'wrench': Wrench,
'package': Package,
'bell': Bell,
'home': Home,
// 业务图标
'building': Building2,
'store': Store,
'shopping-bag': ShoppingBag,
'archive': Archive,
'user-check': UserCheck,
'bar-chart': BarChart3,
'message': MessageSquare,
'music': Music,
'monitor': Monitor,
'tool': Tool,
'documentation': FileText,
// 内容管理
'star': Star,
'tab': Tag,
'example': Image,
'input': Search,
'education': GraduationCap,
'button': ButtonIcon,
'enter': Bell,
'build': Tool,
// 系统管理
'tree-table': TreePine,
'tree': TreePine,
'post': FileText,
'dict': FileText,
'edit': Edit,
'log': FileText,
'form': FileText,
'logininfor': Activity,
// 监控相关
'online': Users,
'job': Calendar,
'druid': Database,
'server': Server,
'redis': Database,
'redis-list': Database,
// 工具相关
'code': Code,
'swagger': Globe,
// 通用图标
'#': Hash,
'folder': Folder,
'folder-open': FolderOpen,
'chevron-right': ChevronRight,
'chevron-down': ChevronDown,
}
// 获取图标组件
export function getIcon(iconName: string) {
return iconMap[iconName] || Folder
}

110
src/lib/utils/route.ts Normal file
View File

@@ -0,0 +1,110 @@
import { RouteItem, SidebarMenuItem } from '@/lib/types/route'
// 转换后端路由数据为前端可用格式
export function transformRoutes(routes: RouteItem[]): RouteItem[] {
return routes.map(route => ({
...route,
children: route.children ? transformRoutes(route.children) : undefined
}))
}
// 生成侧边栏菜单数据
export function generateSidebarMenu(routes: RouteItem[]): SidebarMenuItem[] {
const menuItems: SidebarMenuItem[] = []
routes.forEach(route => {
// 跳过隐藏的路由
if (route.hidden) return
// 如果有子路由,处理子路由
if (route.children && route.children.length > 0) {
const visibleChildren = route.children.filter(child => !child.hidden)
if (visibleChildren.length > 0) {
// 如果是单个子路由且父路由没有 alwaysShow直接显示子路由
if (visibleChildren.length === 1 && !route.alwaysShow) {
const child = visibleChildren[0]
menuItems.push({
name: child.name,
path: child.path.startsWith('/') ? child.path : `${route.path}/${child.path}`,
title: child.meta?.title || child.name,
icon: child.meta?.icon || 'folder'
})
} else {
// 多个子路由或强制显示父路由
menuItems.push({
name: route.name,
path: route.path,
title: route.meta?.title || route.name,
icon: route.meta?.icon || 'folder',
children: generateSidebarMenu(visibleChildren)
})
}
}
} else {
// 没有子路由的直接添加
menuItems.push({
name: route.name,
path: route.path,
title: route.meta?.title || route.name,
icon: route.meta?.icon || 'folder'
})
}
})
return menuItems
}
// 根据路径查找路由信息
export function findRouteByPath(routes: RouteItem[], path: string): RouteItem | null {
for (const route of routes) {
if (route.path === path) {
return route
}
if (route.children) {
const found = findRouteByPath(route.children, path)
if (found) return found
}
}
return null
}
// 扁平化路由结构
export function flattenRoutes(routes: RouteItem[]): RouteItem[] {
const flattened: RouteItem[] = []
routes.forEach(route => {
flattened.push(route)
if (route.children) {
flattened.push(...flattenRoutes(route.children))
}
})
return flattened
}
// 获取面包屑路径
export function getBreadcrumbs(routes: RouteItem[], currentPath: string): RouteItem[] {
const breadcrumbs: RouteItem[] = []
function findPath(routes: RouteItem[], path: string, parents: RouteItem[] = []): boolean {
for (const route of routes) {
const currentParents = [...parents, route]
if (route.path === path) {
breadcrumbs.push(...currentParents)
return true
}
if (route.children && findPath(route.children, path, currentParents)) {
return true
}
}
return false
}
findPath(routes, currentPath)
return breadcrumbs
}

68
src/lib/utils/storage.ts Normal file
View File

@@ -0,0 +1,68 @@
// 安全的 localStorage 访问工具,避免 SSR 水合错误
// 安全地获取 localStorage 中的值
export function getStorageItem(key: string): string | null {
try {
return localStorage.getItem(key)
} catch (error) {
// 在服务端环境下返回 null
return null
}
}
// 安全地设置 localStorage 中的值
export function setStorageItem(key: string, value: string): void {
try {
localStorage.setItem(key, value)
} catch (error) {
// 在服务端环境下忽略错误
console.warn('无法访问 localStorage:', error)
}
}
// 安全地移除 localStorage 中的值
export function removeStorageItem(key: string): void {
try {
localStorage.removeItem(key)
} catch (error) {
// 在服务端环境下忽略错误
console.warn('无法访问 localStorage:', error)
}
}
// 获取用户信息
export function getUserData(): any | null {
try {
const user = getStorageItem('user')
if (user) {
return JSON.parse(user)
}
} catch (error) {
console.error('解析用户数据失败:', error)
}
return null
}
// 获取用户 token
export function getUserToken(): string | null {
const userData = getUserData()
return userData?.token || null
}
// 获取用户角色 ID
export function getUserRoleId(): string | null {
const userData = getUserData()
return userData?.role || null
}
// 检查是否在客户端环境
export function isClient(): boolean {
return typeof window !== 'undefined'
}
// 安全地执行客户端操作
export function runOnClient(callback: () => void): void {
if (isClient()) {
callback()
}
}