FEAT => 本次更新项目为:

设备绑定指引
This commit is contained in:
超级老白兔
2025-07-30 12:52:58 +08:00
parent 4a4e9a611f
commit f78af9e77c
14 changed files with 208 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
# 基础环境变量示例 # 基础环境变量示例
# VITE_API_BASE_URL=http://www.yishi.com VITE_API_BASE_URL=http://www.yishi.com
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com # VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
VITE_APP_TITLE=Nkebao Base VITE_APP_TITLE=Nkebao Base

View File

@@ -1,11 +1,8 @@
import React from "react"; import React from "react";
import AppRouter from "@/router"; import AppRouter from "@/router";
function App() { function App() {
return ( return <AppRouter />;
<>
<AppRouter />
</>
);
} }
export default App; export default App;

View File

@@ -0,0 +1,90 @@
import React, { useEffect, useState, useMemo } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useDeviceStore } from "@/store/module/device";
import { useUserStore } from "@/store/module/user";
import { updateDeviceCount } from "@/utils/device";
interface DeviceGuardProps {
children: React.ReactNode;
}
const DeviceGuard: React.FC<DeviceGuardProps> = ({ children }) => {
const navigate = useNavigate();
const location = useLocation();
const { isLoggedIn } = useUserStore();
const { setDeviceCount } = useDeviceStore();
const [isChecking, setIsChecking] = useState(true);
// 不需要设备检查的路径
const EXEMPT_PATHS = useMemo(
() => ["/login", "/guide", "/register", "/forgot-password"],
[],
);
useEffect(() => {
const checkDeviceStatus = async () => {
// 如果用户未登录,不需要检查设备状态
if (!isLoggedIn) {
setIsChecking(false);
return;
}
// 如果当前路径是豁免路径,不需要检查设备状态
if (EXEMPT_PATHS.includes(location.pathname)) {
setIsChecking(false);
return;
}
try {
// 从API获取最新的设备数量并更新到store
const currentDeviceCount = await updateDeviceCount(setDeviceCount);
// 如果设备数量为0且不在guide页面跳转到guide页面
if (currentDeviceCount === 0 && location.pathname !== "/guide") {
navigate("/guide");
return;
}
// 如果设备数量大于0且在guide页面跳转到首页
if (currentDeviceCount > 0 && location.pathname === "/guide") {
navigate("/");
return;
}
} catch (error) {
console.error("检查设备状态失败:", error);
// 如果检查失败默认跳转到guide页面
if (location.pathname !== "/guide") {
navigate("/guide");
return;
}
} finally {
setIsChecking(false);
}
};
checkDeviceStatus();
}, [isLoggedIn, location.pathname, navigate, setDeviceCount, EXEMPT_PATHS]);
// 如果正在检查,显示加载状态
if (isChecking) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100vh",
background: "var(--primary-color)",
}}
>
<div style={{ color: "white", fontSize: "16px" }}>
...
</div>
</div>
);
}
return <>{children}</>;
};
export default DeviceGuard;

View File

@@ -0,0 +1,13 @@
import request from "@/api/request";
// 获取设备二维码
export const fetchDeviceQRCode = (accountId: string) =>
request("/v1/api/device/add", { accountId }, "POST");
// 通过IMEI添加设备
export const addDeviceByImei = (imei: string, name: string) =>
request("/v1/api/device/add-by-imei", { imei, name }, "POST");
// 获取设备列表
export const fetchDeviceList = (params: { accountId?: string }) =>
request("/v1/devices/add-results", params, "GET");

View File

