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 { .guideContainer {
min-height: 100vh; height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: var(--primary-color);
padding: 20px; padding: 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
@@ -26,7 +26,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: var(--primary-color);
} }
.loadingText { .loadingText {
@@ -38,42 +38,40 @@
.header { .header {
text-align: center; text-align: center;
margin-bottom: 40px; margin-bottom: 20px;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.iconContainer { .iconContainer {
width: 80px; width: 60px;
height: 80px; height: 60px;
background: rgba(255, 255, 255, 0.2); background: #fff;
border-radius: 50%; border-radius: 50%;
display: flex; margin: 0 auto 12px;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3); border: 1px solid rgba(255, 255, 255, 0.3);
overflow: hidden;
} }
.warningIcon { .logo {
font-size: 40px; width: 100%;
color: #ffd700; height: 100%;
object-fit: contain;
} }
.title { .title {
color: white; color: white;
font-size: 28px; font-size: 22px;
font-weight: 700; font-weight: 700;
margin-bottom: 12px; margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.subtitle { .subtitle {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
font-size: 16px; font-size: 14px;
line-height: 1.5; line-height: 1.4;
max-width: 300px; max-width: 280px;
margin: 0 auto; margin: 0 auto;
} }
@@ -81,34 +79,36 @@
flex: 1; flex: 1;
position: relative; position: relative;
z-index: 1; z-index: 1;
overflow-y: auto;
padding-right: 4px;
} }
.deviceStatus { .deviceStatus {
margin-bottom: 30px; margin-bottom: 16px;
} }
.statusCard { .statusCard {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 16px; border-radius: 12px;
padding: 20px; padding: 12px;
display: flex; display: flex;
align-items: center; 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); backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
.statusIcon { .statusIcon {
width: 50px; width: 40px;
height: 50px; height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: var(--primary-color);
border-radius: 12px; border-radius: 8px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 16px; margin-right: 12px;
color: white; color: white;
font-size: 24px; font-size: 20px;
} }
.statusInfo { .statusInfo {
@@ -116,69 +116,69 @@
} }
.statusTitle { .statusTitle {
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 2px;
} }
.statusValue { .statusValue {
font-size: 14px; font-size: 12px;
color: #666; color: #666;
} }
.deviceCount { .deviceCount {
color: #667eea; color: var(--primary-color);
font-weight: 700; font-weight: 700;
font-size: 18px; font-size: 16px;
} }
.guideSteps { .guideSteps {
margin-bottom: 30px; margin-bottom: 16px;
} }
.stepsTitle { .stepsTitle {
color: white; color: white;
font-size: 20px; font-size: 16px;
font-weight: 600; font-weight: 600;
margin-bottom: 20px; margin-bottom: 12px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
} }
.stepList { .stepList {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 8px;
} }
.stepItem { .stepItem {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 12px; border-radius: 8px;
padding: 16px; padding: 10px;
display: flex; display: flex;
align-items: flex-start; 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); backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
transition: transform 0.2s ease; transition: transform 0.2s ease;
&:hover { &:hover {
transform: translateY(-2px); transform: translateY(-1px);
} }
} }
.stepNumber { .stepNumber {
width: 32px; width: 24px;
height: 32px; height: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: var(--primary-color);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: white; color: white;
font-weight: 700; font-weight: 700;
font-size: 14px; font-size: 12px;
margin-right: 16px; margin-right: 10px;
flex-shrink: 0; flex-shrink: 0;
} }
@@ -187,23 +187,23 @@
} }
.stepTitle { .stepTitle {
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 4px; margin-bottom: 2px;
} }
.stepDesc { .stepDesc {
font-size: 14px; font-size: 12px;
color: #666; color: #666;
line-height: 1.4; line-height: 1.3;
} }
.tips { .tips {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border-radius: 12px; border-radius: 8px;
padding: 20px; padding: 12px;
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); backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
} }
@@ -211,24 +211,24 @@
.tipsTitle { .tipsTitle {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: 16px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 12px; margin-bottom: 8px;
} }
.tipsIcon { .tipsIcon {
color: #ff6b6b; color: #ff6b6b;
margin-right: 8px; margin-right: 6px;
font-size: 18px; font-size: 16px;
} }
.tipsContent { .tipsContent {
p { p {
font-size: 14px; font-size: 12px;
color: #666; color: #666;
line-height: 1.6; line-height: 1.4;
margin-bottom: 8px; margin-bottom: 4px;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
@@ -237,41 +237,41 @@
} }
.footer { .footer {
margin-top: 30px; margin-top: 16px;
position: relative; position: relative;
z-index: 1; z-index: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 8px;
} }
.primaryButton { .primaryButton {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: white;
border: none; border: none;
border-radius: 12px; border-radius: 8px;
height: 48px; height: 44px;
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: white; color: var(--primary-color);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4); box-shadow: 0 2px 12px rgba(255, 255, 255, 0.4);
transition: all 0.3s ease; transition: all 0.3s ease;
&:active { &:active {
transform: translateY(1px); 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 { .buttonIcon {
margin-left: 8px; margin-left: 6px;
font-size: 14px; font-size: 12px;
} }
.secondaryButton { .secondaryButton {
border: 2px solid rgba(255, 255, 255, 0.8); border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 12px; border-radius: 8px;
height: 48px; height: 44px;
font-size: 16px; font-size: 15px;
font-weight: 600; font-weight: 600;
color: white; color: white;
background: transparent; background: transparent;
@@ -285,27 +285,27 @@
// 响应式设计 // 响应式设计
@media (max-width: 480px) { @media (max-width: 480px) {
.guideContainer { .guideContainer {
padding: 16px; padding: 12px;
} }
.title { .title {
font-size: 24px; font-size: 20px;
} }
.subtitle { .subtitle {
font-size: 14px; font-size: 13px;
} }
.statusCard { .statusCard {
padding: 16px; padding: 10px;
} }
.stepItem { .stepItem {
padding: 14px; padding: 8px;
} }
.tips { .tips {
padding: 16px; padding: 10px;
} }
} }
@@ -313,7 +313,7 @@
@keyframes fadeInUp { @keyframes fadeInUp {
from { from {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(20px);
} }
to { to {
opacity: 1; opacity: 1;
@@ -325,7 +325,7 @@
.deviceStatus, .deviceStatus,
.guideSteps, .guideSteps,
.tips { .tips {
animation: fadeInUp 0.6s ease-out; animation: fadeInUp 0.5s ease-out;
} }
.guideSteps { .guideSteps {
@@ -337,5 +337,5 @@
} }
.footer { .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 { useNavigate } from "react-router-dom";
import { Button, Toast } from "antd-mobile"; import { Button, Toast, Popup, Tabs, Input } from "antd-mobile";
import { import {
MobileOutlined, MobileOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
ArrowRightOutlined, ArrowRightOutlined,
QrcodeOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import Layout from "@/components/Layout/Layout";
import { useUserStore } from "@/store/module/user";
import { getDashboard } from "@/pages/mobile/home/api"; 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"; import styles from "./index.module.scss";
const Guide: React.FC = () => { const Guide: React.FC = () => {
@@ -16,21 +18,29 @@ const Guide: React.FC = () => {
const { user } = useUserStore(); const { user } = useUserStore();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [deviceCount, setDeviceCount] = useState(0); 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 { try {
setLoading(true); setLoading(true);
const dashboardData = await getDashboard(); const dashboardData = await getDashboard();
const deviceNum = dashboardData?.deviceNum || 0; const deviceNum = dashboardData?.deviceNum || 0;
setDeviceCount(deviceNum); setDeviceCount(deviceNum);
setHasDevices(deviceNum > 0);
// 如果已有设备,直接跳转到首页 // 如果已有设备,直接跳转到首页
if (deviceNum > 0) { if (deviceNum > 0) {
@@ -46,16 +56,116 @@ const Guide: React.FC = () => {
} finally { } finally {
setLoading(false); 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 = () => { const handleGoToDevices = () => {
navigate("/devices"); handleGetQr();
setAddVisible(true);
}; };
// 跳转到首页(跳过引导) // 手动添加设备
const handleSkipGuide = () => { const handleAddDevice = async () => {
navigate("/"); 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) { if (loading) {
@@ -74,12 +184,10 @@ const Guide: React.FC = () => {
{/* 头部区域 */} {/* 头部区域 */}
<div className={styles.header}> <div className={styles.header}>
<div className={styles.iconContainer}> <div className={styles.iconContainer}>
<ExclamationCircleOutlined className={styles.warningIcon} /> <img src="/logo.png" alt="存客宝" className={styles.logo} />
</div> </div>
<h1 className={styles.title}>使</h1> <h1 className={styles.title}>使</h1>
<p className={styles.subtitle}> <p className={styles.subtitle}></p>
使
</p>
</div> </div>
{/* 内容区域 */} {/* 内容区域 */}
@@ -92,7 +200,7 @@ const Guide: React.FC = () => {
<div className={styles.statusInfo}> <div className={styles.statusInfo}>
<div className={styles.statusTitle}></div> <div className={styles.statusTitle}></div>
<div className={styles.statusValue}> <div className={styles.statusValue}>
<span className={styles.deviceCount}>{deviceCount}</span> <span className={styles.deviceCount}>{deviceCount}</span>
</div> </div>
</div> </div>
@@ -100,14 +208,14 @@ const Guide: React.FC = () => {
</div> </div>
<div className={styles.guideSteps}> <div className={styles.guideSteps}>
<h2 className={styles.stepsTitle}></h2> <h2 className={styles.stepsTitle}></h2>
<div className={styles.stepList}> <div className={styles.stepList}>
<div className={styles.stepItem}> <div className={styles.stepItem}>
<div className={styles.stepNumber}>1</div> <div className={styles.stepNumber}>1</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<div className={styles.stepTitle}></div> <div className={styles.stepTitle}></div>
<div className={styles.stepDesc}> <div className={styles.stepDesc}>
</div> </div>
</div> </div>
</div> </div>
@@ -115,9 +223,7 @@ const Guide: React.FC = () => {
<div className={styles.stepNumber}>2</div> <div className={styles.stepNumber}>2</div>
<div className={styles.stepContent}> <div className={styles.stepContent}>
<div className={styles.stepTitle}></div> <div className={styles.stepTitle}></div>
<div className={styles.stepDesc}> <div className={styles.stepDesc}></div>
</div>
</div> </div>
</div> </div>
<div className={styles.stepItem}> <div className={styles.stepItem}>
@@ -138,7 +244,7 @@ const Guide: React.FC = () => {
</div> </div>
<div className={styles.tipsContent}> <div className={styles.tipsContent}>
<p> </p> <p> </p>
<p> 10</p> <p> 10</p>
<p> </p> <p> </p>
</div> </div>
@@ -157,18 +263,86 @@ const Guide: React.FC = () => {
<ArrowRightOutlined className={styles.buttonIcon} /> <ArrowRightOutlined className={styles.buttonIcon} />
</Button> </Button>
<Button
block
fill="outline"
size="large"
className={styles.secondaryButton}
onClick={handleSkipGuide}
>
</Button>
</div> </div>
</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> </Layout>
); );
}; };

View File

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

View File

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