diff --git a/AGENTS.md b/AGENTS.md index 244ad09..0e47161 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,26 @@ - 样式优先使用 `.scss`,但保持简单,避免过度抽象。 - 非必要不新增依赖,尤其避免在项目初期引入复杂状态管理、UI 大全家桶或重型工具链。 +## 主题与组件约定 + +- 颜色统一收口到 `src/styles/theme.scss`,优先使用 CSS 变量,不要在页面和组件里散落写十六进制颜色值。 +- 新增颜色时,先判断是否属于已有语义色: + - 品牌色 + - 页面背景色 + - 卡片背景色 + - 文字主次色 + - 警告色 + - 危险色 +- 如果某个颜色只在单个页面临时使用,也应优先评估是否值得提升为公共主题变量。 +- 通用 UI 优先拆到 `src/components/`,尤其是以下类型: + - 按钮 + - 卡片 + - 底部导航 + - 列表项 + - 状态块 +- 如果一个结构或样式在两个及以上页面/区域重复出现,优先抽成可复用组件,而不是复制粘贴。 +- 组件设计尽量保持“样式可配置、职责单一、命名清晰”,避免做过度复杂的大而全组件。 + ## 修改边界 - 可以新增或修改当前工作区内与本项目直接相关的文件。 @@ -52,3 +72,43 @@ - 每次新增关键功能、配置步骤或部署步骤时,优先同步更新 `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" diff --git a/README.md b/README.md index f58a044..2395436 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ 当前首页已经改成了一版“设备绑定首页(无设备状态)”业务样式,方便你直接在这个基础上继续接真实接口和页面跳转。 +项目里也已经开始整理公共能力: + +- 颜色统一收口到全局主题变量,后续页面尽量不要再写散落的十六进制颜色 +- 通用 UI 优先拆成可复用组件,方便后续多页面共用 + ## 1. 目前已经包含什么 - `Taro + React + TypeScript` 项目骨架 @@ -184,6 +189,18 @@ npm run dev:weapp ## 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) + ### 改页面文案和逻辑 编辑: diff --git a/src/app.scss b/src/app.scss index 7d16128..e614bd7 100644 --- a/src/app.scss +++ b/src/app.scss @@ -1,5 +1,7 @@ +@use "./styles/theme.scss"; + page { - background: #1b2130; - color: #f5f7fb; + background: var(--color-bg-page); + color: var(--color-text-primary); font-size: 32rpx; } diff --git a/src/components/action-button/index.scss b/src/components/action-button/index.scss new file mode 100644 index 0000000..d0ccb3f --- /dev/null +++ b/src/components/action-button/index.scss @@ -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; +} diff --git a/src/components/action-button/index.tsx b/src/components/action-button/index.tsx new file mode 100644 index 0000000..596e907 --- /dev/null +++ b/src/components/action-button/index.tsx @@ -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 ( + + + {label} + + ); +} diff --git a/src/components/bottom-tabbar/index.scss b/src/components/bottom-tabbar/index.scss new file mode 100644 index 0000000..dba216f --- /dev/null +++ b/src/components/bottom-tabbar/index.scss @@ -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); +} diff --git a/src/components/bottom-tabbar/index.tsx b/src/components/bottom-tabbar/index.tsx new file mode 100644 index 0000000..055c0dd --- /dev/null +++ b/src/components/bottom-tabbar/index.tsx @@ -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 ( + + {items.map((item) => ( + onItemClick(item)}> + + {item.badge ? : null} + + {item.label} + + ))} + + ); +} diff --git a/src/components/panel-card/index.scss b/src/components/panel-card/index.scss new file mode 100644 index 0000000..7049e7d --- /dev/null +++ b/src/components/panel-card/index.scss @@ -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); +} diff --git a/src/components/panel-card/index.tsx b/src/components/panel-card/index.tsx new file mode 100644 index 0000000..5857e25 --- /dev/null +++ b/src/components/panel-card/index.tsx @@ -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 {children}; +} diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss index 62c4826..2eb104b 100644 --- a/src/pages/index/index.scss +++ b/src/pages/index/index.scss @@ -3,7 +3,7 @@ min-height: 100vh; padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx; 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; } @@ -34,33 +34,37 @@ .device-header { position: relative; z-index: 1; - display: flex; - align-items: center; - justify-content: space-between; + width: 100%; 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 { + position: absolute; + left: 0; + top: calc(var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx); min-width: 92rpx; - height: 48rpx; + height: var(--menu-height, 32px); padding: 0 24rpx; border-radius: 999rpx; - background: linear-gradient(90deg, #35e5b3 0%, #20c9bf 100%); - color: #ffffff; + background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%); + color: var(--color-text-white); font-size: 22rpx; - line-height: 48rpx; + line-height: var(--menu-height, 32px); 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 { + 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; height: 36rpx; - margin-top: 28rpx; - border: 2rpx solid rgba(255, 255, 255, 0.86); + border: 2rpx solid var(--color-border-strong); border-radius: 50%; - color: #ffffff; + color: var(--color-text-white); font-size: 30rpx; line-height: 30rpx; text-align: center; @@ -72,7 +76,9 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 22rpx; + width: 100%; + height: 90rpx; + margin-bottom: 18rpx; } .device-summary__title-wrap { @@ -82,108 +88,29 @@ } .device-summary__title { - color: #f3f7ff; + color: var(--color-text-title); font-size: 28rpx; font-weight: 600; } .device-summary__count { - color: #ff964f; + color: var(--color-text-highlight); font-size: 26rpx; font-weight: 700; } .device-summary__arrow { - color: #8a93a7; + color: var(--color-text-muted); 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 { 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 { - position: relative; - z-index: 1; margin-top: 22rpx; 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 { @@ -198,7 +125,7 @@ height: 16rpx; margin-right: 12rpx; border-radius: 4rpx; - background: #ff9d57; + background: var(--color-warning-main); } .device-notice-card__horn::before, @@ -214,7 +141,7 @@ height: 0; border-top: 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 { @@ -222,14 +149,14 @@ top: 2rpx; width: 8rpx; height: 8rpx; - border-top: 2rpx solid #ff9d57; - border-right: 2rpx solid #ff9d57; + border-top: 2rpx solid var(--color-warning-main); + border-right: 2rpx solid var(--color-warning-main); border-radius: 0 8rpx 0 0; transform: rotate(45deg); } .device-notice-card__title { - color: #af7e42; + color: var(--color-text-warning); font-size: 22rpx; font-weight: 600; } @@ -241,94 +168,7 @@ } .device-notice-card__item { - color: #9f7a4c; + color: var(--color-text-warning-secondary); font-size: 22rpx; 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; -} diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 276668b..81e630e 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -2,6 +2,9 @@ import { Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; import type { CSSProperties } 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"; type BluetoothStatus = "idle" | "searching" | "empty" | "success"; @@ -18,7 +21,7 @@ const notices = [ "3. 扫码功能需开启相机权限" ]; -const navItems = [ +const navItems: TabbarItem[] = [ { key: "home", label: "首页", active: true }, { key: "report", label: "报告", active: false }, { key: "assistant", label: "小e", active: false }, @@ -55,6 +58,8 @@ export default function Index() { const [bluetoothStatus, setBluetoothStatus] = useState("idle"); const [topSafeHeight, setTopSafeHeight] = useState(0); const [menuSafeWidth, setMenuSafeWidth] = useState(0); + const [menuTop, setMenuTop] = useState(0); + const [menuHeight, setMenuHeight] = useState(0); const discoveryTimerRef = useRef | null>(null); useEffect(() => { @@ -66,6 +71,8 @@ export default function Index() { setTopSafeHeight(safeTop); setMenuSafeWidth(safeRight); + setMenuTop(menuButtonRect?.top || Math.max((windowInfo.statusBarHeight || 0) + 6, 0)); + setMenuHeight(menuButtonRect?.height || 32); return () => { if (discoveryTimerRef.current) { @@ -240,7 +247,9 @@ export default function Index() { const pageStyle = { "--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; return ( @@ -266,19 +275,12 @@ export default function Index() { > - - - - 扫码 添加新设备 - + + + + - - - 蓝牙搜附近的设备 - - - - + 绑定前请注意以下几点: @@ -291,18 +293,9 @@ export default function Index() { ))} - + - - {navItems.map((item) => ( - handleTabClick(item.label)}> - - {item.badge ? : null} - - {item.label} - - ))} - + handleTabClick(item.label)} /> ); } diff --git a/src/styles/theme.scss b/src/styles/theme.scss new file mode 100644 index 0000000..6d9d2ae --- /dev/null +++ b/src/styles/theme.scss @@ -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); +}