From 9b5789fb754413ce85767ce9e0022f0a06fec6fa Mon Sep 17 00:00:00 2001 From: 12451 <12451@example.com> Date: Thu, 2 Jul 2026 18:23:13 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20H5=E5=92=8CWEB=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E5=88=86=E7=A6=BB=EF=BC=8CH5=E7=8B=AC=E7=AB=8B=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=8C=E6=89=AB=E7=A0=81=E8=B7=AF=E5=BE=84=E6=94=B9?= =?UTF-8?q?=E4=B8=BA/h5/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- deploy/nginx/app.anzhizhichong.com.conf | 11 + software/h5/index.html | 13 + software/h5/package.json | 27 ++ software/h5/src/App.tsx | 71 +++++ software/h5/src/api/devices.ts | 90 +++++++ software/h5/src/api/permission.ts | 105 ++++++++ software/h5/src/api/request.ts | 72 +++++ software/h5/src/api/user.ts | 121 +++++++++ software/h5/src/h5/api.ts | 154 +++++++++++ software/h5/src/h5/auth.ts | 56 ++++ software/h5/src/h5/components.tsx | 249 +++++++++++++++++ software/h5/src/h5/layout.tsx | 36 +++ .../h5/src/h5/pages/compartment-detail.tsx | 253 ++++++++++++++++++ software/h5/src/h5/pages/device-detail.tsx | 84 ++++++ software/h5/src/h5/pages/devices.tsx | 129 +++++++++ software/h5/src/h5/pages/home.tsx | 102 +++++++ software/h5/src/h5/pages/login.tsx | 101 +++++++ software/h5/src/h5/pages/me.tsx | 63 +++++ software/h5/src/h5/routes.tsx | 82 ++++++ software/h5/src/index.css | 1 + software/h5/src/main.tsx | 10 + software/h5/tsconfig.app.json | 31 +++ software/h5/tsconfig.json | 7 + software/h5/tsconfig.node.json | 23 ++ software/h5/vite.config.ts | 23 ++ software/web/src/App.tsx | 11 +- 27 files changed, 1917 insertions(+), 10 deletions(-) create mode 100644 software/h5/index.html create mode 100644 software/h5/package.json create mode 100644 software/h5/src/App.tsx create mode 100644 software/h5/src/api/devices.ts create mode 100644 software/h5/src/api/permission.ts create mode 100644 software/h5/src/api/request.ts create mode 100644 software/h5/src/api/user.ts create mode 100644 software/h5/src/h5/api.ts create mode 100644 software/h5/src/h5/auth.ts create mode 100644 software/h5/src/h5/components.tsx create mode 100644 software/h5/src/h5/layout.tsx create mode 100644 software/h5/src/h5/pages/compartment-detail.tsx create mode 100644 software/h5/src/h5/pages/device-detail.tsx create mode 100644 software/h5/src/h5/pages/devices.tsx create mode 100644 software/h5/src/h5/pages/home.tsx create mode 100644 software/h5/src/h5/pages/login.tsx create mode 100644 software/h5/src/h5/pages/me.tsx create mode 100644 software/h5/src/h5/routes.tsx create mode 100644 software/h5/src/index.css create mode 100644 software/h5/src/main.tsx create mode 100644 software/h5/tsconfig.app.json create mode 100644 software/h5/tsconfig.json create mode 100644 software/h5/tsconfig.node.json create mode 100644 software/h5/vite.config.ts diff --git a/AGENTS.md b/AGENTS.md index f1468a0..014ef11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ - **平台抽象ID**:`10-00000000`(绑定柜控板,二维码对应此ID) - **设备ID**:4G SIM卡15位IMEI(10进制) - **仓体全局ID**:`平台层ID-仓板ID-仓体ID` -- **扫码链接**:`https://app.anzhizhichong.com/pms/{抽象ID}` +- **扫码链接**:`https://app.anzhizhichong.com/h5/{抽象ID}` ## 技术栈 diff --git a/deploy/nginx/app.anzhizhichong.com.conf b/deploy/nginx/app.anzhizhichong.com.conf index 953333c..f2c7126 100644 --- a/deploy/nginx/app.anzhizhichong.com.conf +++ b/deploy/nginx/app.anzhizhichong.com.conf @@ -44,4 +44,15 @@ server { alias /data/pms/web/; try_files $uri $uri/ /pms/index.html; } + + # H5用户端 - 不带斜杠时重定向 + location = /h5 { + return 301 /h5/; + } + + # H5用户端 + location /h5/ { + alias /data/pms/h5/; + try_files $uri $uri/ /h5/index.html; + } } diff --git a/software/h5/index.html b/software/h5/index.html new file mode 100644 index 0000000..390911e --- /dev/null +++ b/software/h5/index.html @@ -0,0 +1,13 @@ + + + + + + + 安知充 + + +
+ + + diff --git a/software/h5/package.json b/software/h5/package.json new file mode 100644 index 0000000..c932d74 --- /dev/null +++ b/software/h5/package.json @@ -0,0 +1,27 @@ +{ + "name": "pms-h5", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.7", + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.1", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.2", + "@types/node": "^24.13.2", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.3", + "tailwindcss": "^4.3.2", + "typescript": "~6.0.2", + "vite": "^8.1.1" + } +} diff --git a/software/h5/src/App.tsx b/software/h5/src/App.tsx new file mode 100644 index 0000000..8f18913 --- /dev/null +++ b/software/h5/src/App.tsx @@ -0,0 +1,71 @@ +/** + * H5 用户端根组件 + */ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { useH5AuthStore } from './auth'; +import H5Layout from './layout'; +import H5Login from './pages/login'; +import H5Home from './pages/home'; +import H5Devices from './pages/devices'; +import H5DeviceDetail from './pages/device-detail'; +import H5CompartmentDetail from './pages/compartment-detail'; +import H5Me from './pages/me'; +import { Loading } from './components'; +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { h5Api } from './api'; + +/** 扫码入口处理 */ +function ScanRedirect() { + const { abstractId } = useParams<{ abstractId: string }>(); + const navigate = useNavigate(); + const [error, setError] = useState(''); + + useEffect(() => { + if (!abstractId || ['login', 'devices', 'me', 'compartments'].includes(abstractId)) { + return; + } + h5Api.getCabinetByAbstractId(abstractId) + .then((res) => navigate(`/devices/${res.data.id}`, { replace: true })) + .catch(() => setError('未找到该设备')); + }, [abstractId, navigate]); + + if (error) { + return ( +
+

{error}

+
+ ); + } + + return ( +
+ +
+ ); +} + +export default function App() { + const restore = useH5AuthStore((s) => s.restore); + + useEffect(() => { + restore(); + }, [restore]); + + return ( + + + } /> + }> + } /> + } /> + } /> + } /> + + } /> + } /> + } /> + + + ); +} diff --git a/software/h5/src/api/devices.ts b/software/h5/src/api/devices.ts new file mode 100644 index 0000000..4d68666 --- /dev/null +++ b/software/h5/src/api/devices.ts @@ -0,0 +1,90 @@ +/** + * 设备管理 API + * 组织/项目/柜子 CRUD + 树状结构 + */ +import { api } from './request'; + +export interface Organization { + id: number; + name: string; +} + +export interface Project { + id: number; + organization_id?: number; + name: string; +} + +export interface Cabinet { + id: number; + project_id?: number; + abstract_id: string; + imei: string; + auth_str?: string; + name?: string; + status: number; +} + +export interface Compartment { + id: number; + cabin_board_id: number; + channel_id: number; + status: number; +} + +export interface CabinBoard { + id: number; + board_id: number; + status: number; + compartments: Compartment[]; +} + +export interface CabinetDetail extends Cabinet { + cabin_boards: CabinBoard[]; +} + +export interface TreeNode { + id: number; + name: string; + children?: TreeNode[]; + abstract_id?: string; + imei?: string; + status?: number; +} + +// 组织 API +export const orgApi = { + list: () => api.get('/organizations'), + create: (data: { name: string }) => api.post('/organizations', data), + update: (id: number, data: { name: string }) => api.put(`/organizations/${id}`, data), + delete: (id: number) => api.delete(`/organizations/${id}`), +}; + +// 项目 API +export const projectApi = { + list: (orgId: number) => api.get(`/organizations/${orgId}/projects`), + create: (orgId: number, data: { name: string }) => + api.post(`/organizations/${orgId}/projects`, data), + update: (id: number, data: { name: string }) => api.put(`/projects/${id}`, data), + delete: (id: number) => api.delete(`/projects/${id}`), +}; + +// 柜子 API +export const cabinetApi = { + list: (projectId: number) => api.get(`/projects/${projectId}/cabinets`), + create: (data: { imeis: string[]; project_id: number }) => + api.post<{ ids: number[] }>('/cabinets', data), + update: (id: number, data: { project_id?: number; name?: string }) => + api.put(`/cabinets/${id}`, data), + delete: (id: number) => api.delete(`/cabinets/${id}`), + regenerateAuth: (id: number) => api.post<{ auth_str: string }>(`/cabinets/${id}/regenerate-auth`), + getDetail: (id: number) => api.get(`/cabinets/${id}`), + // 接触器控制 + powOn: (id: number) => api.post(`/cabinets/${id}/pow-on`), + powOff: (id: number) => api.post(`/cabinets/${id}/pow-off`), +}; + +// 组织树 API +export const treeApi = { + get: () => api.get('/organization-tree'), +}; diff --git a/software/h5/src/api/permission.ts b/software/h5/src/api/permission.ts new file mode 100644 index 0000000..880dd6d --- /dev/null +++ b/software/h5/src/api/permission.ts @@ -0,0 +1,105 @@ +/** + * 权限管理 API + * + * 角色管理、权限码查询、用户权限配置等接口封装 + */ + +import { api } from '@/api/request'; + +/** 角色信息 */ +export interface Role { + id: number; + name: string; + description?: string; + permission_codes: string[]; +} + +/** 权限码信息 */ +export interface PermissionItem { + id: number; + code: string; + name: string; +} + +/** 用户权限范围条目 */ +export interface ScopeEntry { + id?: number; + /** 1组织 2项目 3设备 */ + scope_type: number; + /** 对应的组织/项目/设备ID */ + scope_id: number; + /** 1查看 2操作 */ + permission_type: number; +} + +/** 用户权限详情 */ +export interface UserPermissionDetail { + user_id: number; + name: string; + phone: string; + role_level: number; + organization_id: number | null; + roles: Role[]; + permission_codes: string[]; + scopes: ScopeEntry[]; +} + +// ── 角色管理 ── + +/** 获取所有角色列表 */ +export async function fetchRoles(): Promise { + const res = await api.get('/roles'); + return res.data ?? []; +} + +/** 创建角色 */ +export async function createRole(name: string, description?: string) { + return api.post('/roles', { name, description }); +} + +/** 更新角色 */ +export async function updateRole(id: number, name: string, description?: string) { + return api.put(`/roles/${id}`, { name, description }); +} + +/** 删除角色 */ +export async function deleteRole(id: number) { + return api.delete(`/roles/${id}`); +} + +/** 设置角色权限(全量替换) */ +export async function setRolePermissions(roleId: number, permissionCodes: string[]) { + return api.put(`/roles/${roleId}/permissions`, { permission_codes: permissionCodes }); +} + +// ── 权限码 ── + +/** 获取所有权限码列表 */ +export async function fetchPermissions(): Promise { + const res = await api.get('/permissions'); + return res.data ?? []; +} + +// ── 用户权限 ── + +/** 获取用户权限详情 */ +export async function fetchUserPermissions(userId: number): Promise { + const res = await api.get(`/users/${userId}/permissions`); + return res.data; +} + +/** 设置用户角色(全量替换) */ +export async function setUserRoles(userId: number, roleIds: number[]) { + return api.put(`/users/${userId}/roles`, { role_ids: roleIds }); +} + +/** 设置用户权限范围(全量替换) */ +export async function setUserScopes(userId: number, scopes: ScopeEntry[]) { + return api.put(`/users/${userId}/scopes`, { scopes }); +} + +/** 获取用户权限范围 */ +export async function fetchUserScopes(userId: number): Promise { + const res = await api.get(`/users/${userId}/scopes`); + return res.data ?? []; +} diff --git a/software/h5/src/api/request.ts b/software/h5/src/api/request.ts new file mode 100644 index 0000000..2d9af9b --- /dev/null +++ b/software/h5/src/api/request.ts @@ -0,0 +1,72 @@ +/** + * HTTP 请求封装 + * - 自动注入 Authorization token + * - 统一异常处理 + * - 类型友好的响应 + */ + +const API_BASE = import.meta.env.VITE_API_BASE || '/pms/api'; + +export interface ApiResponse { + code: number; + data: T; + message: string; +} + +export class ApiError extends Error { + public code: number; + + constructor( + code: number, + message: string, + ) { + super(message); + this.name = 'ApiError'; + this.code = code; + } +} + +function getToken(): string | null { + return localStorage.getItem('token'); +} + +async function request( + url: string, + options: RequestInit = {}, +): Promise> { + const token = getToken(); + const headers: Record = { + 'Content-Type': 'application/json', + ...(options.headers as Record), + }; + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const res = await fetch(`${API_BASE}${url}`, { + ...options, + headers, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new ApiError(res.status, body.message || res.statusText); + } + + return res.json(); + } catch (err) { + if (err instanceof ApiError) throw err; + throw new ApiError(0, (err as Error).message || '网络错误'); + } +} + +export const api = { + get: (url: string) => request(url), + post: (url: string, data?: unknown) => + request(url, { method: 'POST', body: JSON.stringify(data) }), + put: (url: string, data?: unknown) => + request(url, { method: 'PUT', body: JSON.stringify(data) }), + delete: (url: string) => request(url, { method: 'DELETE' }), +}; diff --git a/software/h5/src/api/user.ts b/software/h5/src/api/user.ts new file mode 100644 index 0000000..3008b3f --- /dev/null +++ b/software/h5/src/api/user.ts @@ -0,0 +1,121 @@ +/** + * 用户管理和操作日志 API + */ + +import { api } from '@/api/request'; + +// ── 用户管理 ── + +/** 用户信息 */ +export interface User { + id: number; + phone: string; + name: string | null; + role: number; + role_text: string; + organization_id: number | null; + organization_name: string | null; + status: number; + created_at: string; +} + +/** 用户列表响应 */ +export interface UserListData { + list: User[]; + total: number; +} + +/** 创建用户参数 */ +export interface CreateUserParams { + phone: string; + name?: string; + role?: number; + organization_id?: number | null; + password?: string; +} + +/** 编辑用户参数 */ +export interface UpdateUserParams { + name?: string; + role?: number; + organization_id?: number | null; +} + +/** 用户列表(分页) */ +export async function fetchUsers(params: { + page?: number; + page_size?: number; + keyword?: string; +}): Promise { + const searchParams = new URLSearchParams(); + if (params.page) searchParams.set('page', String(params.page)); + if (params.page_size) searchParams.set('page_size', String(params.page_size)); + if (params.keyword) searchParams.set('keyword', params.keyword); + const res = await api.get(`/users?${searchParams.toString()}`); + return res.data; +} + +/** 创建用户 */ +export async function createUser(data: CreateUserParams) { + return api.post('/users', data); +} + +/** 编辑用户 */ +export async function updateUser(id: number, data: UpdateUserParams) { + return api.put(`/users/${id}`, data); +} + +/** 重置密码 */ +export async function resetPassword(id: number, password?: string) { + return api.put(`/users/${id}/reset-password`, { password }); +} + +/** 启用/禁用用户 */ +export async function setUserStatus(id: number, status: number) { + return api.put(`/users/${id}/status`, { status }); +} + +/** 删除用户 */ +export async function deleteUser(id: number) { + return api.delete(`/users/${id}`); +} + +// ── 操作日志 ── + +/** 操作日志项 */ +export interface OperationLog { + id: number; + user_id: number; + user_name: string; + action: string; + target_type: string | null; + target_id: number | null; + detail: string | null; + created_at: string; +} + +/** 操作日志列表响应 */ +export interface OperationLogListData { + list: OperationLog[]; + total: number; +} + +/** 操作日志列表(分页+过滤) */ +export async function fetchOperationLogs(params: { + user_id?: number; + action?: string; + start_time?: string; + end_time?: string; + page?: number; + page_size?: number; +}): Promise { + const searchParams = new URLSearchParams(); + if (params.user_id) searchParams.set('user_id', String(params.user_id)); + if (params.action) searchParams.set('action', params.action); + if (params.start_time) searchParams.set('start_time', params.start_time); + if (params.end_time) searchParams.set('end_time', params.end_time); + if (params.page) searchParams.set('page', String(params.page)); + if (params.page_size) searchParams.set('page_size', String(params.page_size)); + const res = await api.get(`/operation-logs?${searchParams.toString()}`); + return res.data; +} diff --git a/software/h5/src/h5/api.ts b/software/h5/src/h5/api.ts new file mode 100644 index 0000000..8285b4d --- /dev/null +++ b/software/h5/src/h5/api.ts @@ -0,0 +1,154 @@ +/** + * H5 用户端 API 服务 + * 封装所有 H5 端接口调用 + */ +import { api } from '@/api/request'; + +// ─── 数据类型 ─── + +export interface DashboardData { + online_cabinets: number; + charging_count: number; + idle_channels: number; + fault_channels: number; + today_charge_count: number; + today_charge_hours: number; + today_energy_kwh: number; + alerts: AlertItem[]; + projects: ProjectSummary[]; +} + +export interface AlertItem { + project: string; + cabinet: string; + channel: number; + alert_type: string; +} + +export interface ProjectSummary { + id: number; + name: string; + cabinet_count: number; + charging: number; + idle: number; + fault: number; +} + +export interface H5Project { + id: number; + name: string; +} + +export interface H5Cabinet { + id: number; + abstract_id: string; + name?: string; + status: number; // 0=离线 1=在线 + project_id: number; +} + +export interface CompartmentStatus { + id: number; + channel_id: number; + status: number; // 0=空闲 1=充电中 2=故障 3=离线 + voltage?: number; + current?: number; + power?: number; + energy?: number; + soc?: number; // 荷电状态 % + soh?: number; // 健康度 % + cell_voltages?: number[]; + cell_temps?: number[]; + bms_alerts?: string[]; +} + +export interface CabinBoardStatus { + id: number; + board_id: number; + status: number; + compartments: CompartmentStatus[]; +} + +export interface CabinetDetail { + id: number; + abstract_id: string; + imei: string; + name?: string; + status: number; + cabin_boards: CabinBoardStatus[]; +} + +export interface CompartmentDetail { + id: number; + channel_id: number; + status: number; + voltage: number; + current: number; + power: number; + energy: number; + soc: number; + soh: number; + cell_voltages: number[]; + cell_temps: number[]; + bms_alerts: string[]; + charge_records: ChargeRecord[]; +} + +export interface ChargeRecord { + id: number; + start_time: string; + end_time?: string; + energy_kwh: number; + cost: number; +} + +export interface H5User { + id: number; + phone: string; + name: string; +} + +export interface H5LoginResponse { + token: string; + user: H5User; +} + +// ─── API 函数 ─── + +export const h5Api = { + // 认证 + login: (data: { phone: string; password: string }) => + api.post('/h5/auth/login', data), + + // 首页仪表盘 + getDashboard: () => api.get('/h5/dashboard'), + + // 项目列表 + getProjects: () => api.get('/h5/projects'), + + // 设备列表(按项目) + getCabinets: (projectId?: number) => + api.get(`/h5/cabinets${projectId ? `?project_id=${projectId}` : ''}`), + + // 设备详情(含仓板+仓体状态) + getCabinetDetail: (id: number) => + api.get(`/h5/cabinets/${id}`), + + // 通过 abstract_id 获取设备详情(扫码) + getCabinetByAbstractId: (abstractId: string) => + api.get(`/h5/cabinets/abstract/${abstractId}`), + + // 仓体/通道详情 + getCompartmentDetail: (id: number) => + api.get(`/h5/compartments/${id}`), + + // 充电控制 + startCharge: (compartmentId: number) => + api.post<{ success: boolean }>('/h5/charge/start', { compartment_id: compartmentId }), + stopCharge: (compartmentId: number) => + api.post<{ success: boolean }>('/h5/charge/stop', { compartment_id: compartmentId }), + + // 开门 + openDoor: (compartmentId: number) => + api.post<{ success: boolean }>('/h5/door/open', { compartment_id: compartmentId }), +}; diff --git a/software/h5/src/h5/auth.ts b/software/h5/src/h5/auth.ts new file mode 100644 index 0000000..3ec76c8 --- /dev/null +++ b/software/h5/src/h5/auth.ts @@ -0,0 +1,56 @@ +/** + * H5 用户端认证状态管理 + * - 与后台管理独立,使用不同 token + * - localStorage key 使用 h5_ 前缀避免冲突 + */ +import { create } from 'zustand'; +import { h5Api, type H5User } from './api'; + +const TOKEN_KEY = 'h5_token'; +const USER_KEY = 'h5_user'; + +interface AuthState { + token: string | null; + user: H5User | null; + loading: boolean; + isAuthenticated: boolean; + login: (phone: string, password: string) => Promise; + logout: () => void; + restore: () => void; +} + +export const useH5AuthStore = create((set) => ({ + token: null, + user: null, + loading: false, + isAuthenticated: false, + + login: async (phone: string, password: string) => { + set({ loading: true }); + try { + const res = await h5Api.login({ phone, password }); + const { token, user } = res.data; + localStorage.setItem(TOKEN_KEY, token); + localStorage.setItem(USER_KEY, JSON.stringify(user)); + set({ token, user, isAuthenticated: true, loading: false }); + } catch { + set({ loading: false }); + throw new Error('登录失败,请检查手机号和密码'); + } + }, + + logout: () => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(USER_KEY); + set({ token: null, user: null, isAuthenticated: false }); + }, + + restore: () => { + const token = localStorage.getItem(TOKEN_KEY); + const raw = localStorage.getItem(USER_KEY); + if (token) { + const user = raw ? JSON.parse(raw) as H5User : null; + set({ token, user, isAuthenticated: true }); + } + }, +})); diff --git a/software/h5/src/h5/components.tsx b/software/h5/src/h5/components.tsx new file mode 100644 index 0000000..9cc0a6d --- /dev/null +++ b/software/h5/src/h5/components.tsx @@ -0,0 +1,249 @@ +/** + * H5 共享组件:底部导航、统计卡片、通道卡片、加载/错误状态 + */ +import { type ReactNode } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; + +// ─── 底部导航 ─── + +const tabs = [ + { key: '/pms', label: '首页', icon: HomeIcon }, + { key: '/pms/devices', label: '设备', icon: DeviceIcon }, + { key: '/pms/me', label: '我的', icon: MeIcon }, +]; + +export function BottomNav() { + const navigate = useNavigate(); + const location = useLocation(); + + const activeKey = '/' + location.pathname.split('/').slice(0, 3).join('/'); + + return ( + + ); +} + +function HomeIcon({ active }: { active: boolean }) { + return ( + + + + ); +} + +function DeviceIcon({ active }: { active: boolean }) { + return ( + + + + ); +} + +function MeIcon({ active }: { active: boolean }) { + return ( + + + + ); +} + +// ─── 统计卡片 ─── + +interface StatCardProps { + label: string; + value: string | number; + sub?: string; + color?: string; +} + +export function StatCard({ label, value, sub, color = 'blue' }: StatCardProps) { + const colors: Record = { + blue: 'bg-blue-50 text-blue-700', + green: 'bg-green-50 text-green-700', + orange: 'bg-orange-50 text-orange-700', + red: 'bg-red-50 text-red-700', + gray: 'bg-gray-50 text-gray-700', + }; + + return ( +
+ {value} + {label} + {sub && {sub}} +
+ ); +} + +// ─── 告警项 ─── + +interface AlertCardProps { + project: string; + cabinet: string; + channel: number; + alert_type: string; +} + +export function AlertCard({ project, cabinet, channel, alert_type }: AlertCardProps) { + return ( +
+ ⚠️ +
+ {project} + - {cabinet} 通道{channel} +

