feat: extract theme tokens and reusable ui components
This commit is contained in:
60
AGENTS.md
60
AGENTS.md
@@ -34,6 +34,26 @@
|
|||||||
- 样式优先使用 `.scss`,但保持简单,避免过度抽象。
|
- 样式优先使用 `.scss`,但保持简单,避免过度抽象。
|
||||||
- 非必要不新增依赖,尤其避免在项目初期引入复杂状态管理、UI 大全家桶或重型工具链。
|
- 非必要不新增依赖,尤其避免在项目初期引入复杂状态管理、UI 大全家桶或重型工具链。
|
||||||
|
|
||||||
|
## 主题与组件约定
|
||||||
|
|
||||||
|
- 颜色统一收口到 `src/styles/theme.scss`,优先使用 CSS 变量,不要在页面和组件里散落写十六进制颜色值。
|
||||||
|
- 新增颜色时,先判断是否属于已有语义色:
|
||||||
|
- 品牌色
|
||||||
|
- 页面背景色
|
||||||
|
- 卡片背景色
|
||||||
|
- 文字主次色
|
||||||
|
- 警告色
|
||||||
|
- 危险色
|
||||||
|
- 如果某个颜色只在单个页面临时使用,也应优先评估是否值得提升为公共主题变量。
|
||||||
|
- 通用 UI 优先拆到 `src/components/`,尤其是以下类型:
|
||||||
|
- 按钮
|
||||||
|
- 卡片
|
||||||
|
- 底部导航
|
||||||
|
- 列表项
|
||||||
|
- 状态块
|
||||||
|
- 如果一个结构或样式在两个及以上页面/区域重复出现,优先抽成可复用组件,而不是复制粘贴。
|
||||||
|
- 组件设计尽量保持“样式可配置、职责单一、命名清晰”,避免做过度复杂的大而全组件。
|
||||||
|
|
||||||
## 修改边界
|
## 修改边界
|
||||||
|
|
||||||
- 可以新增或修改当前工作区内与本项目直接相关的文件。
|
- 可以新增或修改当前工作区内与本项目直接相关的文件。
|
||||||
@@ -52,3 +72,43 @@
|
|||||||
- 每次新增关键功能、配置步骤或部署步骤时,优先同步更新 `README.md`。
|
- 每次新增关键功能、配置步骤或部署步骤时,优先同步更新 `README.md`。
|
||||||
- 文档写法以“零基础可照做”为标准,避免只写结论不写路径。
|
- 文档写法以“零基础可照做”为标准,避免只写结论不写路径。
|
||||||
- 如果修改了 Taro 的开发、编译、预览或发布流程,必须更新 `README.md` 中对应命令和目录说明。
|
- 如果修改了 Taro 的开发、编译、预览或发布流程,必须更新 `README.md` 中对应命令和目录说明。
|
||||||
|
|
||||||
|
## 原始设计色板参考
|
||||||
|
|
||||||
|
以下颜色可作为设计来源参考,但在代码中应优先映射为 `src/styles/theme.scss` 中的语义化变量,而不是直接把 `color1`、`color2` 这类名称搬进业务代码。
|
||||||
|
|
||||||
|
color1: '#45D989',
|
||||||
|
color2: "#00C1AA",
|
||||||
|
color3: "#333333",
|
||||||
|
color4: "#D3D3D3",
|
||||||
|
color6: "#FBF5D5",
|
||||||
|
color5: "#FFFFF006",
|
||||||
|
color7: "#00C1AA",
|
||||||
|
color8: "#FF9F66",
|
||||||
|
color9: "#FF7159",
|
||||||
|
color10: "#E60012",
|
||||||
|
color11: "#00C1AA",
|
||||||
|
color12: "#10CFF1",
|
||||||
|
color13: "#FF9F66",
|
||||||
|
color14: "#FF7159",
|
||||||
|
color15: "#F6F6F6",
|
||||||
|
color16: "#333333",
|
||||||
|
color17: "#FFFFFF",
|
||||||
|
color18: "#FFFFFF",
|
||||||
|
color19: "#FFFFFF",
|
||||||
|
color20: "#f7f8fa",
|
||||||
|
color21: "#eaeaea",
|
||||||
|
color22: "#eaeaea",
|
||||||
|
color25: "#FF7159",
|
||||||
|
color26: "#4AD8FA",
|
||||||
|
color27: "#f7f8fa",
|
||||||
|
color28: "#4E8408",
|
||||||
|
color29: "#79BC31",
|
||||||
|
color30: "#E55E92",
|
||||||
|
color31: "#FF1D25",
|
||||||
|
color32: "#7bbb33",
|
||||||
|
color33: "#fe15b8d",
|
||||||
|
color34: "#EE0000",
|
||||||
|
color38: "#E3E4E5",
|
||||||
|
color39: "#F3F5F6",
|
||||||
|
color40: "#333333"
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
当前首页已经改成了一版“设备绑定首页(无设备状态)”业务样式,方便你直接在这个基础上继续接真实接口和页面跳转。
|
当前首页已经改成了一版“设备绑定首页(无设备状态)”业务样式,方便你直接在这个基础上继续接真实接口和页面跳转。
|
||||||
|
|
||||||
|
项目里也已经开始整理公共能力:
|
||||||
|
|
||||||
|
- 颜色统一收口到全局主题变量,后续页面尽量不要再写散落的十六进制颜色
|
||||||
|
- 通用 UI 优先拆成可复用组件,方便后续多页面共用
|
||||||
|
|
||||||
## 1. 目前已经包含什么
|
## 1. 目前已经包含什么
|
||||||
|
|
||||||
- `Taro + React + TypeScript` 项目骨架
|
- `Taro + React + TypeScript` 项目骨架
|
||||||
@@ -184,6 +189,18 @@ npm run dev:weapp
|
|||||||
|
|
||||||
## 13. 你接下来最常做的开发动作
|
## 13. 你接下来最常做的开发动作
|
||||||
|
|
||||||
|
### 改公共主题色
|
||||||
|
|
||||||
|
编辑:
|
||||||
|
|
||||||
|
- [src/styles/theme.scss](C:/Users/a/Documents/New%20project%203/src/styles/theme.scss)
|
||||||
|
|
||||||
|
### 改通用组件
|
||||||
|
|
||||||
|
编辑:
|
||||||
|
|
||||||
|
- [src/components](C:/Users/a/Documents/New%20project%203/src/components)
|
||||||
|
|
||||||
### 改页面文案和逻辑
|
### 改页面文案和逻辑
|
||||||
|
|
||||||
编辑:
|
编辑:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
@use "./styles/theme.scss";
|
||||||
|
|
||||||
page {
|
page {
|
||||||
background: #1b2130;
|
background: var(--color-bg-page);
|
||||||
color: #f5f7fb;
|
color: var(--color-text-primary);
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/components/action-button/index.scss
Normal file
65
src/components/action-button/index.scss
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
.action-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 74rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%);
|
||||||
|
box-shadow: 0 14rpx 26rpx var(--color-brand-shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button--secondary {
|
||||||
|
margin-top: 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon {
|
||||||
|
position: relative;
|
||||||
|
width: 32rpx;
|
||||||
|
height: 32rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon::before,
|
||||||
|
.action-button__icon::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon--scan::before {
|
||||||
|
inset: 4rpx;
|
||||||
|
border: 2rpx solid var(--color-text-white);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon--scan::after {
|
||||||
|
left: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
top: 15rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
background: var(--color-text-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon--bluetooth::before {
|
||||||
|
left: 14rpx;
|
||||||
|
top: 2rpx;
|
||||||
|
width: 2rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: var(--color-text-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__icon--bluetooth::after {
|
||||||
|
left: 8rpx;
|
||||||
|
top: 6rpx;
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
border-top: 2rpx solid var(--color-text-white);
|
||||||
|
border-right: 2rpx solid var(--color-text-white);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button__label {
|
||||||
|
color: var(--color-text-white);
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 1rpx;
|
||||||
|
}
|
||||||
18
src/components/action-button/index.tsx
Normal file
18
src/components/action-button/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Text, View } from "@tarojs/components";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
type ActionButtonProps = {
|
||||||
|
icon: "scan" | "bluetooth";
|
||||||
|
label: string;
|
||||||
|
secondary?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ActionButton({ icon, label, secondary = false, onClick }: ActionButtonProps) {
|
||||||
|
return (
|
||||||
|
<View className={`action-button ${secondary ? "action-button--secondary" : ""}`} onClick={onClick}>
|
||||||
|
<View className={`action-button__icon action-button__icon--${icon}`} />
|
||||||
|
<Text className="action-button__label">{label}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
86
src/components/bottom-tabbar/index.scss
Normal file
86
src/components/bottom-tabbar/index.scss
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
.bottom-tabbar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 16rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
background: var(--color-bg-tabbar);
|
||||||
|
box-shadow: var(--shadow-tabbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__item {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 88rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon {
|
||||||
|
position: relative;
|
||||||
|
width: 34rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx solid var(--color-border-tab);
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon::before,
|
||||||
|
.bottom-tabbar__icon::after {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon::before {
|
||||||
|
left: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
top: 10rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
background: currentColor;
|
||||||
|
color: var(--color-border-tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon::after {
|
||||||
|
left: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
bottom: 10rpx;
|
||||||
|
height: 2rpx;
|
||||||
|
background: currentColor;
|
||||||
|
color: var(--color-border-tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon--active {
|
||||||
|
border-color: var(--color-text-accent);
|
||||||
|
background: var(--color-bg-accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__icon--active::before,
|
||||||
|
.bottom-tabbar__icon--active::after {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -4rpx;
|
||||||
|
right: -4rpx;
|
||||||
|
width: 10rpx;
|
||||||
|
height: 10rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-danger);
|
||||||
|
box-shadow: 0 0 0 4rpx var(--color-bg-tabbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-tabbar__label--active {
|
||||||
|
color: var(--color-text-accent);
|
||||||
|
}
|
||||||
29
src/components/bottom-tabbar/index.tsx
Normal file
29
src/components/bottom-tabbar/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Text, View } from "@tarojs/components";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
export type TabbarItem = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
badge?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BottomTabbarProps = {
|
||||||
|
items: TabbarItem[];
|
||||||
|
onItemClick: (item: TabbarItem) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BottomTabbar({ items, onItemClick }: BottomTabbarProps) {
|
||||||
|
return (
|
||||||
|
<View className="bottom-tabbar">
|
||||||
|
{items.map((item) => (
|
||||||
|
<View className="bottom-tabbar__item" key={item.key} onClick={() => onItemClick(item)}>
|
||||||
|
<View className={`bottom-tabbar__icon ${item.active ? "bottom-tabbar__icon--active" : ""}`}>
|
||||||
|
{item.badge ? <View className="bottom-tabbar__badge" /> : null}
|
||||||
|
</View>
|
||||||
|
<Text className={`bottom-tabbar__label ${item.active ? "bottom-tabbar__label--active" : ""}`}>{item.label}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/components/panel-card/index.scss
Normal file
13
src/components/panel-card/index.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
.panel-card {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: var(--color-bg-surface);
|
||||||
|
box-shadow: inset 0 0 0 2rpx var(--color-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-card--warning {
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background: var(--color-bg-warning);
|
||||||
|
box-shadow: var(--shadow-surface);
|
||||||
|
}
|
||||||
13
src/components/panel-card/index.tsx
Normal file
13
src/components/panel-card/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { View } from "@tarojs/components";
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import "./index.scss";
|
||||||
|
|
||||||
|
type PanelCardProps = PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
warning?: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export default function PanelCard({ className = "", warning = false, children }: PanelCardProps) {
|
||||||
|
const classes = ["panel-card", warning ? "panel-card--warning" : "", className].filter(Boolean).join(" ");
|
||||||
|
return <View className={classes}>{children}</View>;
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx;
|
padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, #1e2432 0%, #191f2c 100%);
|
background: linear-gradient(180deg, var(--color-bg-page-gradient-start) 0%, var(--color-bg-page-gradient-end) 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,33 +34,37 @@
|
|||||||
.device-header {
|
.device-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-right: calc(var(--menu-safe-width, 0px) + 12rpx);
|
padding-right: calc(var(--menu-safe-width, 0px) + 12rpx);
|
||||||
margin-bottom: 26rpx;
|
margin-bottom: 8rpx;
|
||||||
|
min-height: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 36rpx + 6rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header__login {
|
.device-header__login {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: calc(var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx);
|
||||||
min-width: 92rpx;
|
min-width: 92rpx;
|
||||||
height: 48rpx;
|
height: var(--menu-height, 32px);
|
||||||
padding: 0 24rpx;
|
padding: 0 24rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: linear-gradient(90deg, #35e5b3 0%, #20c9bf 100%);
|
background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%);
|
||||||
color: #ffffff;
|
color: var(--color-text-white);
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
line-height: 48rpx;
|
line-height: var(--menu-height, 32px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 12rpx 24rpx rgba(32, 214, 181, 0.22);
|
box-shadow: 0 12rpx 24rpx var(--color-brand-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header__add {
|
.device-header__add {
|
||||||
|
position: absolute;
|
||||||
|
right: calc(var(--menu-safe-width, 0px) + 2rpx);
|
||||||
|
top: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 6rpx);
|
||||||
width: 36rpx;
|
width: 36rpx;
|
||||||
height: 36rpx;
|
height: 36rpx;
|
||||||
margin-top: 28rpx;
|
border: 2rpx solid var(--color-border-strong);
|
||||||
border: 2rpx solid rgba(255, 255, 255, 0.86);
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
color: #ffffff;
|
color: var(--color-text-white);
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
line-height: 30rpx;
|
line-height: 30rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -72,7 +76,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 22rpx;
|
width: 100%;
|
||||||
|
height: 90rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-summary__title-wrap {
|
.device-summary__title-wrap {
|
||||||
@@ -82,108 +88,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-summary__title {
|
.device-summary__title {
|
||||||
color: #f3f7ff;
|
color: var(--color-text-title);
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-summary__count {
|
.device-summary__count {
|
||||||
color: #ff964f;
|
color: var(--color-text-highlight);
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-summary__arrow {
|
.device-summary__arrow {
|
||||||
color: #8a93a7;
|
color: var(--color-text-muted);
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-actions-card {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
background: rgba(42, 48, 66, 0.96);
|
|
||||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-actions-card {
|
.device-actions-card {
|
||||||
padding: 34rpx 30rpx;
|
padding: 34rpx 30rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-action {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 74rpx;
|
|
||||||
border-radius: 999rpx;
|
|
||||||
background: linear-gradient(90deg, #39e6ad 0%, #1fc9c1 100%);
|
|
||||||
box-shadow: 0 14rpx 26rpx rgba(20, 184, 166, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-action--secondary {
|
|
||||||
margin-top: 34rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-action__icon {
|
|
||||||
position: relative;
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
margin-right: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-action__icon::before,
|
|
||||||
.device-action__icon::after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-action__icon--bluetooth::before {
|
|
||||||
left: 14rpx;
|
|
||||||
top: 2rpx;
|
|
||||||
width: 2rpx;
|
|
||||||
height: 28rpx;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-action__label {
|
|
||||||
color: #ffffff;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-notice-card {
|
.device-notice-card {
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
margin-top: 22rpx;
|
margin-top: 22rpx;
|
||||||
padding: 22rpx 24rpx 24rpx;
|
padding: 22rpx 24rpx 24rpx;
|
||||||
border-radius: 22rpx;
|
|
||||||
background: #fbefc7;
|
|
||||||
box-shadow: 0 12rpx 24rpx rgba(6, 10, 22, 0.14);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-notice-card__title-row {
|
.device-notice-card__title-row {
|
||||||
@@ -198,7 +125,7 @@
|
|||||||
height: 16rpx;
|
height: 16rpx;
|
||||||
margin-right: 12rpx;
|
margin-right: 12rpx;
|
||||||
border-radius: 4rpx;
|
border-radius: 4rpx;
|
||||||
background: #ff9d57;
|
background: var(--color-warning-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-notice-card__horn::before,
|
.device-notice-card__horn::before,
|
||||||
@@ -214,7 +141,7 @@
|
|||||||
height: 0;
|
height: 0;
|
||||||
border-top: 6rpx solid transparent;
|
border-top: 6rpx solid transparent;
|
||||||
border-bottom: 6rpx solid transparent;
|
border-bottom: 6rpx solid transparent;
|
||||||
border-left: 8rpx solid #ff9d57;
|
border-left: 8rpx solid var(--color-warning-main);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-notice-card__horn::after {
|
.device-notice-card__horn::after {
|
||||||
@@ -222,14 +149,14 @@
|
|||||||
top: 2rpx;
|
top: 2rpx;
|
||||||
width: 8rpx;
|
width: 8rpx;
|
||||||
height: 8rpx;
|
height: 8rpx;
|
||||||
border-top: 2rpx solid #ff9d57;
|
border-top: 2rpx solid var(--color-warning-main);
|
||||||
border-right: 2rpx solid #ff9d57;
|
border-right: 2rpx solid var(--color-warning-main);
|
||||||
border-radius: 0 8rpx 0 0;
|
border-radius: 0 8rpx 0 0;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-notice-card__title {
|
.device-notice-card__title {
|
||||||
color: #af7e42;
|
color: var(--color-text-warning);
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -241,94 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-notice-card__item {
|
.device-notice-card__item {
|
||||||
color: #9f7a4c;
|
color: var(--color-text-warning-secondary);
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-tabbar {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 5;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding: 16rpx 24rpx calc(20rpx + env(safe-area-inset-bottom));
|
|
||||||
background: rgba(30, 35, 48, 0.98);
|
|
||||||
box-shadow: 0 -8rpx 24rpx rgba(4, 8, 20, 0.34);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__item {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
min-width: 88rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon {
|
|
||||||
position: relative;
|
|
||||||
width: 34rpx;
|
|
||||||
height: 34rpx;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
border: 2rpx solid #8f97a9;
|
|
||||||
opacity: 0.86;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon::before,
|
|
||||||
.device-tabbar__icon::after {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon::before {
|
|
||||||
left: 8rpx;
|
|
||||||
right: 8rpx;
|
|
||||||
top: 10rpx;
|
|
||||||
height: 2rpx;
|
|
||||||
background: currentColor;
|
|
||||||
color: #8f97a9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon::after {
|
|
||||||
left: 8rpx;
|
|
||||||
right: 8rpx;
|
|
||||||
bottom: 10rpx;
|
|
||||||
height: 2rpx;
|
|
||||||
background: currentColor;
|
|
||||||
color: #8f97a9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon--active {
|
|
||||||
border-color: #36e4aa;
|
|
||||||
background: rgba(54, 228, 170, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__icon--active::before,
|
|
||||||
.device-tabbar__icon--active::after {
|
|
||||||
color: #36e4aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -4rpx;
|
|
||||||
right: -4rpx;
|
|
||||||
width: 10rpx;
|
|
||||||
height: 10rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #ff4d4f;
|
|
||||||
box-shadow: 0 0 0 4rpx rgba(30, 35, 48, 0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__label {
|
|
||||||
color: #b0b6c4;
|
|
||||||
font-size: 20rpx;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.device-tabbar__label--active {
|
|
||||||
color: #36e4aa;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { Text, View } from "@tarojs/components";
|
|||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import ActionButton from "../../components/action-button";
|
||||||
|
import BottomTabbar, { type TabbarItem } from "../../components/bottom-tabbar";
|
||||||
|
import PanelCard from "../../components/panel-card";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
|
||||||
type BluetoothStatus = "idle" | "searching" | "empty" | "success";
|
type BluetoothStatus = "idle" | "searching" | "empty" | "success";
|
||||||
@@ -18,7 +21,7 @@ const notices = [
|
|||||||
"3. 扫码功能需开启相机权限"
|
"3. 扫码功能需开启相机权限"
|
||||||
];
|
];
|
||||||
|
|
||||||
const navItems = [
|
const navItems: TabbarItem[] = [
|
||||||
{ key: "home", label: "首页", active: true },
|
{ key: "home", label: "首页", active: true },
|
||||||
{ key: "report", label: "报告", active: false },
|
{ key: "report", label: "报告", active: false },
|
||||||
{ key: "assistant", label: "小e", active: false },
|
{ key: "assistant", label: "小e", active: false },
|
||||||
@@ -55,6 +58,8 @@ export default function Index() {
|
|||||||
const [bluetoothStatus, setBluetoothStatus] = useState<BluetoothStatus>("idle");
|
const [bluetoothStatus, setBluetoothStatus] = useState<BluetoothStatus>("idle");
|
||||||
const [topSafeHeight, setTopSafeHeight] = useState(0);
|
const [topSafeHeight, setTopSafeHeight] = useState(0);
|
||||||
const [menuSafeWidth, setMenuSafeWidth] = useState(0);
|
const [menuSafeWidth, setMenuSafeWidth] = useState(0);
|
||||||
|
const [menuTop, setMenuTop] = useState(0);
|
||||||
|
const [menuHeight, setMenuHeight] = useState(0);
|
||||||
const discoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const discoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -66,6 +71,8 @@ export default function Index() {
|
|||||||
|
|
||||||
setTopSafeHeight(safeTop);
|
setTopSafeHeight(safeTop);
|
||||||
setMenuSafeWidth(safeRight);
|
setMenuSafeWidth(safeRight);
|
||||||
|
setMenuTop(menuButtonRect?.top || Math.max((windowInfo.statusBarHeight || 0) + 6, 0));
|
||||||
|
setMenuHeight(menuButtonRect?.height || 32);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (discoveryTimerRef.current) {
|
if (discoveryTimerRef.current) {
|
||||||
@@ -240,7 +247,9 @@ export default function Index() {
|
|||||||
|
|
||||||
const pageStyle = {
|
const pageStyle = {
|
||||||
"--top-safe-height": `${topSafeHeight}px`,
|
"--top-safe-height": `${topSafeHeight}px`,
|
||||||
"--menu-safe-width": `${menuSafeWidth}px`
|
"--menu-safe-width": `${menuSafeWidth}px`,
|
||||||
|
"--menu-top": `${menuTop}px`,
|
||||||
|
"--menu-height": `${menuHeight}px`
|
||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -266,19 +275,12 @@ export default function Index() {
|
|||||||
<Text className="device-summary__arrow">></Text>
|
<Text className="device-summary__arrow">></Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="device-actions-card">
|
<PanelCard className="device-actions-card">
|
||||||
<View className="device-action" onClick={handleScanBind}>
|
<ActionButton icon="scan" label="扫码 添加新设备" onClick={handleScanBind} />
|
||||||
<View className="device-action__icon device-action__icon--scan" />
|
<ActionButton icon="bluetooth" label="蓝牙搜附近的设备" secondary onClick={handleBluetoothBind} />
|
||||||
<Text className="device-action__label">扫码 添加新设备</Text>
|
</PanelCard>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="device-action device-action--secondary" onClick={handleBluetoothBind}>
|
<PanelCard className="device-notice-card" warning>
|
||||||
<View className="device-action__icon device-action__icon--bluetooth" />
|
|
||||||
<Text className="device-action__label">蓝牙搜附近的设备</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="device-notice-card">
|
|
||||||
<View className="device-notice-card__title-row">
|
<View className="device-notice-card__title-row">
|
||||||
<View className="device-notice-card__horn" />
|
<View className="device-notice-card__horn" />
|
||||||
<Text className="device-notice-card__title">绑定前请注意以下几点:</Text>
|
<Text className="device-notice-card__title">绑定前请注意以下几点:</Text>
|
||||||
@@ -291,18 +293,9 @@ export default function Index() {
|
|||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</PanelCard>
|
||||||
|
|
||||||
<View className="device-tabbar">
|
<BottomTabbar items={navItems} onItemClick={(item) => handleTabClick(item.label)} />
|
||||||
{navItems.map((item) => (
|
|
||||||
<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={`device-tabbar__label ${item.active ? "device-tabbar__label--active" : ""}`}>{item.label}</Text>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
src/styles/theme.scss
Normal file
35
src/styles/theme.scss
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
:root,
|
||||||
|
page {
|
||||||
|
--color-bg-page: #1b2130;
|
||||||
|
--color-bg-page-gradient-start: #1e2432;
|
||||||
|
--color-bg-page-gradient-end: #191f2c;
|
||||||
|
--color-bg-surface: rgba(42, 48, 66, 0.96);
|
||||||
|
--color-bg-tabbar: rgba(30, 35, 48, 0.98);
|
||||||
|
--color-bg-accent-soft: rgba(54, 228, 170, 0.12);
|
||||||
|
--color-bg-warning: #fbefc7;
|
||||||
|
|
||||||
|
--color-text-primary: #f5f7fb;
|
||||||
|
--color-text-title: #f3f7ff;
|
||||||
|
--color-text-secondary: #b0b6c4;
|
||||||
|
--color-text-muted: #8a93a7;
|
||||||
|
--color-text-white: #ffffff;
|
||||||
|
--color-text-accent: #36e4aa;
|
||||||
|
--color-text-warning: #af7e42;
|
||||||
|
--color-text-warning-secondary: #9f7a4c;
|
||||||
|
--color-text-highlight: #ff964f;
|
||||||
|
|
||||||
|
--color-border-light: rgba(255, 255, 255, 0.03);
|
||||||
|
--color-border-strong: rgba(255, 255, 255, 0.86);
|
||||||
|
--color-border-tab: #8f97a9;
|
||||||
|
|
||||||
|
--color-brand-start: #35e5b3;
|
||||||
|
--color-brand-end: #20c9bf;
|
||||||
|
--color-brand-shadow: rgba(32, 214, 181, 0.22);
|
||||||
|
--color-brand-shadow-soft: rgba(20, 184, 166, 0.18);
|
||||||
|
|
||||||
|
--color-warning-main: #ff9d57;
|
||||||
|
--color-danger: #ff4d4f;
|
||||||
|
|
||||||
|
--shadow-surface: 0 12rpx 24rpx rgba(6, 10, 22, 0.14);
|
||||||
|
--shadow-tabbar: 0 -8rpx 24rpx rgba(4, 8, 20, 0.34);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user