FEAT => 本次更新项目为:

存客宝设备绑定引导页
This commit is contained in:
超级老白兔
2025-07-30 10:40:15 +08:00
parent 51495a93ff
commit 4a4e9a611f
5 changed files with 297 additions and 129 deletions

BIN
nkebao/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

View File

@@ -1,7 +1,7 @@
.guideContainer {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
height: 100vh;
background: var(--primary-color);
padding: 16px;
display: flex;
flex-direction: column;
position: relative;
@@ -26,7 +26,7 @@
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: var(--primary-color);
}
.loadingText {
@@ -38,42 +38,40 @@
.header {
text-align: center;
margin-bottom: 40px;
margin-bottom: 20px;
position: relative;
z-index: 1;
}
.iconContainer {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.2);
width: 60px;
height: 60px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
backdrop-filter: blur(10px);
margin: 0 auto 12px;
border: 1px solid rgba(255, 255, 255, 0.3);
overflow: hidden;
}
.warningIcon {
font-size: 40px;
color: #ffd700;
.logo {
width: 100%;
height: 100%;
object-fit: contain;
}
.title {
color: white;
font-size: 28px;
font-size: 22px;
font-weight: 700;
margin-bottom: 12px;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.subtitle {
color: rgba(255, 255, 255, 0.9);
font-size: 16px;
line-height: 1.5;
max-width: 300px;
font-size: 14px;
line-height: 1.4;
max-width: 280px;
margin: 0 auto;
}
@@ -81,34 +79,36 @@
flex: 1;
position: relative;
z-index: 1;
overflow-y: auto;
padding-right: 4px;
}
.deviceStatus {
margin-bottom: 30px;
margin-bottom: 16px;
}
.statusCard {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 20px;
border-radius: 12px;
padding: 12px;
display: flex;
align-items: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.statusIcon {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
width: 40px;
height: 40px;
background: var(--primary-color);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
margin-right: 12px;
color: white;
font-size: 24px;
font-size: 20px;
}
.statusInfo {
@@ -116,69 +116,69 @@
}
.statusTitle {
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
margin-bottom: 2px;
}
.statusValue {
font-size: 14px;
font-size: 12px;
color: #666;
}
.deviceCount {
color: #667eea;
color: var(--primary-color);
font-weight: 700;
font-size: 18px;
font-size: 16px;
}
.guideSteps {
margin-bottom: 30px;
margin-bottom: 16px;
}
.stepsTitle {
color: white;
font-size: 20px;
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
margin-bottom: 12px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.stepList {
display: flex;
flex-direction: column;
gap: 16px;
gap: 8px;
}
.stepItem {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 16px;
border-radius: 8px;
padding: 10px;
display: flex;
align-items: flex-start;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
transform: translateY(-1px);
}
}
.stepNumber {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
width: 24px;
height: 24px;
background: var(--primary-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 14px;
margin-right: 16px;
font-size: 12px;
margin-right: 10px;
flex-shrink: 0;
}
@@ -187,23 +187,23 @@
}
.stepTitle {
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
margin-bottom: 2px;
}
.stepDesc {
font-size: 14px;
font-size: 12px;
color: #666;
line-height: 1.4;
line-height: 1.3;
}
.tips {
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
border-radius: 8px;
padding: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
@@ -211,24 +211,24 @@
.tipsTitle {
display: flex;
align-items: center;
font-size: 16px;
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
margin-bottom: 8px;
}
.tipsIcon {
color: #ff6b6b;
margin-right: 8px;
font-size: 18px;
margin-right: 6px;
font-size: 16px;
}
.tipsContent {
p {
font-size: 14px;
font-size: 12px;
color: #666;
line-height: 1.6;
margin-bottom: 8px;
line-height: 1.4;
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
@@ -237,41 +237,41 @@
}
.footer {
margin-top: 30px;
margin-top: 16px;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 12px;
gap: 8px;
}
.primaryButton {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: white;
border: none;
border-radius: 12px;
height: 48px;
font-size: 16px;
border-radius: 8px;
height: 44px;
font-size: 15px;
font-weight: 600;
color: white;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
color: var(--primary-color);
box-shadow: 0 2px 12px rgba(255, 255, 255, 0.4);
transition: all 0.3s ease;
&:active {
transform: translateY(1px);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.4);
box-shadow: 0 1px 6px rgba(255, 255, 255, 0.4);
}
}
.buttonIcon {
margin-left: 8px;
font-size: 14px;
margin-left: 6px;
font-size: 12px;
}
.secondaryButton {
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 12px;
height: 48px;
font-size: 16px;
border-radius: 8px;
height: 44px;
font-size: 15px;
font-weight: 600;
color: white;
background: transparent;
@@ -285,27 +285,27 @@
// 响应式设计
@media (max-width: 480px) {
.guideContainer {
padding: 16px;
padding: 12px;
}
.title {
font-size: 24px;
font-size: 20px;
}
.subtitle {
font-size: 14px;
font-size: 13px;
}
.statusCard {
padding: 16px;
padding: 10px;
}
.stepItem {
padding: 14px;
padding: 8px;
}
.tips {
padding: 16px;
padding: 10px;
}
}
@@ -313,7 +313,7 @@
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
transform: translateY(20px);
}
to {
opacity: 1;
@@ -325,7 +325,7 @@
.deviceStatus,
.guideSteps,
.tips {
animation: fadeInUp 0.6s ease-out;
animation: fadeInUp 0.5s ease-out;
}
.guideSteps {
@@ -337,5 +337,5 @@
}
.footer {
animation: fadeInUp 0.6s ease-out 0.3s both;
animation: fadeInUp 0.5s ease-out 0.3s both;
}

View File

@@ -1,14 +1,16 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useCallback, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Toast } from "antd-mobile";
import { Button, Toast, Popup, Tabs, Input } from "antd-mobile";
import {
MobileOutlined,
ExclamationCircleOutlined,
ArrowRightOutlined,
QrcodeOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import { useUserStore } from "@/store/module/user";
import { getDashboard } from "@/pages/mobile/home/api";
import { fetchDeviceQRCode, addDeviceByImei } from "@/api/devices";
import { useUserStore } from "@/store/module/user";
import styles from "./index.module.scss";
const Guide: React.FC = () => {
@@ -16,21 +18,29 @@ const Guide: React.FC = () => {
const { user } = useUserStore();
const [loading, setLoading] = useState(true);
const [deviceCount, setDeviceCount] = useState(0);
const [hasDevices, setHasDevices] = useState(false);
useEffect(() => {
checkDeviceStatus();
}, []);
// 添加设备弹窗状态
const [addVisible, setAddVisible] = useState(false);
const [addTab, setAddTab] = useState("scan");
const [qrLoading, setQrLoading] = useState(false);
const [qrCode, setQrCode] = useState<string | null>(null);
const [imei, setImei] = useState("");
const [name, setName] = useState("");
const [addLoading, setAddLoading] = useState(false);
// 轮询监听相关
const [isPolling, setIsPolling] = useState(false);
const pollingRef = useRef<NodeJS.Timeout | null>(null);
const initialDeviceCountRef = useRef(0);
// 检查设备绑定状态
const checkDeviceStatus = async () => {
const checkDeviceStatus = useCallback(async () => {
try {
setLoading(true);
const dashboardData = await getDashboard();
const deviceNum = dashboardData?.deviceNum || 0;
setDeviceCount(deviceNum);
setHasDevices(deviceNum > 0);
// 如果已有设备,直接跳转到首页
if (deviceNum > 0) {
@@ -46,16 +56,116 @@ const Guide: React.FC = () => {
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
checkDeviceStatus();
}, [checkDeviceStatus]);
// 开始轮询监听设备状态
const startPolling = useCallback(() => {
if (isPolling) return;
setIsPolling(true);
initialDeviceCountRef.current = deviceCount;
const pollDeviceStatus = async () => {
try {
const dashboardData = await getDashboard();
const currentDeviceCount = dashboardData?.deviceNum || 0;
// 如果设备数量增加了,说明有新设备添加成功
if (currentDeviceCount > initialDeviceCountRef.current) {
Toast.show({ content: "设备添加成功!", position: "top" });
setAddVisible(false);
setDeviceCount(currentDeviceCount);
setIsPolling(false);
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
// 可以选择跳转到首页或继续留在当前页面
// navigate("/");
return;
}
} catch (error) {
console.error("轮询检查设备状态失败:", error);
}
};
// 每3秒检查一次设备状态
pollingRef.current = setInterval(pollDeviceStatus, 3000);
}, [isPolling, deviceCount]);
// 停止轮询
const stopPolling = useCallback(() => {
setIsPolling(false);
if (pollingRef.current) {
clearInterval(pollingRef.current);
pollingRef.current = null;
}
}, []);
// 组件卸载时清理轮询
useEffect(() => {
return () => {
if (pollingRef.current) {
clearInterval(pollingRef.current);
}
};
}, []);
// 获取二维码
const handleGetQr = async () => {
setQrLoading(true);
setQrCode(null);
try {
const accountId = user.s2_accountId;
if (!accountId) throw new Error("未获取到用户信息");
const res = await fetchDeviceQRCode(accountId);
setQrCode(res.qrCode);
// 获取二维码后开始轮询监听
startPolling();
} catch (e: any) {
Toast.show({ content: e.message || "获取二维码失败", position: "top" });
} finally {
setQrLoading(false);
}
};
// 跳转到设备管理页面
const handleGoToDevices = () => {
navigate("/devices");
handleGetQr();
setAddVisible(true);
};
// 跳转到首页(跳过引导)
const handleSkipGuide = () => {
navigate("/");
// 手动添加设备
const handleAddDevice = async () => {
if (!imei.trim() || !name.trim()) {
Toast.show({ content: "请填写完整信息", position: "top" });
return;
}
setAddLoading(true);
try {
await addDeviceByImei(imei, name);
Toast.show({ content: "添加成功", position: "top" });
setAddVisible(false);
setImei("");
setName("");
// 重新检查设备状态
await checkDeviceStatus();
} catch (e: any) {
Toast.show({ content: e.message || "添加失败", position: "top" });
} finally {
setAddLoading(false);
}
};
// 关闭弹窗时停止轮询
const handleClosePopup = () => {
setAddVisible(false);
stopPolling();
setQrCode(null);
};
if (loading) {
@@ -74,12 +184,10 @@ const Guide: React.FC = () => {
{/* 头部区域 */}
<div className={styles.header}>
<div className={styles.iconContainer}>
<ExclamationCircleOutlined className={styles.warningIcon} />
<img src="/logo.png" alt="存客宝" className={styles.logo} />
</div>
<h1 className={styles.title}>使</h1>
<p className={styles.subtitle}>
使
</p>
<p className={styles.subtitle}></p>
</div>
{/* 内容区域 */}
@@ -92,7 +200,7 @@ const Guide: React.FC = () => {
<div className={styles.statusInfo}>
<div className={styles.statusTitle}></div>
<div className={styles.statusValue}>
<span className={styles.deviceCount}>{deviceCount}</span>
</div>
</div>
@@ -100,14 +208,14 @@ const Guide: React.FC = () => {
</div>
<div className={styles.guideSteps}>
<h2 className={styles.stepsTitle}></h2>
<h2 className={styles.stepsTitle}></h2>
<div className={styles.stepList}>
<div className={styles.stepItem}>
<div className={styles.stepNumber}>1</div>
<div className={styles.stepContent}>
<div className={styles.stepTitle}></div>
<div className={styles.stepDesc}>
</div>
</div>
</div>
@@ -115,9 +223,7 @@ const Guide: React.FC = () => {
<div className={styles.stepNumber}>2</div>
<div className={styles.stepContent}>
<div className={styles.stepTitle}></div>
<div className={styles.stepDesc}>
</div>
<div className={styles.stepDesc}></div>
</div>
</div>
<div className={styles.stepItem}>
@@ -138,7 +244,7 @@ const Guide: React.FC = () => {
</div>
<div className={styles.tipsContent}>
<p> </p>
<p> </p>
<p> 10</p>
<p> </p>
</div>
@@ -157,18 +263,86 @@ const Guide: React.FC = () => {
<ArrowRightOutlined className={styles.buttonIcon} />
</Button>
<Button
block
fill="outline"
size="large"
className={styles.secondaryButton}
onClick={handleSkipGuide}
>
</Button>
</div>
</div>
{/* 添加设备弹窗 */}
<Popup
visible={addVisible}
onMaskClick={handleClosePopup}
bodyStyle={{
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
minHeight: 320,
}}
>
<div style={{ padding: 20 }}>
<Tabs
activeKey={addTab}
onChange={setAddTab}
style={{ marginBottom: 16 }}
>
<Tabs.Tab title="扫码添加" key="scan" />
<Tabs.Tab title="手动添加" key="manual" />
</Tabs>
{addTab === "scan" && (
<div style={{ textAlign: "center", minHeight: 200 }}>
<Button color="primary" onClick={handleGetQr} loading={qrLoading}>
<QrcodeOutlined />
</Button>
{qrCode && (
<div style={{ marginTop: 16 }}>
<img
src={qrCode}
alt="二维码"
style={{
width: 180,
height: 180,
background: "#f5f5f5",
borderRadius: 8,
margin: "0 auto",
}}
/>
<div style={{ color: "#888", fontSize: 12, marginTop: 8 }}>
</div>
{isPolling && (
<div
style={{ color: "#1890ff", fontSize: 12, marginTop: 8 }}
>
...
</div>
)}
</div>
)}
</div>
)}
{addTab === "manual" && (
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Input
placeholder="设备名称"
value={name}
onChange={val => setName(val)}
clearable
/>
<Input
placeholder="设备IMEI"
value={imei}
onChange={val => setImei(val)}
clearable
/>
<Button
color="primary"
onClick={handleAddDevice}
loading={addLoading}
>
</Button>
</div>
)}
</div>
</Popup>
</Layout>
);
};

View File

@@ -105,7 +105,7 @@ const Login: React.FC = () => {
try {
const dashboardData = await getDashboard();
const deviceNum = dashboardData?.deviceNum || 0;
console.log(deviceNum, "deviceNum");
// 如果没有绑定设备,跳转到引导页面
if (deviceNum === 0) {
navigate("/guide");

View File

@@ -46,9 +46,7 @@ const Scene: React.FC = () => {
image:
item.image ||
"https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-api.png",
description:
scenarioDescriptions[item.name?.toLowerCase()] ||
"通过该平台进行获客",
description: "",
count: item.count,
growth: item.growth,
status: item.status,
@@ -144,11 +142,7 @@ const Scene: React.FC = () => {
</div>
</div>
<div className={style["card-title"]}>{scenario.name}</div>
{scenario.description && (
<div className={style["card-desc"]}>
{scenario.description}
</div>
)}
<div className={style["card-stats"]}>
<span className={style["card-count"]}>
: {scenario.count}