{alert_type}

+
+
+ ); +} + +// ─── 项目卡片 ─── + +interface ProjectCardProps { + name: string; + cabinet_count: number; + charging: number; + idle: number; + fault: number; + onClick: () => void; +} + +export function ProjectCard({ name, cabinet_count, charging, idle, fault, onClick }: ProjectCardProps) { + return ( + + ); +} + +// ─── 通道卡片 ─── + +interface ChannelCardProps { + channelId: number; + status: number; // 0=空闲 1=充电中 2=故障 3=离线 + voltage?: number; + current?: number; + onClick: () => void; +} + +const statusConfig: Record = { + 0: { label: '空闲', bg: 'bg-gray-50', dot: 'bg-gray-400' }, + 1: { label: '充电中', bg: 'bg-green-50', dot: 'bg-green-500' }, + 2: { label: '故障', bg: 'bg-red-50', dot: 'bg-red-500' }, + 3: { label: '离线', bg: 'bg-gray-100', dot: 'bg-gray-400' }, +}; + +export function ChannelCard({ channelId, status, voltage, current, onClick }: ChannelCardProps) { + const cfg = statusConfig[status] || statusConfig[3]; + + return ( + + ); +} + +// ─── 加载状态 ─── + +export function Loading() { + return ( +
+
+ 加载中... +
+ ); +} + +// ─── 错误状态 ─── + +export function ErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) { + return ( +
+ 😵 +