@@ -8,16 +8,15 @@ import {
QrcodeOutlined, QrcodeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { getDashboard } from "@/pages/mobile/home/api"; import { fetchDeviceQRCode, addDeviceByImei, fetchDeviceList } from "./api";
import { fetchDeviceQRCode, addDeviceByImei } from "@/api/devices"; import { useUserStore, useDeviceStore } from "@/store";
import { useUserStore } from "@/store/module/user";
import styles from "./index.module.scss"; import styles from "./index.module.scss";
const Guide: React.FC = () => { const Guide: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useUserStore(); const { user } = useUserStore();
const { deviceCount, setDeviceCount } = useDeviceStore();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [deviceCount, setDeviceCount] = useState(0);
// 添加设备弹窗状态 // 添加设备弹窗状态
const [addVisible, setAddVisible] = useState(false); const [addVisible, setAddVisible] = useState(false);
@@ -37,8 +36,10 @@ const Guide: React.FC = () => {
const checkDeviceStatus = useCallback(async () => { const checkDeviceStatus = useCallback(async () => {
try { try {
setLoading(true); setLoading(true);
const dashboardData = await getDashboard(); const dashboardData = await fetchDeviceList({
const deviceNum = dashboardData?.deviceNum || 0; accountId: user.s2_accountId,
});
const deviceNum = dashboardData.added ? 1 : 0;
setDeviceCount(deviceNum); setDeviceCount(deviceNum);
@@ -48,7 +49,6 @@ const Guide: React.FC = () => {
return; return;
} }
} catch (error) { } catch (error) {
console.error("检查设备状态失败:", error);
Toast.show({ Toast.show({
content: "检查设备状态失败,请重试", content: "检查设备状态失败,请重试",
position: "top", position: "top",
@@ -56,7 +56,7 @@ const Guide: React.FC = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, [navigate, setDeviceCount]);
useEffect(() => { useEffect(() => {
checkDeviceStatus(); checkDeviceStatus();
@@ -71,7 +71,9 @@ const Guide: React.FC = () => {
const pollDeviceStatus = async () => { const pollDeviceStatus = async () => {
try { try {
const dashboardData = await getDashboard(); const dashboardData = await fetchDeviceList({
accountId: user.s2_accountId,
});
const currentDeviceCount = dashboardData?.deviceNum || 0; const currentDeviceCount = dashboardData?.deviceNum || 0;
// 如果设备数量增加了,说明有新设备添加成功 // 如果设备数量增加了,说明有新设备添加成功
@@ -95,7 +97,7 @@ const Guide: React.FC = () => {
// 每3秒检查一次设备状态 // 每3秒检查一次设备状态
pollingRef.current = setInterval(pollDeviceStatus, 3000); pollingRef.current = setInterval(pollDeviceStatus, 3000);
}, [isPolling, deviceCount]); }, [isPolling, deviceCount, setDeviceCount]);
// 停止轮询 // 停止轮询
const stopPolling = useCallback(() => { const stopPolling = useCallback(() => {

View File

@@ -6,9 +6,9 @@ import {
EyeOutline, EyeOutline,
UserOutline, UserOutline,
} from "antd-mobile-icons"; } from "antd-mobile-icons";
import { useUserStore } from "@/store/module/user"; import { useUserStore, useDeviceStore } from "@/store";
import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api"; import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api";
import { getDashboard } from "@/pages/mobile/home/api"; import { updateDeviceCount } from "@/utils/device";
import style from "./login.module.scss"; import style from "./login.module.scss";
const Login: React.FC = () => { const Login: React.FC = () => {
@@ -22,6 +22,7 @@ const Login: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { login } = useUserStore(); const { login } = useUserStore();
const { setDeviceCount } = useDeviceStore();
// 倒计时效果 // 倒计时效果
useEffect(() => { useEffect(() => {
@@ -43,33 +44,17 @@ const Login: React.FC = () => {
// 发送验证码 // 发送验证码
const handleSendVerificationCode = async () => { const handleSendVerificationCode = async () => {
const account = form.getFieldValue("account");
if (!account) {
Toast.show({ content: "请输入手机号", position: "top" });
return;
}
// 手机号格式验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(account)) {
Toast.show({ content: "请输入正确的11位手机号", position: "top" });
return;
}
try { try {
setLoading(true); const phone = form.getFieldValue("phone");
await sendVerificationCode({ if (!phone) {
mobile: account, Toast.show({ content: "请输入手机号", position: "top" });
type: "login", return;
}); }
await sendVerificationCode(phone);
Toast.show({ content: "验证码已发送", position: "top" });
setCountdown(60); setCountdown(60);
} catch (error) { Toast.show({ content: "验证码已发送", position: "top" });
// 错误已在request中处理这里不需要额外处理 } catch (error: any) {
} finally { // 错误已在request中处理
setLoading(false);
} }
}; };
@@ -101,11 +86,11 @@ const Login: React.FC = () => {
Toast.show({ content: "登录成功", position: "top" }); Toast.show({ content: "登录成功", position: "top" });
// 检查设备绑定状态 // 检查设备绑定状态并更新到store
try { try {
const dashboardData = await getDashboard(); const deviceNum = await updateDeviceCount(setDeviceCount);
const deviceNum = dashboardData?.deviceNum || 0;
console.log(deviceNum, "deviceNum"); console.log(deviceNum, "deviceNum");
// 如果没有绑定设备,跳转到引导页面 // 如果没有绑定设备,跳转到引导页面
if (deviceNum === 0) { if (deviceNum === 0) {
navigate("/guide"); navigate("/guide");
@@ -113,7 +98,10 @@ const Login: React.FC = () => {
} }
} catch (error) { } catch (error) {
console.error("检查设备状态失败:", error); console.error("检查设备状态失败:", error);
// 如果检查失败,默认跳转到首页 // 如果检查失败,设置设备数量为0并跳转到guide页面
setDeviceCount(0);
navigate("/guide");
return;
} }
// 跳转到首页或重定向URL // 跳转到首页或重定向URL

View File

@@ -3,13 +3,12 @@ import { useParams, useNavigate } from "react-router-dom";
import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile"; import { NavBar, Tabs, Switch, Toast, SpinLoading, Button } from "antd-mobile";
import { SettingOutlined, RedoOutlined } from "@ant-design/icons"; import { SettingOutlined, RedoOutlined } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import MeauMobile from "@/components/MeauMobile/MeauMoible";
import { import {
fetchDeviceDetail, fetchDeviceDetail,
fetchDeviceRelatedAccounts, fetchDeviceRelatedAccounts,
fetchDeviceHandleLogs, fetchDeviceHandleLogs,
updateDeviceTaskConfig, updateDeviceTaskConfig,
} from "@/api/devices"; } from "./api";
import type { Device, WechatAccount, HandleLog } from "@/types/device"; import type { Device, WechatAccount, HandleLog } from "@/types/device";
const DeviceDetail: React.FC = () => { const DeviceDetail: React.FC = () => {

View File

@@ -1,4 +1,4 @@
import request from "./request"; import request from "@/api/request";
// 获取设备列表 // 获取设备列表
export const fetchDeviceList = (params: { export const fetchDeviceList = (params: {

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState, useCallback } from "react"; import React, { useEffect, useRef, useState, useCallback } from "react";
import { NavBar, Popup, Tabs, Toast, SpinLoading, Dialog } from "antd-mobile"; import { Popup, Tabs, Toast, SpinLoading } from "antd-mobile";
import { Button, Input, Pagination, Checkbox } from "antd"; import { Button, Input, Pagination, Checkbox } from "antd";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { AddOutline, DeleteOutline } from "antd-mobile-icons"; import { AddOutline, DeleteOutline } from "antd-mobile-icons";
@@ -7,7 +7,6 @@ import {
ReloadOutlined, ReloadOutlined,
SearchOutlined, SearchOutlined,
QrcodeOutlined, QrcodeOutlined,
ArrowLeftOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { import {
@@ -15,7 +14,7 @@ import {
fetchDeviceQRCode, fetchDeviceQRCode,
addDeviceByImei, addDeviceByImei,
deleteDevice, deleteDevice,
} from "@/api/devices"; } from "./api";
import type { Device } from "@/types/device"; import type { Device } from "@/types/device";
import { comfirm } from "@/utils/common"; import { comfirm } from "@/utils/common";
import { useUserStore } from "@/store/module/user"; import { useUserStore } from "@/store/module/user";

View File

@@ -1,6 +1,6 @@
import request from "@/api/request"; import request from "@/api/request";
import type { TrafficPoolListResponse, DeviceOption } from "./data"; import type { TrafficPoolListResponse, DeviceOption } from "./data";
import { fetchDeviceList } from "@/api/devices"; import { fetchDeviceList } from "@/pages/guide/api";
// 获取流量池列表 // 获取流量池列表
export function fetchTrafficPoolList(params: { export function fetchTrafficPoolList(params: {

View File

@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { BrowserRouter, useRoutes, RouteObject } from "react-router-dom"; import { BrowserRouter, useRoutes, RouteObject } from "react-router-dom";
import PermissionRoute from "./permissionRoute"; import PermissionRoute from "./permissionRoute";
import DeviceGuard from "@/components/DeviceGuard";
// 动态导入所有 module 下的 ts/tsx 路由模块 // 动态导入所有 module 下的 ts/tsx 路由模块
const modules = import.meta.glob("./module/*.{ts,tsx}", { eager: true }); const modules = import.meta.glob("./module/*.{ts,tsx}", { eager: true });
@@ -42,7 +43,9 @@ const AppRouter: React.FC = () => (
v7_relativeSplatPath: true, v7_relativeSplatPath: true,
}} }}
> >
<AppRoutes /> <DeviceGuard>
<AppRoutes />
</DeviceGuard>
</BrowserRouter> </BrowserRouter>
); );

View File

@@ -1,2 +1,3 @@
export * from "./module/user"; export * from "./module/user";
export * from "./module/device";
// 未来可继续合并其他模块 // 未来可继续合并其他模块

View File

@@ -0,0 +1,30 @@
import { createPersistStore } from "@/store/createPersistStore";
export interface DeviceState {
deviceCount: number;
setDeviceCount: (count: number) => void;
updateDeviceCount: () => Promise<void>;
resetDeviceCount: () => void;
}
export const useDeviceStore = createPersistStore<DeviceState>(
(set, get) => ({
deviceCount: 0,
setDeviceCount: (count: number) => set({ deviceCount: count }),
updateDeviceCount: async () => {
try {
// 这里需要导入getDashboard但为了避免循环依赖我们通过参数传入
// 实际使用时会在组件中调用并传入API函数
set({ deviceCount: 0 }); // 默认设置为0实际值由调用方设置
} catch (error) {
console.error("更新设备数量失败:", error);
set({ deviceCount: 0 });
}
},
resetDeviceCount: () => set({ deviceCount: 0 }),
}),
"device-store",
state => ({
deviceCount: state.deviceCount,
}),
);

View File

@@ -0,0 +1,30 @@
import { getDashboard } from "@/pages/mobile/home/api";
/**
* 更新设备数量到store
* @param setDeviceCount store中的setDeviceCount函数
* @returns 更新后的设备数量
*/
export const updateDeviceCount = async (
setDeviceCount: (count: number) => void,
): Promise<number> => {
try {
const dashboardData = await getDashboard();
const deviceCount = dashboardData?.deviceNum || 0;
setDeviceCount(deviceCount);
return deviceCount;
} catch (error) {
console.error("更新设备数量失败:", error);
setDeviceCount(0);
return 0;
}
};
/**
* 检查是否需要设备绑定
* @param deviceCount 设备数量
* @returns 是否需要设备绑定
*/
export const needsDeviceBinding = (deviceCount: number): boolean => {
return deviceCount === 0;
};