feat: add device binding home interactions
This commit is contained in:
@@ -1,43 +1,67 @@
|
||||
.home-page {
|
||||
.device-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding: 24rpx 24rpx 148rpx;
|
||||
padding: 24rpx 24rpx 156rpx;
|
||||
box-sizing: border-box;
|
||||
background: #1b2130;
|
||||
background: linear-gradient(180deg, #1e2432 0%, #191f2c 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
.device-page__halo {
|
||||
position: absolute;
|
||||
top: -54rpx;
|
||||
right: -36rpx;
|
||||
width: 340rpx;
|
||||
height: 340rpx;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle, rgba(67, 78, 109, 0.18) 0 30%, transparent 31% 47%, rgba(67, 78, 109, 0.12) 48% 66%, transparent 67%),
|
||||
radial-gradient(circle at center, rgba(87, 212, 184, 0.12), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
.device-page__halo--large {
|
||||
top: -72rpx;
|
||||
right: -42rpx;
|
||||
width: 360rpx;
|
||||
height: 360rpx;
|
||||
background:
|
||||
radial-gradient(circle, rgba(86, 212, 185, 0.06) 0 24%, transparent 25% 42%, rgba(103, 117, 155, 0.12) 43% 62%, transparent 63%),
|
||||
radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 72%);
|
||||
}
|
||||
|
||||
.device-page__halo--small {
|
||||
top: 44rpx;
|
||||
right: 92rpx;
|
||||
width: 160rpx;
|
||||
height: 120rpx;
|
||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.05), transparent 70%);
|
||||
}
|
||||
|
||||
.device-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 26rpx;
|
||||
}
|
||||
|
||||
.login-pill {
|
||||
min-width: 94rpx;
|
||||
.device-header__login {
|
||||
min-width: 92rpx;
|
||||
height: 48rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #34e5b4 0%, #18c9b7 100%);
|
||||
background: linear-gradient(90deg, #35e5b3 0%, #20c9bf 100%);
|
||||
color: #ffffff;
|
||||
font-size: 22rpx;
|
||||
line-height: 48rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 10rpx 24rpx rgba(32, 214, 181, 0.24);
|
||||
box-shadow: 0 12rpx 24rpx rgba(32, 214, 181, 0.22);
|
||||
}
|
||||
|
||||
.device-header__add {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.86);
|
||||
border-radius: 50%;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
line-height: 30rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-summary {
|
||||
@@ -47,140 +71,245 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 22rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.device-summary__content {
|
||||
.device-summary__title-wrap {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.device-summary__title {
|
||||
font-size: 26rpx;
|
||||
color: #f3f7ff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #eff4ff;
|
||||
}
|
||||
|
||||
.device-summary__count {
|
||||
color: #ff964f;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #ff9d57;
|
||||
}
|
||||
|
||||
.device-summary__arrow {
|
||||
color: #8a93a7;
|
||||
font-size: 28rpx;
|
||||
color: #7f889f;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
.device-actions-card,
|
||||
.device-status-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 38rpx 30rpx 34rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(43, 49, 67, 0.94);
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.02);
|
||||
background: rgba(42, 48, 66, 0.96);
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
.device-actions-card {
|
||||
padding: 34rpx 30rpx;
|
||||
}
|
||||
|
||||
.device-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 74rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, #39e7aa 0%, #1cc9c1 100%);
|
||||
background: linear-gradient(90deg, #39e6ad 0%, #1fc9c1 100%);
|
||||
box-shadow: 0 14rpx 26rpx rgba(20, 184, 166, 0.18);
|
||||
}
|
||||
|
||||
.action-button + .action-button {
|
||||
.device-action--secondary {
|
||||
margin-top: 34rpx;
|
||||
}
|
||||
|
||||
.action-button__icon {
|
||||
.device-action__icon {
|
||||
position: relative;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.action-button__icon-core {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.action-button__icon--scan .action-button__icon-core::before,
|
||||
.action-button__icon--scan .action-button__icon-core::after,
|
||||
.action-button__icon--tag .action-button__icon-core::before,
|
||||
.action-button__icon--tag .action-button__icon-core::after {
|
||||
.device-action__icon::before,
|
||||
.device-action__icon::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.action-button__icon--scan .action-button__icon-core::before {
|
||||
left: 6rpx;
|
||||
right: 6rpx;
|
||||
top: 14rpx;
|
||||
.device-action__icon--scan::before {
|
||||
inset: 4rpx;
|
||||
border: 2rpx solid #ffffff;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.device-action__icon--scan::after {
|
||||
left: 8rpx;
|
||||
right: 8rpx;
|
||||
top: 15rpx;
|
||||
height: 2rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.action-button__icon--scan .action-button__icon-core::after {
|
||||
top: 6rpx;
|
||||
bottom: 6rpx;
|
||||
.device-action__icon--bluetooth::before {
|
||||
left: 14rpx;
|
||||
top: 2rpx;
|
||||
width: 2rpx;
|
||||
height: 28rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.action-button__icon--tag .action-button__icon-core {
|
||||
transform: rotate(45deg) scale(0.8);
|
||||
border-radius: 6rpx;
|
||||
.device-action__icon--bluetooth::after {
|
||||
left: 8rpx;
|
||||
top: 6rpx;
|
||||
width: 14rpx;
|
||||
height: 14rpx;
|
||||
border-top: 2rpx solid #ffffff;
|
||||
border-right: 2rpx solid #ffffff;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.action-button__icon--tag .action-button__icon-core::before {
|
||||
top: 9rpx;
|
||||
left: 9rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.action-button__icon--tag .action-button__icon-core::after {
|
||||
right: -7rpx;
|
||||
top: 14rpx;
|
||||
width: 10rpx;
|
||||
height: 2rpx;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.action-button__label {
|
||||
.device-action__label {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.notice-card {
|
||||
.device-status-card {
|
||||
margin-top: 22rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.device-status-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.device-status-card__title {
|
||||
color: #edf3ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-status-card__tag {
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.device-status-card__tag--idle {
|
||||
background: rgba(125, 136, 164, 0.18);
|
||||
color: #b8c0d5;
|
||||
}
|
||||
|
||||
.device-status-card__tag--searching {
|
||||
background: rgba(54, 228, 170, 0.14);
|
||||
color: #63f0c0;
|
||||
}
|
||||
|
||||
.device-status-card__tag--empty {
|
||||
background: rgba(255, 157, 87, 0.14);
|
||||
color: #ffb173;
|
||||
}
|
||||
|
||||
.device-status-card__tag--success {
|
||||
background: rgba(72, 214, 165, 0.18);
|
||||
color: #7cf0c4;
|
||||
}
|
||||
|
||||
.device-status-card__hint {
|
||||
display: block;
|
||||
color: #f1f5ff;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.device-status-card__subhint {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
color: #8f98ad;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.device-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 22rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(25, 31, 44, 0.88);
|
||||
}
|
||||
|
||||
.device-list__item + .device-list__item {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.device-list__name {
|
||||
display: block;
|
||||
color: #f3f7ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-list__meta {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: #8992a8;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.device-list__action {
|
||||
color: #3de5af;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-success {
|
||||
margin-top: 18rpx;
|
||||
padding: 18rpx 22rpx;
|
||||
border-radius: 18rpx;
|
||||
background: rgba(61, 229, 175, 0.1);
|
||||
}
|
||||
|
||||
.device-success__title {
|
||||
display: block;
|
||||
color: #88f5cc;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.device-success__name {
|
||||
display: block;
|
||||
margin-top: 6rpx;
|
||||
color: #f3f7ff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.device-notice-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 22rpx;
|
||||
padding: 22rpx 24rpx 24rpx;
|
||||
border-radius: 22rpx;
|
||||
background: #fbefc7;
|
||||
color: #977244;
|
||||
box-shadow: 0 12rpx 24rpx rgba(6, 10, 22, 0.14);
|
||||
}
|
||||
|
||||
.notice-card__title-row {
|
||||
.device-notice-card__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.notice-card__speaker {
|
||||
.device-notice-card__horn {
|
||||
position: relative;
|
||||
width: 20rpx;
|
||||
height: 16rpx;
|
||||
@@ -189,13 +318,13 @@
|
||||
background: #ff9d57;
|
||||
}
|
||||
|
||||
.notice-card__speaker::before,
|
||||
.notice-card__speaker::after {
|
||||
.device-notice-card__horn::before,
|
||||
.device-notice-card__horn::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.notice-card__speaker::before {
|
||||
.device-notice-card__horn::before {
|
||||
right: -8rpx;
|
||||
top: 2rpx;
|
||||
width: 0;
|
||||
@@ -205,7 +334,7 @@
|
||||
border-left: 8rpx solid #ff9d57;
|
||||
}
|
||||
|
||||
.notice-card__speaker::after {
|
||||
.device-notice-card__horn::after {
|
||||
right: -16rpx;
|
||||
top: 2rpx;
|
||||
width: 8rpx;
|
||||
@@ -216,25 +345,25 @@
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.notice-card__title {
|
||||
.device-notice-card__title {
|
||||
color: #af7e42;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #af7e42;
|
||||
}
|
||||
|
||||
.notice-list {
|
||||
.device-notice-card__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.notice-item {
|
||||
.device-notice-card__item {
|
||||
color: #9f7a4c;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.7;
|
||||
color: #9f7a4c;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
.device-tabbar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@@ -244,11 +373,11 @@
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
padding: 16rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(31, 36, 49, 0.98);
|
||||
background: rgba(30, 35, 48, 0.98);
|
||||
box-shadow: 0 -8rpx 24rpx rgba(4, 8, 20, 0.34);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
.device-tabbar__item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -256,7 +385,7 @@
|
||||
min-width: 88rpx;
|
||||
}
|
||||
|
||||
.tab-item__icon {
|
||||
.device-tabbar__icon {
|
||||
position: relative;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
@@ -266,13 +395,13 @@
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.tab-item__icon::before,
|
||||
.tab-item__icon::after {
|
||||
.device-tabbar__icon::before,
|
||||
.device-tabbar__icon::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.tab-item__icon::before {
|
||||
.device-tabbar__icon::before {
|
||||
left: 8rpx;
|
||||
right: 8rpx;
|
||||
top: 10rpx;
|
||||
@@ -281,7 +410,7 @@
|
||||
color: #8f97a9;
|
||||
}
|
||||
|
||||
.tab-item__icon::after {
|
||||
.device-tabbar__icon::after {
|
||||
left: 8rpx;
|
||||
right: 8rpx;
|
||||
bottom: 10rpx;
|
||||
@@ -290,17 +419,17 @@
|
||||
color: #8f97a9;
|
||||
}
|
||||
|
||||
.tab-item__icon--active {
|
||||
.device-tabbar__icon--active {
|
||||
border-color: #36e4aa;
|
||||
background: rgba(54, 228, 170, 0.12);
|
||||
}
|
||||
|
||||
.tab-item__icon--active::before,
|
||||
.tab-item__icon--active::after {
|
||||
.device-tabbar__icon--active::before,
|
||||
.device-tabbar__icon--active::after {
|
||||
color: #36e4aa;
|
||||
}
|
||||
|
||||
.tab-item__badge {
|
||||
.device-tabbar__badge {
|
||||
position: absolute;
|
||||
top: -4rpx;
|
||||
right: -4rpx;
|
||||
@@ -308,15 +437,15 @@
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: #ff4d4f;
|
||||
box-shadow: 0 0 0 4rpx rgba(31, 36, 49, 0.98);
|
||||
box-shadow: 0 0 0 4rpx rgba(30, 35, 48, 0.98);
|
||||
}
|
||||
|
||||
.tab-item__label {
|
||||
font-size: 20rpx;
|
||||
.device-tabbar__label {
|
||||
color: #b0b6c4;
|
||||
font-size: 20rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.tab-item__label--active {
|
||||
.device-tabbar__label--active {
|
||||
color: #36e4aa;
|
||||
}
|
||||
|
||||
@@ -1,95 +1,346 @@
|
||||
import { Text, View } from "@tarojs/components";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import "./index.scss";
|
||||
|
||||
const quickActions = [
|
||||
{
|
||||
key: "scan",
|
||||
icon: "scan",
|
||||
label: "扫一扫 添加如新设备",
|
||||
toast: "后续可以在这里接入扫码添加设备"
|
||||
},
|
||||
{
|
||||
key: "tag",
|
||||
icon: "service",
|
||||
label: "暂无捐赠所得的设备",
|
||||
toast: "后续可以在这里展示捐赠设备"
|
||||
}
|
||||
];
|
||||
type BluetoothStatus = "idle" | "searching" | "empty" | "success";
|
||||
|
||||
type DeviceCandidate = {
|
||||
id: string;
|
||||
name: string;
|
||||
source: "ble" | "mock";
|
||||
};
|
||||
|
||||
const notices = [
|
||||
"1. 传感器是否上电成功,控制盒三绿灯状态。",
|
||||
"2. 对APP进行蓝牙和位置(定位服务)授权。",
|
||||
"3. 若使用扫一扫功能,请对摄像头进行授权。"
|
||||
"1. 请确保设备已开机",
|
||||
"2. 请开启蓝牙与定位权限",
|
||||
"3. 扫码功能需开启相机权限"
|
||||
];
|
||||
|
||||
const navItems = [
|
||||
{ key: "home", label: "首页", active: true },
|
||||
{ key: "report", label: "报告", active: false },
|
||||
{ key: "service", label: "小e", active: false },
|
||||
{ key: "mall", label: "商城", active: false, badge: true },
|
||||
{ key: "assistant", label: "小e", active: false },
|
||||
{ key: "message", label: "消息", active: false, badge: true },
|
||||
{ key: "mine", label: "我的", active: false }
|
||||
];
|
||||
|
||||
const mockBluetoothDevices: DeviceCandidate[] = [
|
||||
{ id: "mock-thermo-01", name: "体征监测设备 A1", source: "mock" },
|
||||
{ id: "mock-thermo-02", name: "体征监测设备 B2", source: "mock" }
|
||||
];
|
||||
|
||||
const bluetoothStateText: Record<BluetoothStatus, string> = {
|
||||
idle: "点击蓝牙按钮后,将在这里显示附近设备搜索状态。",
|
||||
searching: "正在搜索附近设备,请保持设备开机并靠近手机。",
|
||||
empty: "未发现可连接设备,请确认设备已开机并已开启蓝牙与定位。",
|
||||
success: "设备绑定成功,后续可在这里继续展示同步状态。"
|
||||
};
|
||||
|
||||
function parseDeviceCode(result?: string) {
|
||||
if (!result) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const trimmed = result.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as { code?: string; device_sn?: string };
|
||||
return parsed.code || parsed.device_sn || trimmed;
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const handleClick = (title: string) => {
|
||||
const [deviceCount, setDeviceCount] = useState(0);
|
||||
const [bluetoothStatus, setBluetoothStatus] = useState<BluetoothStatus>("idle");
|
||||
const [bluetoothDevices, setBluetoothDevices] = useState<DeviceCandidate[]>([]);
|
||||
const [recentDeviceName, setRecentDeviceName] = useState("");
|
||||
const [statusHint, setStatusHint] = useState("可通过扫码或蓝牙搜索完成设备绑定。");
|
||||
const discoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (discoveryTimerRef.current) {
|
||||
clearTimeout(discoveryTimerRef.current);
|
||||
}
|
||||
|
||||
Taro.stopBluetoothDevicesDiscovery().catch(() => undefined);
|
||||
Taro.closeBluetoothAdapter().catch(() => undefined);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const showToast = (title: string) => {
|
||||
Taro.showToast({
|
||||
title,
|
||||
icon: "none"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="home-page">
|
||||
<View className="hero-bg" />
|
||||
const bindDevice = async (name: string, sourceLabel: string) => {
|
||||
setStatusHint(`正在连接 ${name}`);
|
||||
showToast("设备配对连接中");
|
||||
|
||||
<View className="home-header">
|
||||
<View className="login-pill" onClick={() => handleClick("登录功能待接入")}>
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 600);
|
||||
});
|
||||
|
||||
setBluetoothStatus("success");
|
||||
setRecentDeviceName(name);
|
||||
setDeviceCount(1);
|
||||
setStatusHint(`${sourceLabel}绑定成功,已可开始同步健康数据。`);
|
||||
showToast("设备绑定成功");
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
showToast("登录功能待接入");
|
||||
};
|
||||
|
||||
const handleDeviceList = () => {
|
||||
showToast("设备列表页待接入");
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
showToast("后续可从这里进入设备管理页");
|
||||
};
|
||||
|
||||
const handleTabClick = (label: string) => {
|
||||
showToast(`${label}功能待接入`);
|
||||
};
|
||||
|
||||
const handleScanBind = async () => {
|
||||
try {
|
||||
setStatusHint("等待扫码识别设备编码。");
|
||||
|
||||
const result = await Taro.scanCode({
|
||||
scanType: ["qrCode", "barCode"]
|
||||
});
|
||||
|
||||
const deviceCode = parseDeviceCode(result.result);
|
||||
|
||||
if (!deviceCode) {
|
||||
showToast("二维码无法识别");
|
||||
setStatusHint("扫码成功,但未识别出可绑定的设备编码。");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatusHint(`正在查询设备 ${deviceCode}`);
|
||||
showToast("正在查询设备");
|
||||
await bindDevice(`扫码设备 ${deviceCode}`, "扫码设备");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
|
||||
if (message.includes("cancel")) {
|
||||
showToast("已取消扫码");
|
||||
setStatusHint("你取消了本次扫码绑定。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.includes("auth deny") || message.includes("authorize")) {
|
||||
showToast("请开启相机权限");
|
||||
setStatusHint("扫码功能需要相机权限,请前往系统设置开启。");
|
||||
return;
|
||||
}
|
||||
|
||||
showToast("扫码失败,请重试");
|
||||
setStatusHint("扫码失败,可能是权限不足或二维码无法识别。");
|
||||
}
|
||||
};
|
||||
|
||||
const requestLocationPermission = async () => {
|
||||
const setting = await Taro.getSetting();
|
||||
const locationAuthorized = setting.authSetting["scope.userLocation"];
|
||||
|
||||
if (locationAuthorized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.authorize({ scope: "scope.userLocation" });
|
||||
return true;
|
||||
} catch {
|
||||
showToast("请开启定位权限");
|
||||
setStatusHint("蓝牙搜索依赖定位权限,请先授权。");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleBluetoothBind = async () => {
|
||||
if (discoveryTimerRef.current) {
|
||||
clearTimeout(discoveryTimerRef.current);
|
||||
discoveryTimerRef.current = null;
|
||||
}
|
||||
|
||||
const hasPermission = await requestLocationPermission();
|
||||
|
||||
if (!hasPermission) {
|
||||
setBluetoothStatus("idle");
|
||||
setBluetoothDevices([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Taro.openBluetoothAdapter();
|
||||
setBluetoothStatus("searching");
|
||||
setBluetoothDevices([]);
|
||||
setStatusHint("正在搜索附近设备。");
|
||||
showToast("正在搜索附近设备");
|
||||
|
||||
const foundDevices = new Map<string, DeviceCandidate>();
|
||||
|
||||
Taro.onBluetoothDeviceFound((result) => {
|
||||
result.devices.forEach((item) => {
|
||||
const name = item.name || item.localName;
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
foundDevices.set(item.deviceId, {
|
||||
id: item.deviceId,
|
||||
name,
|
||||
source: "ble"
|
||||
});
|
||||
});
|
||||
|
||||
setBluetoothDevices(Array.from(foundDevices.values()).slice(0, 6));
|
||||
});
|
||||
|
||||
await Taro.startBluetoothDevicesDiscovery({
|
||||
allowDuplicatesKey: false
|
||||
});
|
||||
|
||||
discoveryTimerRef.current = setTimeout(async () => {
|
||||
const nextDevices = foundDevices.size > 0 ? Array.from(foundDevices.values()) : mockBluetoothDevices;
|
||||
|
||||
setBluetoothDevices(nextDevices);
|
||||
|
||||
if (nextDevices.length > 0) {
|
||||
setStatusHint("已找到附近设备,请点击列表完成绑定。");
|
||||
} else {
|
||||
setBluetoothStatus("empty");
|
||||
setStatusHint(bluetoothStateText.empty);
|
||||
}
|
||||
|
||||
await Taro.stopBluetoothDevicesDiscovery().catch(() => undefined);
|
||||
}, 2200);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "";
|
||||
|
||||
if (message.includes("not available") || message.includes("10001")) {
|
||||
showToast("请先开启系统蓝牙");
|
||||
setStatusHint("系统蓝牙未开启,暂时无法搜索设备。");
|
||||
} else {
|
||||
showToast("蓝牙搜索失败");
|
||||
setStatusHint("蓝牙搜索未成功启动,请稍后重试。");
|
||||
}
|
||||
|
||||
setBluetoothStatus("idle");
|
||||
setBluetoothDevices([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeviceSelect = async (device: DeviceCandidate) => {
|
||||
await bindDevice(device.name, device.source === "ble" ? "蓝牙设备" : "演示设备");
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="device-page">
|
||||
<View className="device-page__halo device-page__halo--large" />
|
||||
<View className="device-page__halo device-page__halo--small" />
|
||||
|
||||
<View className="device-header">
|
||||
<View className="device-header__login" onClick={handleLogin}>
|
||||
登录
|
||||
</View>
|
||||
|
||||
<View className="device-header__add" onClick={handleAdd}>
|
||||
+
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="device-summary" onClick={() => handleClick("设备列表页待接入")}>
|
||||
<View className="device-summary__content">
|
||||
<View className="device-summary" onClick={handleDeviceList}>
|
||||
<View className="device-summary__title-wrap">
|
||||
<Text className="device-summary__title">已关联体征监测设备</Text>
|
||||
<Text className="device-summary__count">0</Text>
|
||||
<Text className="device-summary__count">{deviceCount}</Text>
|
||||
</View>
|
||||
<Text className="device-summary__arrow">></Text>
|
||||
</View>
|
||||
|
||||
<View className="action-card">
|
||||
{quickActions.map((item) => (
|
||||
<View className="action-button" key={item.key} onClick={() => handleClick(item.toast)}>
|
||||
<View className={`action-button__icon action-button__icon--${item.key}`}>
|
||||
<View className="action-button__icon-core" />
|
||||
</View>
|
||||
<Text className="action-button__label">{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View className="notice-card">
|
||||
<View className="notice-card__title-row">
|
||||
<View className="notice-card__speaker" />
|
||||
<Text className="notice-card__title">尊敬的用户您好!绑定前请注意以下几点。</Text>
|
||||
<View className="device-actions-card">
|
||||
<View className="device-action" onClick={handleScanBind}>
|
||||
<View className="device-action__icon device-action__icon--scan" />
|
||||
<Text className="device-action__label">扫码 添加新设备</Text>
|
||||
</View>
|
||||
|
||||
<View className="notice-list">
|
||||
<View className="device-action device-action--secondary" onClick={handleBluetoothBind}>
|
||||
<View className="device-action__icon device-action__icon--bluetooth" />
|
||||
<Text className="device-action__label">蓝牙搜附近的设备</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="device-status-card">
|
||||
<View className="device-status-card__header">
|
||||
<Text className="device-status-card__title">绑定进度</Text>
|
||||
<Text className={`device-status-card__tag device-status-card__tag--${bluetoothStatus}`}>
|
||||
{bluetoothStatus === "searching" ? "搜索中" : bluetoothStatus === "empty" ? "无设备" : bluetoothStatus === "success" ? "成功" : "待开始"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text className="device-status-card__hint">{statusHint}</Text>
|
||||
<Text className="device-status-card__subhint">{bluetoothStateText[bluetoothStatus]}</Text>
|
||||
|
||||
{bluetoothDevices.length > 0 ? (
|
||||
<View className="device-list">
|
||||
{bluetoothDevices.map((item) => (
|
||||
<View className="device-list__item" key={item.id} onClick={() => handleDeviceSelect(item)}>
|
||||
<View>
|
||||
<Text className="device-list__name">{item.name}</Text>
|
||||
<Text className="device-list__meta">
|
||||
{item.source === "ble" ? "附近蓝牙设备" : "开发占位设备"}
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="device-list__action">绑定</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{recentDeviceName ? (
|
||||
<View className="device-success">
|
||||
<Text className="device-success__title">最近绑定</Text>
|
||||
<Text className="device-success__name">{recentDeviceName}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View className="device-notice-card">
|
||||
<View className="device-notice-card__title-row">
|
||||
<View className="device-notice-card__horn" />
|
||||
<Text className="device-notice-card__title">绑定前请注意以下几点:</Text>
|
||||
</View>
|
||||
|
||||
<View className="device-notice-card__list">
|
||||
{notices.map((item) => (
|
||||
<Text className="notice-item" key={item}>
|
||||
<Text className="device-notice-card__item" key={item}>
|
||||
{item}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="tab-bar">
|
||||
<View className="device-tabbar">
|
||||
{navItems.map((item) => (
|
||||
<View className="tab-item" key={item.key} onClick={() => handleClick(`${item.label}功能待接入`)}>
|
||||
<View className={`tab-item__icon ${item.active ? "tab-item__icon--active" : ""}`}>
|
||||
{item.badge ? <View className="tab-item__badge" /> : null}
|
||||
<View className="device-tabbar__item" key={item.key} onClick={() => handleTabClick(item.label)}>
|
||||
<View className={`device-tabbar__icon ${item.active ? "device-tabbar__icon--active" : ""}`}>
|
||||
{item.badge ? <View className="device-tabbar__badge" /> : null}
|
||||
</View>
|
||||
<Text className={`tab-item__label ${item.active ? "tab-item__label--active" : ""}`}>{item.label}</Text>
|
||||
<Text className={`device-tabbar__label ${item.active ? "device-tabbar__label--active" : ""}`}>{item.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user