{message}

+ {onRetry && ( + + )} +
+ ); +} + +// ─── 页面容器 ─── + +export function PageContainer({ children, title, onBack }: { children: ReactNode; title?: string; onBack?: () => void }) { + return ( +
+ {title && ( +
+ {onBack && ( + + )} +

{title}

+
+ )} +
{children}
+
+ ); +} + +// ─── 确认弹窗 ─── + +export function ConfirmDialog({ open, title, message, onConfirm, onCancel }: { + open: boolean; + title: string; + message: string; + onConfirm: () => void; + onCancel: () => void; +}) { + if (!open) return null; + return ( +
+
e.stopPropagation()}> +

{title}

+

{message}

+
+ + +
+
+
+ ); +} diff --git a/software/h5/src/h5/layout.tsx b/software/h5/src/h5/layout.tsx new file mode 100644 index 0000000..b897205 --- /dev/null +++ b/software/h5/src/h5/layout.tsx @@ -0,0 +1,36 @@ +/** + * H5 移动端布局 + * - 底部 Tab 导航(首页/设备/我的) + * - 使用 Outlet 渲染子页面 + */ +import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { useH5AuthStore } from './auth'; +import { BottomNav } from './components'; + +export default function H5Layout() { + const { isAuthenticated } = useH5AuthStore(); + const location = useLocation(); + + // 未认证时重定向到登录页,保留目标地址 + if (!isAuthenticated) { + return ; + } + + return ( +
+
+ +
+ +
+ ); +} + +/** 不带底部导航的 H5 布局(登录页、通道详情等全屏页面) */ +export function H5PlainLayout({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ); +} diff --git a/software/h5/src/h5/pages/compartment-detail.tsx b/software/h5/src/h5/pages/compartment-detail.tsx new file mode 100644 index 0000000..f3a79e5 --- /dev/null +++ b/software/h5/src/h5/pages/compartment-detail.tsx @@ -0,0 +1,253 @@ +/** + * H5 通道详情 + * - 实时状态(SOC/SOH) + * - 电气参数(电压/电流/功率/能耗) + * - BMS数据(单体电压/温度/告警) + * - 充电曲线(示意) + * - 操作按钮(停止充电、开门) + */ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { h5Api, type CompartmentDetail } from '../api'; +import { Loading, ErrorState, PageContainer, ConfirmDialog } from '../components'; + +export default function H5CompartmentDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [actionLoading, setActionLoading] = useState(''); + const [confirmAction, setConfirmAction] = useState<'stop' | 'door' | null>(null); + + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + setError(''); + try { + const res = await h5Api.getCompartmentDetail(Number(id)); + setData(res.data); + } catch { + setError('加载通道详情失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchDetail(); + // ponytail: 15s auto-refresh during charging, add WebSocket for real-time + const timer = setInterval(fetchDetail, 15000); + return () => clearInterval(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleAction = async (action: 'start' | 'stop' | 'door') => { + if (!id || actionLoading) return; + setActionLoading(action); + try { + if (action === 'start') { + await h5Api.startCharge(Number(id)); + } else if (action === 'stop') { + await h5Api.stopCharge(Number(id)); + } else if (action === 'door') { + await h5Api.openDoor(Number(id)); + } + setConfirmAction(null); + fetchDetail(); + } catch { + setError('操作失败,请重试'); + } finally { + setActionLoading(''); + } + }; + + if (loading) return ; + if (error && !data) return ; + if (!data) return null; + + const statusLabel: Record = { 0: '空闲', 1: '充电中', 2: '故障', 3: '离线' }; + const isCharging = data.status === 1; + const isIdle = data.status === 0; + + return ( + navigate(-1)}> + {/* 实时状态 */} +
+
+

实时状态

+ + {statusLabel[data.status] || '未知'} + +
+
+
+

{data.soc ?? '-'}%

+

SOC

+
+
+

{data.soh ?? '-'}%

+

SOH

+
+
+

{data.energy?.toFixed(1) ?? '-'}

+

能耗(kWh)

+
+
+
+ + {/* 电气参数 */} +
+

电气参数

+
+
+ 电压 + {data.voltage?.toFixed(1) ?? '-'} V +
+
+ 电流 + {data.current?.toFixed(1) ?? '-'} A +
+
+ 功率 + {data.power?.toFixed(1) ?? '-'} W +
+
+ 能耗 + {data.energy?.toFixed(3) ?? '-'} kWh +
+
+
+ + {/* BMS 数据 */} + {data.cell_voltages && data.cell_voltages.length > 0 && ( +
+

BMS 数据

+ {/* 单体电压 */} +
+

单体电压 (V)

+
+ {data.cell_voltages.map((v, i) => ( + + #{i + 1}: {v.toFixed(3)} + + ))} +
+
+ {/* 单体温度 */} + {data.cell_temps && data.cell_temps.length > 0 && ( +
+

温度 (°C)

+
+ {data.cell_temps.map((t, i) => ( + 50 ? 'bg-red-50 text-red-700' : 'bg-orange-50 text-orange-700'}`}> + #{i + 1}: {t.toFixed(1)} + + ))} +
+
+ )} + {/* BMS 告警 */} + {data.bms_alerts && data.bms_alerts.length > 0 && ( +
+

告警

+
    + {data.bms_alerts.map((alert, i) => ( +
  • {alert}
  • + ))} +
+
+ )} +
+ )} + + {/* 充电曲线示意 */} + {isCharging && ( +
+

充电曲线

+ {/* TODO: 待后端提供充电曲线API后替换为真实数据 */} +
+ {[30, 45, 55, 50, 65, 70, 75, 72, 80, 85, 82, 88].map((h, i) => ( +
+ ))} +
+
+ 12:00 + 当前 +
+ {/* ponytail: static mock chart, replace with real chart lib when backend provides data */} +
+ )} + + {/* 充电记录 */} + {data.charge_records && data.charge_records.length > 0 && ( +
+

充电记录

+
+ {data.charge_records.slice(0, 5).map((record) => ( +
+
+

{record.start_time.slice(0, 16)}

+ {record.end_time &&

{record.end_time.slice(0, 16)}

} +
+
+

{record.energy_kwh.toFixed(2)} kWh

+

¥{record.cost.toFixed(2)}

+
+
+ ))} +
+
+ )} + + {/* 操作按钮 */} +
+ {isIdle && ( + + )} + {isCharging && ( + + )} + +
+ + {/* 确认弹窗 */} + handleAction('stop')} + onCancel={() => setConfirmAction(null)} + /> + handleAction('door')} + onCancel={() => setConfirmAction(null)} + /> +
+ ); +} diff --git a/software/h5/src/h5/pages/device-detail.tsx b/software/h5/src/h5/pages/device-detail.tsx new file mode 100644 index 0000000..d617b47 --- /dev/null +++ b/software/h5/src/h5/pages/device-detail.tsx @@ -0,0 +1,84 @@ +/** + * H5 设备详情(柜子视图) + * - 柜子基本信息 + * - 仓板列表,每板6个通道 + * - 点击通道进入通道详情 + */ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { h5Api, type CabinetDetail } from '../api'; +import { ChannelCard, Loading, ErrorState, PageContainer } from '../components'; + +export default function H5DeviceDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const fetchDetail = async () => { + if (!id) return; + setLoading(true); + setError(''); + try { + const res = await h5Api.getCabinetDetail(Number(id)); + setData(res.data); + } catch { + setError('加载设备详情失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchDetail(); + // ponytail: no periodic refresh, add if real-time needed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + if (loading) return ; + if (error) return ; + if (!data) return null; + + return ( + navigate('/pms/devices')}> + {/* 设备基本信息 */} +
+
+
+

{data.name || data.abstract_id}

+

ID: {data.abstract_id}

+ {data.imei &&

IMEI: {data.imei}

} +
+ + {data.status === 1 ? '在线' : '离线'} + +
+
+ + {/* 仓板列表 */} + {data.cabin_boards.map((board) => ( +
+

+ 仓板 {board.board_id} + + ({board.status === 1 ? '在线' : '离线'}) + +

+
+ {board.compartments.map((comp) => ( + navigate(`/pms/compartments/${comp.id}`)} + /> + ))} +
+
+ ))} +
+ ); +} diff --git a/software/h5/src/h5/pages/devices.tsx b/software/h5/src/h5/pages/devices.tsx new file mode 100644 index 0000000..0648092 --- /dev/null +++ b/software/h5/src/h5/pages/devices.tsx @@ -0,0 +1,129 @@ +/** + * H5 设备页 + * - 项目列表 → 选择项目后显示设备列表 + * - 点击设备进入设备详情 + */ +import { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { h5Api, type H5Project, type H5Cabinet } from '../api'; +import { Loading, ErrorState, PageContainer } from '../components'; + +type ViewMode = 'projects' | 'cabinets'; + +export default function H5Devices() { + const [searchParams] = useSearchParams(); + const projectId = searchParams.get('project_id'); + const navigate = useNavigate(); + + const [mode, setMode] = useState(projectId ? 'cabinets' : 'projects'); + const [projects, setProjects] = useState([]); + const [cabinets, setCabinets] = useState([]); + const [selectedProject, setSelectedProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const fetchProjects = async () => { + setLoading(true); + setError(''); + try { + const res = await h5Api.getProjects(); + setProjects(res.data); + } catch { + setError('加载项目列表失败'); + } finally { + setLoading(false); + } + }; + + const fetchCabinets = async (pid: number) => { + setLoading(true); + setError(''); + try { + const res = await h5Api.getCabinets(pid); + setCabinets(res.data); + } catch { + setError('加载设备列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (projectId) { + const pid = Number(projectId); + setMode('cabinets'); + setSelectedProject(projects.find((p) => p.id === pid) || null); + fetchCabinets(pid); + } else { + setMode('projects'); + fetchProjects(); + } + // ponytail: projects refetched on mount, ok for current scope + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [projectId]); + + const handleProjectClick = (project: H5Project) => { + setSelectedProject(project); + setMode('cabinets'); + navigate(`/pms/devices?project_id=${project.id}`, { replace: true }); + fetchCabinets(project.id); + }; + + const handleBack = () => { + setMode('projects'); + setSelectedProject(null); + setCabinets([]); + navigate('/pms/devices', { replace: true }); + }; + + if (loading) return ; + if (error) return selectedProject && fetchCabinets(selectedProject.id)} />; + + return ( + + {mode === 'projects' ? ( +
+ {projects.length === 0 &&

暂无项目

} + {projects.map((project) => ( + + ))} +
+ ) : ( +
+ {cabinets.length === 0 &&

暂无设备

} + {cabinets.map((cabinet) => ( + + ))} +
+ )} +
+ ); +} diff --git a/software/h5/src/h5/pages/home.tsx b/software/h5/src/h5/pages/home.tsx new file mode 100644 index 0000000..f4e0f6f --- /dev/null +++ b/software/h5/src/h5/pages/home.tsx @@ -0,0 +1,102 @@ +/** + * H5 首页 Dashboard + * - 统计卡片(在线/充电中/空闲/故障) + * - 今日统计 + * - 告警通知 + * - 项目列表 + */ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { h5Api, type DashboardData } from '../api'; +import { StatCard, AlertCard, ProjectCard, Loading, ErrorState } from '../components'; + +export default function H5Home() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const navigate = useNavigate(); + + const fetchData = async () => { + setLoading(true); + setError(''); + try { + const res = await h5Api.getDashboard(); + setData(res.data); + } catch { + setError('加载失败,请重试'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + // ponytail: 30s auto-refresh, add WebSocket if real-time needed + }, []); + + if (loading) return ; + if (error) return ; + if (!data) return null; + + return ( +
+ {/* 统计卡片 */} +
+

设备概览

+
+ + + + +
+
+ + {/* 今日统计 */} +
+

今日统计

+
+
+

{data.today_charge_count}

+

充电次数

+
+
+
+

{data.today_charge_hours}

+

充电时长(h)

+
+
+
+

{data.today_energy_kwh.toLocaleString()}

+

能耗(kWh)

+
+
+
+ + {/* 告警通知 */} + {data.alerts.length > 0 && ( +
+

告警通知

+
+ {data.alerts.map((alert, i) => ( + + ))} +
+
+ )} + + {/* 项目列表 */} +
+

项目列表

+
+ {data.projects.map((project) => ( + navigate(`/pms/devices?project_id=${project.id}`)} + /> + ))} +
+
+
+ ); +} diff --git a/software/h5/src/h5/pages/login.tsx b/software/h5/src/h5/pages/login.tsx new file mode 100644 index 0000000..c3e861d --- /dev/null +++ b/software/h5/src/h5/pages/login.tsx @@ -0,0 +1,101 @@ +/** + * 登录页 + * 深色科技风格,与产品介绍页统一 + */ +import { useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useH5AuthStore } from '../auth'; + +export default function H5Login() { + const [phone, setPhone] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const { login, loading } = useH5AuthStore(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const redirect = searchParams.get('redirect') || '/pms'; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + if (!phone || !password) { + setError('请输入手机号和密码'); + return; + } + try { + await login(phone, password); + navigate(redirect, { replace: true }); + } catch (err) { + setError((err as Error).message); + } + }; + + return ( +
+ {/* 背景网格 */} +
+ + {/* 内容 */} +
+ {/* Logo */} +
+
+ + + +
+

安知充

+

充电柜管理系统

+
+ + {/* 登录表单 */} +
+
+ setPhone(e.target.value)} + autoComplete="tel" + /> +
+
+ setPassword(e.target.value)} + autoComplete="current-password" + /> +
+ + {error && ( +

{error}

+ )} + + +
+ + {/* 底部信息 */} +
+

无锡艾动电子有限公司

+
+
+
+ ); +} diff --git a/software/h5/src/h5/pages/me.tsx b/software/h5/src/h5/pages/me.tsx new file mode 100644 index 0000000..6772500 --- /dev/null +++ b/software/h5/src/h5/pages/me.tsx @@ -0,0 +1,63 @@ +/** + * H5 我的页面 + * - 个人信息展示 + * - 退出登录 + */ +import { useNavigate } from 'react-router-dom'; +import { useH5AuthStore } from '../auth'; +import { PageContainer, ConfirmDialog } from '../components'; +import { useState } from 'react'; + +export default function H5Me() { + const { user, logout } = useH5AuthStore(); + const navigate = useNavigate(); + const [showLogout, setShowLogout] = useState(false); + + const handleLogout = () => { + logout(); + navigate('/pms/login', { replace: true }); + }; + + return ( + + {/* 用户信息 */} +
+
+ {(user?.name || user?.phone || '?').charAt(0)} +
+
+

{user?.name || '用户'}

+

{user?.phone}

+
+
+ + {/* 菜单项 */} +
+ + {/* ponytail: password change page placeholder, add when backend endpoint ready */} +
+ +
+ +
+ + setShowLogout(false)} + /> +
+ ); +} diff --git a/software/h5/src/h5/routes.tsx b/software/h5/src/h5/routes.tsx new file mode 100644 index 0000000..1f18032 --- /dev/null +++ b/software/h5/src/h5/routes.tsx @@ -0,0 +1,82 @@ +/** + * H5 路由配置 + * 所有路由相对于 /h5/ 基路径 + */ +import { Route } from 'react-router-dom'; +import H5Layout from './layout'; +import H5Login from './pages/login'; +import H5Home from './pages/home'; +import H5Devices from './pages/devices'; +import H5DeviceDetail from './pages/device-detail'; +import H5CompartmentDetail from './pages/compartment-detail'; +import H5Me from './pages/me'; + +/** + * H5 路由片段: + * + * /login - 登录 + * / - 首页(需认证,底部导航) + * /devices - 设备列表(需认证,底部导航) + * /devices/:id - 设备详情(需认证,底部导航) + * /compartments/:id - 通道详情(需认证,全屏) + * /me - 我的(需认证,底部导航) + * /:abstractId - 扫码入口 + */ +export const h5Routes = ( + <> + {/* 登录页 - 无底部导航 */} + } /> + + {/* H5 主布局(含底部导航 + 认证守卫) */} + }> + } /> + } /> + } /> + } /> + + + {/* 通道详情 - 全屏页面,直接挂载 */} + } /> + + {/* 扫码入口: /{abstract_id} → 设备详情 */} + } /> + +); + +/** 扫码入口处理:通过 abstract_id 查设备详情 */ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { h5Api } from './api'; +import { Loading } from './components'; + +function ScanRedirect() { + const { abstractId } = useParams<{ abstractId: string }>(); + const navigate = useNavigate(); + const [error, setError] = useState(''); + + useEffect(() => { + // 排除已知路径 + if (!abstractId || + ['login', 'devices', 'me', 'compartments'].includes(abstractId)) { + return; + } + + h5Api.getCabinetByAbstractId(abstractId) + .then((res) => navigate(`/devices/${res.data.id}`, { replace: true })) + .catch(() => setError('未找到该设备')); + }, [abstractId, navigate]); + + if (error) { + return ( +
+

{error}

+
+ ); + } + + return ( +
+ +
+ ); +} diff --git a/software/h5/src/index.css b/software/h5/src/index.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/software/h5/src/index.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/software/h5/src/main.tsx b/software/h5/src/main.tsx new file mode 100644 index 0000000..964aeb4 --- /dev/null +++ b/software/h5/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/software/h5/tsconfig.app.json b/software/h5/tsconfig.app.json new file mode 100644 index 0000000..8a2c607 --- /dev/null +++ b/software/h5/tsconfig.app.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023", "DOM"], + "module": "esnext", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + "skipLibCheck": true, + "ignoreDeprecations": "6.0", + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"] +} diff --git a/software/h5/tsconfig.json b/software/h5/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/software/h5/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/software/h5/tsconfig.node.json b/software/h5/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/software/h5/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/software/h5/vite.config.ts b/software/h5/vite.config.ts new file mode 100644 index 0000000..d304e71 --- /dev/null +++ b/software/h5/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' +import path from 'path' + +export default defineConfig({ + base: '/h5/', + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + server: { + port: 5174, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, +}) diff --git a/software/web/src/App.tsx b/software/web/src/App.tsx index 1d602a8..73c1ea8 100644 --- a/software/web/src/App.tsx +++ b/software/web/src/App.tsx @@ -1,6 +1,6 @@ /** * 应用根组件 - * - 路由配置:登录页 + 后台管理布局(嵌套子路由)+ H5用户端(/pms/*) + * - 路由配置:登录页 + 后台管理布局(嵌套子路由) * - 未认证时自动跳转到登录页 */ @@ -12,7 +12,6 @@ import { Navigate, } from 'react-router-dom'; import { useAuthStore } from '@/stores/auth'; -import { useH5AuthStore } from '@/h5/auth'; import AdminLayout from '@/layouts/AdminLayout'; import Login from '@/pages/Login'; import Dashboard from '@/pages/Dashboard'; @@ -24,7 +23,6 @@ import Energy from '@/pages/energy'; import Users from '@/pages/settings/users'; import Roles from '@/pages/settings/roles'; import OperationLogs from '@/pages/settings/operation-logs'; -import { h5Routes } from '@/h5/routes'; /** 路由守卫:未认证时等待恢复,恢复失败则跳转到登录页 */ function RequireAuth({ children }: { children: React.ReactNode }) { @@ -49,13 +47,11 @@ function RedirectIfAuth({ children }: { children: React.ReactNode }) { export default function App() { const restore = useAuthStore((s) => s.restore); - const restoreH5 = useH5AuthStore((s) => s.restore); /** 应用启动时从 localStorage 恢复认证状态 */ useEffect(() => { restore(); - restoreH5(); - }, [restore, restoreH5]); + }, [restore]); return ( @@ -89,9 +85,6 @@ export default function App() { } /> - {/* H5 用户端路由 */} - {h5Routes} - {/* 默认重定向到 Dashboard */} } />