diff --git a/AGENTS.md b/AGENTS.md index 0e47161..7d25a66 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ - 保持项目结构简单、易懂、易修改 - 使用 `Taro + React + TypeScript` 作为核心技术栈 +- 明确这是“微信小程序”项目,不是原生 App 或 H5 项目 - 所有说明尽量使用中文 - 在保证可运行的前提下,减少复杂依赖 @@ -51,9 +52,27 @@ - 底部导航 - 列表项 - 状态块 +- 图标使用保持语义统一: + - 如果页面有“返回上一级页面”操作,优先使用 `back.svg` + - 如果页面有“加号/新增”操作,优先使用 `add.svg` - 如果一个结构或样式在两个及以上页面/区域重复出现,优先抽成可复用组件,而不是复制粘贴。 - 组件设计尽量保持“样式可配置、职责单一、命名清晰”,避免做过度复杂的大而全组件。 +## 小程序设计适配约定 + +- 本项目是微信小程序,后续收到的设计稿即使来自 App,也不能直接按 App 页面生搬硬套。 +- 设计稿如果是 App 视觉稿,落地时必须同时考虑: + - 手机系统状态栏安全区 + - 微信小程序原生胶囊按钮区域 + - 微信小程序页面顶部 appbar 的可用空间 +- 顶部按钮、标题、搜索框、头像、返回区等靠近页面顶部的元素,优先基于小程序原生 `statusBarHeight`、`getMenuButtonBoundingClientRect()` 等信息定位,而不是只按设计稿静态像素值摆放。 +- 当 App 设计稿顶部结构与微信小程序原生导航区域冲突时,优先保证小程序可用性和对齐关系,再尽量还原设计视觉。 +- 页面评审时,顶部区域要重点检查这几项: + - 是否遮挡原生胶囊 + - 是否与原生胶囊水平对齐 + - 是否预留了不同机型的顶部安全区 + - 是否在真机上仍然成立 + ## 修改边界 - 可以新增或修改当前工作区内与本项目直接相关的文件。 diff --git a/scripts/tests/app-bar-source.test.cjs b/scripts/tests/app-bar-source.test.cjs new file mode 100644 index 0000000..4ac0d0b --- /dev/null +++ b/scripts/tests/app-bar-source.test.cjs @@ -0,0 +1,40 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const appBarSource = fs.readFileSync( + path.join(__dirname, "../../src/components/app-bar/index.tsx"), + "utf8" +); +const appBarStyles = fs.readFileSync( + path.join(__dirname, "../../src/components/app-bar/index.scss"), + "utf8" +); + +function run(name, fn) { + try { + fn(); + console.log(`PASS ${name}`); + } catch (error) { + console.error(`FAIL ${name}`); + throw error; + } +} + +run("AppBar back action uses the shared back.svg asset", () => { + assert.match(appBarSource, /back\.svg/); + assert.match(appBarSource, / { + assert.doesNotMatch(appBarSource, /<|>\s*<\s* { + assert.match(appBarStyles, /\.app-bar__back-icon\s*\{[\s\S]*width:\s*32rpx;/); + assert.match(appBarStyles, /\.app-bar__back-icon\s*\{[\s\S]*height:\s*32rpx;/); +}); + +run("AppBar props stay focused on title and back navigation only", () => { + assert.doesNotMatch(appBarSource, /subtitle\?:|eyebrow\?:|align\?:|rightText\?:|onRightAction\?:|leftSlot\?:|rightSlot\?:|bottomSlot\?:/); +}); diff --git a/scripts/tests/message-page-source.test.cjs b/scripts/tests/message-page-source.test.cjs new file mode 100644 index 0000000..63fd9e1 --- /dev/null +++ b/scripts/tests/message-page-source.test.cjs @@ -0,0 +1,45 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); + +const messagePageStyles = fs.readFileSync( + path.join(__dirname, "../../src/pages/message/index.scss"), + "utf8" +); +const messagePageSource = fs.readFileSync( + path.join(__dirname, "../../src/pages/message/index.tsx"), + "utf8" +); + +function run(name, fn) { + try { + fn(); + console.log(`PASS ${name}`); + } catch (error) { + console.error(`FAIL ${name}`); + throw error; + } +} + +run("message tabs container spans full width without manual gap spacing", () => { + assert.match(messagePageStyles, /\.message-tabs\s*\{[\s\S]*width:\s*100%;/); + assert.doesNotMatch(messagePageStyles, /\.message-tabs\s*\{[\s\S]*gap:\s*110rpx;/); +}); + +run("each message tab item takes half of the row", () => { + assert.match(messagePageStyles, /\.message-tabs__item\s*\{[\s\S]*flex:\s*1;/); + assert.match(messagePageStyles, /\.message-tabs__item\s*\{[\s\S]*min-width:\s*0;/); +}); + +run("message tab label and unread dot are grouped in a centered content wrapper", () => { + assert.match(messagePageSource, /className="message-tabs__content"/); + assert.match(messagePageStyles, /\.message-tabs__content\s*\{[\s\S]*display:\s*inline-flex;/); +}); + +run("message tabs are rendered below AppBar instead of inside bottomSlot", () => { + assert.doesNotMatch(messagePageSource, /bottomSlot=\{/); + assert.match( + messagePageSource, + /\s*/ + ); +}); diff --git a/src/assets/svg/add.svg b/src/assets/svg/add.svg new file mode 100644 index 0000000..f29ed3f --- /dev/null +++ b/src/assets/svg/add.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/svg/back.svg b/src/assets/svg/back.svg new file mode 100644 index 0000000..332369b --- /dev/null +++ b/src/assets/svg/back.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/app-bar/index.scss b/src/components/app-bar/index.scss new file mode 100644 index 0000000..d7d3b58 --- /dev/null +++ b/src/components/app-bar/index.scss @@ -0,0 +1,65 @@ +.app-bar { + position: relative; + z-index: 10; + width: 100%; + box-sizing: border-box; + padding-top: var(--appbar-top-inset); +} + +.app-bar__row { + width: 100%; + min-height: var(--appbar-menu-height); + display: flex; + align-items: center; + box-sizing: border-box; +} + +.app-bar__side { + width: var(--appbar-capsule-safe-width); + min-width: var(--appbar-capsule-safe-width); + display: flex; + align-items: center; +} + +.app-bar__side--left { + justify-content: flex-start; +} + +.app-bar__side--right { + justify-content: flex-end; +} + +.app-bar__content { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.app-bar__title { + color: #f3f7ff; + font-size: 32rpx; + font-weight: 600; + line-height: 1.4; +} + +.app-bar__action { + min-width: 56rpx; + height: 56rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.app-bar__action--placeholder { + opacity: 0; +} + +.app-bar__back-icon { + width: 32rpx; + height: 32rpx; + display: block; + flex-shrink: 0; +} diff --git a/src/components/app-bar/index.tsx b/src/components/app-bar/index.tsx new file mode 100644 index 0000000..2feec56 --- /dev/null +++ b/src/components/app-bar/index.tsx @@ -0,0 +1,62 @@ +import { Image, Text, View } from "@tarojs/components"; +import type { CSSProperties } from "react"; +import { useEffect, useState } from "react"; +import backIcon from "../../assets/svg/back.svg"; +import { getAppBarMetrics } from "../../utils/app-bar"; +import type { AppBarMetrics } from "../../utils/app-bar-metrics"; +import "./index.scss"; + +type AppBarProps = { + title: string; + showBack?: boolean; + onBack?: () => void; +}; + +const defaultMetrics: AppBarMetrics = { + topInset: 0, + menuTop: 6, + menuHeight: 32, + capsuleSafeWidth: 96 +}; + +export default function AppBar({ + title, + showBack = false, + onBack +}: AppBarProps) { + const [metrics, setMetrics] = useState(defaultMetrics); + + useEffect(() => { + setMetrics(getAppBarMetrics()); + }, []); + + const style = { + "--appbar-top-inset": `${metrics.topInset}px`, + "--appbar-menu-height": `${metrics.menuHeight}px`, + "--appbar-capsule-safe-width": `${metrics.capsuleSafeWidth}px` + } as CSSProperties; + + return ( + + + + {showBack ? ( + + + + ) : ( + + )} + + + + {title} + + + + + + + + ); +} diff --git a/src/components/secondary-page/index.tsx b/src/components/secondary-page/index.tsx index 4ee6118..755ae06 100644 --- a/src/components/secondary-page/index.tsx +++ b/src/components/secondary-page/index.tsx @@ -1,4 +1,6 @@ import { Text, View } from "@tarojs/components"; +import { navigateBackWithFallback } from "../../utils/app-bar"; +import AppBar from "../app-bar"; import "./index.scss"; export type SecondaryPageItem = { @@ -27,9 +29,10 @@ export default function SecondaryPage(props: SecondaryPageProps) { + navigateBackWithFallback("/pages/mine/index")} /> + {eyebrow} - {title} {description} diff --git a/src/pages/devices/index.scss b/src/pages/devices/index.scss index 5307a8b..33391a5 100644 --- a/src/pages/devices/index.scss +++ b/src/pages/devices/index.scss @@ -1,7 +1,7 @@ .devices-page { position: relative; min-height: 100vh; - padding: 26rpx 24rpx 44rpx; + padding: 0 24rpx 44rpx; box-sizing: border-box; background: linear-gradient( 180deg, @@ -36,6 +36,16 @@ background: radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 70%); } +.devices-page__subtitle { + position: relative; + z-index: 1; + display: block; + margin: 12rpx 0 24rpx; + color: var(--color-text-secondary); + font-size: 24rpx; + line-height: 1.6; +} + .devices-page__content { position: relative; z-index: 1; diff --git a/src/pages/devices/index.tsx b/src/pages/devices/index.tsx index 7b2a01e..59bc27a 100644 --- a/src/pages/devices/index.tsx +++ b/src/pages/devices/index.tsx @@ -1,5 +1,7 @@ import { Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; +import AppBar from "../../components/app-bar"; +import { navigateBackWithFallback } from "../../utils/app-bar"; import { getDeviceCards, type DeviceCard } from "./device-data"; import "./index.scss"; @@ -18,6 +20,9 @@ export default function DevicesPage() { + navigateBackWithFallback("/pages/mine/index")} /> + 已绑定设备与状态总览 + {deviceCards.map((card) => ( ("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(() => { - const windowInfo = typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo() : Taro.getSystemInfoSync(); - const menuButtonRect = - typeof Taro.getMenuButtonBoundingClientRect === "function" ? Taro.getMenuButtonBoundingClientRect() : null; - const safeTop = menuButtonRect?.bottom || windowInfo.statusBarHeight || 0; - const safeRight = menuButtonRect ? windowInfo.windowWidth - menuButtonRect.left : 0; - - setTopSafeHeight(safeTop); - setMenuSafeWidth(safeRight); - setMenuTop(menuButtonRect?.top || Math.max((windowInfo.statusBarHeight || 0) + 6, 0)); - setMenuHeight(menuButtonRect?.height || 32); - return () => { if (discoveryTimerRef.current) { clearTimeout(discoveryTimerRef.current); @@ -236,25 +221,19 @@ export default function Index() { } }; - const pageStyle = { - "--top-safe-height": `${topSafeHeight}px`, - "--menu-safe-width": `${menuSafeWidth}px`, - "--menu-top": `${menuTop}px`, - "--menu-height": `${menuHeight}px` - } as CSSProperties; - return ( - + - + + - 登录 + 登录 - + + + diff --git a/src/pages/message/index.scss b/src/pages/message/index.scss index ac766ba..dac5c46 100644 --- a/src/pages/message/index.scss +++ b/src/pages/message/index.scss @@ -1,7 +1,7 @@ .message-page { position: relative; min-height: 100vh; - padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx; + padding: 0 24rpx 156rpx; box-sizing: border-box; background: linear-gradient(180deg, #1d2331 0%, #171d29 100%); overflow: hidden; @@ -29,37 +29,39 @@ background: radial-gradient(circle, rgba(86, 116, 184, 0.12), transparent 72%); } -.message-header { +.message-tabs { position: relative; z-index: 1; width: 100%; - padding-right: calc(var(--menu-safe-width, 0px) + 12rpx); - margin-bottom: 10rpx; - box-sizing: border-box; - min-height: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 24rpx); -} - -.message-tabs { display: flex; + flex-direction: row; align-items: center; - justify-content: center; - gap: 110rpx; - min-height: inherit; - padding: 0; + margin-top: 12rpx; + margin-bottom: 24rpx; } .message-tabs__item { position: relative; + flex: 1; + height: 64rpx; + min-width: 0; display: flex; align-items: center; justify-content: center; - min-width: 120rpx; +} + +.message-tabs__content { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8rpx; } .message-tabs__label { color: #8f97ac; font-size: 24rpx; line-height: 1.3; + text-align: center; } .message-tabs__label--active { @@ -68,13 +70,12 @@ } .message-tabs__dot { - position: absolute; - top: 6rpx; - right: -10rpx; width: 10rpx; height: 10rpx; border-radius: 50%; background: #ff4d4f; + flex-shrink: 0; + transform: translateY(-8rpx); } .message-tabs__line { diff --git a/src/pages/message/index.tsx b/src/pages/message/index.tsx index 99a9bcd..f519f40 100644 --- a/src/pages/message/index.tsx +++ b/src/pages/message/index.tsx @@ -1,7 +1,6 @@ import { Text, View } from "@tarojs/components"; -import Taro from "@tarojs/taro"; -import type { CSSProperties } from "react"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import AppBar from "../../components/app-bar"; import BottomTabbar from "../../components/bottom-tabbar"; import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar"; import "./index.scss"; @@ -87,54 +86,32 @@ const fieldLabels = { export default function MessagePage() { const [activeTab, setActiveTab] = useState("vital"); - const [topSafeHeight, setTopSafeHeight] = useState(0); - const [menuSafeWidth, setMenuSafeWidth] = useState(0); - const [menuTop, setMenuTop] = useState(0); - const [menuHeight, setMenuHeight] = useState(0); const currentMessages = messageList.filter((item) => item.category === activeTab); - useEffect(() => { - const windowInfo = typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo() : Taro.getSystemInfoSync(); - const menuButtonRect = - typeof Taro.getMenuButtonBoundingClientRect === "function" ? Taro.getMenuButtonBoundingClientRect() : null; - const safeTop = menuButtonRect?.bottom || windowInfo.statusBarHeight || 0; - const safeRight = menuButtonRect ? windowInfo.windowWidth - menuButtonRect.left : 0; - - setTopSafeHeight(safeTop); - setMenuSafeWidth(safeRight); - setMenuTop(menuButtonRect?.top || Math.max((windowInfo.statusBarHeight || 0) + 6, 0)); - setMenuHeight(menuButtonRect?.height || 32); - }, []); - const navItems = createMainTabItems("message"); - const pageStyle = { - "--top-safe-height": `${topSafeHeight}px`, - "--menu-safe-width": `${menuSafeWidth}px`, - "--menu-top": `${menuTop}px`, - "--menu-height": `${menuHeight}px` - } as CSSProperties; - return ( - + - - - {tabs.map((item) => { - const isActive = item.key === activeTab; + - return ( - setActiveTab(item.key)}> + + {tabs.map((item) => { + const isActive = item.key === activeTab; + + return ( + setActiveTab(item.key)}> + {item.label} {item.hasDot ? : null} - {isActive ? : null} - ); - })} - + {isActive ? : null} + + ); + })} diff --git a/src/pages/mine/index.scss b/src/pages/mine/index.scss index f53b738..16b0c87 100644 --- a/src/pages/mine/index.scss +++ b/src/pages/mine/index.scss @@ -1,7 +1,7 @@ .mine-page { position: relative; min-height: 100vh; - padding: calc(var(--top-safe-height, 0px) + 34rpx) 24rpx 170rpx; + padding: 0 24rpx 170rpx; box-sizing: border-box; background: linear-gradient(180deg, #1f2534 0%, #171d29 100%); overflow: hidden; @@ -31,6 +31,14 @@ background: radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 70%); } +.mine-page__top-actions { + position: relative; + z-index: 1; + display: flex; + justify-content: flex-end; + margin: 12rpx 0 18rpx; +} + .mine-page__header-card { position: relative; z-index: 1; @@ -132,7 +140,6 @@ flex-direction: column; align-items: flex-end; padding-left: 18rpx; - padding-right: calc(var(--menu-safe-width, 0px) + 6rpx); } .mine-page__icon-actions { diff --git a/src/pages/mine/index.tsx b/src/pages/mine/index.tsx index 8e81c39..728fdec 100644 --- a/src/pages/mine/index.tsx +++ b/src/pages/mine/index.tsx @@ -1,7 +1,6 @@ import { Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; -import type { CSSProperties } from "react"; -import { useEffect, useState } from "react"; +import AppBar from "../../components/app-bar"; import BottomTabbar from "../../components/bottom-tabbar"; import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar"; import "./index.scss"; @@ -40,20 +39,6 @@ const featureItems: FeatureItem[] = [ ]; export default function MinePage() { - const [topSafeHeight, setTopSafeHeight] = useState(0); - const [menuSafeWidth, setMenuSafeWidth] = useState(0); - - useEffect(() => { - const windowInfo = typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo() : Taro.getSystemInfoSync(); - const menuButtonRect = - typeof Taro.getMenuButtonBoundingClientRect === "function" ? Taro.getMenuButtonBoundingClientRect() : null; - const safeTop = menuButtonRect?.bottom || windowInfo.statusBarHeight || 0; - const safeRight = menuButtonRect ? windowInfo.windowWidth - menuButtonRect.left : 0; - - setTopSafeHeight(safeTop); - setMenuSafeWidth(safeRight); - }, []); - const showToast = (title: string) => { Taro.showToast({ title, @@ -76,16 +61,19 @@ export default function MinePage() { const tabItems = createMainTabItems("mine"); - const pageStyle = { - "--top-safe-height": `${topSafeHeight}px`, - "--menu-safe-width": `${menuSafeWidth}px` - } as CSSProperties; - return ( - + + + + + openPage("/pages/support/index")} /> + openPage("/pages/settings/index")} /> + + + @@ -111,11 +99,6 @@ export default function MinePage() { - - openPage("/pages/support/index")} /> - openPage("/pages/settings/index")} /> - - openPage("/pages/profile/index")}> 个人信息 diff --git a/src/pages/repair-detail/index.scss b/src/pages/repair-detail/index.scss index a35f4df..fff9fbd 100644 --- a/src/pages/repair-detail/index.scss +++ b/src/pages/repair-detail/index.scss @@ -1,7 +1,7 @@ .repair-detail-page { position: relative; min-height: 100vh; - padding: 28rpx 24rpx 72rpx; + padding: 0 24rpx 72rpx; box-sizing: border-box; background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%); overflow: hidden; @@ -36,6 +36,16 @@ z-index: 1; } +.repair-detail-page__header-note { + position: relative; + z-index: 1; + display: block; + margin-top: 12rpx; + color: rgba(255, 255, 255, 0.58); + font-size: 24rpx; + text-align: center; +} + .repair-detail-page__hero { display: flex; flex-direction: column; diff --git a/src/pages/repair-detail/index.tsx b/src/pages/repair-detail/index.tsx index edd04a8..28d1af2 100644 --- a/src/pages/repair-detail/index.tsx +++ b/src/pages/repair-detail/index.tsx @@ -1,5 +1,7 @@ import { Button, Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; +import AppBar from "../../components/app-bar"; +import { navigateBackWithFallback } from "../../utils/app-bar"; import { REPAIR_DEVICE_TYPE_LABELS, REPAIR_DRAFT_STORAGE_KEY, @@ -30,6 +32,9 @@ export default function RepairDetailPage() { + navigateBackWithFallback("/pages/repair/index")} /> + 提交结果与工单信息 + @@ -91,7 +96,7 @@ export default function RepairDetailPage() { - diff --git a/src/pages/repair/index.scss b/src/pages/repair/index.scss index 7a140ea..abded9a 100644 --- a/src/pages/repair/index.scss +++ b/src/pages/repair/index.scss @@ -1,7 +1,7 @@ .repair-page { position: relative; min-height: 100vh; - padding: 28rpx 24rpx 72rpx; + padding: 0 24rpx 72rpx; box-sizing: border-box; background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%); overflow: hidden; @@ -29,7 +29,6 @@ background: radial-gradient(circle, rgba(76, 118, 214, 0.14) 0%, rgba(76, 118, 214, 0) 72%); } -.repair-page__toolbar, .repair-page__tabs, .repair-page__card, .repair-page__add-card, @@ -38,56 +37,27 @@ z-index: 1; } -.repair-page__toolbar { +.repair-page__header-row { + position: relative; + z-index: 1; display: flex; align-items: center; justify-content: space-between; - margin-bottom: 24rpx; + gap: 20rpx; + margin: 12rpx 0 24rpx; } -.repair-page__toolbar-spacer { - width: 1rpx; - height: 1rpx; +.repair-page__header-subtitle { + flex: 1; + color: rgba(255, 255, 255, 0.58); + font-size: 24rpx; + line-height: 1.6; } -.repair-page__history { - display: flex; - align-items: center; - gap: 12rpx; - padding: 8rpx 0 8rpx 12rpx; -} - -.repair-page__clock { - position: relative; - width: 34rpx; - height: 34rpx; - border: 2rpx solid rgba(255, 255, 255, 0.8); - border-radius: 50%; -} - -.repair-page__clock-hand { - position: absolute; - left: 50%; - bottom: 50%; - width: 2rpx; - background: rgba(255, 255, 255, 0.8); - border-radius: 999rpx; - transform-origin: bottom center; -} - -.repair-page__clock-hand--hour { - height: 9rpx; - transform: translateX(-50%) rotate(18deg); -} - -.repair-page__clock-hand--minute { - height: 12rpx; - transform: translateX(-50%) rotate(112deg); -} - -.repair-page__history-text { - color: rgba(255, 255, 255, 0.76); - font-size: 22rpx; +.repair-page__header-action { + flex-shrink: 0; + color: #35e5b3; + font-size: 24rpx; } .repair-page__tabs { diff --git a/src/pages/repair/index.tsx b/src/pages/repair/index.tsx index e2551de..2c3fa20 100644 --- a/src/pages/repair/index.tsx +++ b/src/pages/repair/index.tsx @@ -1,6 +1,8 @@ import { Button, Image, Input, Text, Textarea, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; import { useMemo, useState } from "react"; +import AppBar from "../../components/app-bar"; +import { navigateBackWithFallback } from "../../utils/app-bar"; import { REPAIR_DESCRIPTION_LIMIT, REPAIR_DEVICE_TYPE_LABELS, @@ -236,16 +238,16 @@ export default function RepairPage() { - - - - showToast("历史记录功能待开放")}> - - - - - 历史记录 - + navigateBackWithFallback("/pages/mine/index")} + /> + + 填写设备问题并上传附件 + showToast("历史记录功能待开放")}> + 历史记录 + diff --git a/src/pages/report/index.scss b/src/pages/report/index.scss index 7528c92..3fa15e6 100644 --- a/src/pages/report/index.scss +++ b/src/pages/report/index.scss @@ -9,7 +9,7 @@ position: relative; z-index: 1; min-height: 100vh; - padding: calc(var(--report-top-gap, 0px) + 20rpx) 24rpx 56rpx; + padding: 0 24rpx 56rpx; box-sizing: border-box; } @@ -35,51 +35,25 @@ background: radial-gradient(circle, rgba(36, 174, 255, 0.14) 0%, rgba(36, 174, 255, 0) 70%); } -.report-header { +.report-page__meta-bar { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 24rpx; + gap: 20rpx; + margin: 12rpx 0 24rpx; } -.report-header__action { - display: flex; - align-items: center; - justify-content: center; - min-width: 68rpx; - height: 52rpx; - padding: 0 16rpx; - border-radius: 18rpx; - background: rgba(32, 42, 58, 0.82); - box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.04); -} - -.report-header__action--share { - min-width: 96rpx; -} - -.report-header__action-text { - color: #dce7f8; - font-size: 24rpx; -} - -.report-header__content { +.report-page__date-label { flex: 1; - padding: 0 20rpx; + color: #91a3b8; + font-size: 24rpx; + line-height: 1.6; } -.report-header__title { - display: block; - color: #f7fbff; - font-size: 34rpx; - font-weight: 600; -} - -.report-header__subtitle { - display: block; - margin-top: 6rpx; - color: #8ea0b6; - font-size: 22rpx; +.report-page__share-action { + flex-shrink: 0; + color: #36e4aa; + font-size: 24rpx; } .report-toolbar { diff --git a/src/pages/report/index.tsx b/src/pages/report/index.tsx index a523617..18b7907 100644 --- a/src/pages/report/index.tsx +++ b/src/pages/report/index.tsx @@ -1,7 +1,7 @@ import { ScrollView, Text, View } from "@tarojs/components"; import Taro from "@tarojs/taro"; -import type { CSSProperties } from "react"; import { useMemo, useState } from "react"; +import AppBar from "../../components/app-bar"; import { ReportChartCard } from "../../components/report/chart-card"; import { ReportChipGroup } from "../../components/report/chip-group"; import { ReportDaytimePrediction } from "../../components/report/daytime-prediction"; @@ -9,9 +9,9 @@ import { ReportDistributionCard } from "../../components/report/distribution-car import { ReportInsightList } from "../../components/report/insight-list"; import { ReportKvList } from "../../components/report/kv-list"; import { ReportMetricGrid } from "../../components/report/metric-grid"; -import { ReportPageHeader } from "../../components/report/page-header"; import { ReportScoreOverview } from "../../components/report/score-overview"; import { ReportSummaryBlock } from "../../components/report/summary-block"; +import { navigateBackWithFallback } from "../../utils/app-bar"; import { reportDimensions, reportOptions, reportRecords } from "./mock"; import { getSleepLevel, pickReportRecord } from "./report-utils"; import type { ReportDimension } from "./types"; @@ -81,27 +81,31 @@ export default function ReportPage() { resetSelections(dimension, dateKey, roomKey, nextDevice); }; - const pageStyle = { - "--report-top-gap": `${(typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo().statusBarHeight : Taro.getSystemInfoSync().statusBarHeight) || 0}px` - } as CSSProperties; - return ( - + - Taro.navigateBack({ delta: 1 })} - onShare={() => - Taro.showToast({ - title: "分享功能待接入", - icon: "none" - }) - } + showBack + onBack={() => navigateBackWithFallback("/pages/index/index")} /> + + {currentRecord.dateLabel} + + Taro.showToast({ + title: "分享功能待接入", + icon: "none" + }) + } + > + 分享 + + diff --git a/src/utils/app-bar-metrics.ts b/src/utils/app-bar-metrics.ts new file mode 100644 index 0000000..0c1e498 --- /dev/null +++ b/src/utils/app-bar-metrics.ts @@ -0,0 +1,38 @@ +type AppBarMetricInput = { + menuButtonRect?: Partial<{ + top: number; + left: number; + height: number; + }> | null; + statusBarHeight?: number; + windowWidth?: number; +}; + +export type AppBarMetrics = { + capsuleSafeWidth: number; + menuHeight: number; + menuTop: number; + topInset: number; +}; + +export function computeAppBarMetrics(input: AppBarMetricInput): AppBarMetrics { + const topInset = Math.max(input.statusBarHeight || 0, 0); + const menuHeight = input.menuButtonRect?.height && input.menuButtonRect.height > 0 ? input.menuButtonRect.height : 32; + const menuTop = + typeof input.menuButtonRect?.top === "number" ? input.menuButtonRect.top : Math.max(topInset + 6, topInset); + const capsuleSafeWidth = + typeof input.menuButtonRect?.left === "number" && input.windowWidth + ? Math.max(input.windowWidth - input.menuButtonRect.left, 96) + : 96; + + return { + topInset, + menuTop, + menuHeight, + capsuleSafeWidth + }; +} + +export function resolveBackNavigation(pageStackLength: number) { + return pageStackLength > 1 ? "navigateBack" : "redirectHome"; +} diff --git a/src/utils/app-bar.ts b/src/utils/app-bar.ts new file mode 100644 index 0000000..20bddbc --- /dev/null +++ b/src/utils/app-bar.ts @@ -0,0 +1,27 @@ +import Taro from "@tarojs/taro"; +import { computeAppBarMetrics, resolveBackNavigation, type AppBarMetrics } from "./app-bar-metrics"; + +export { computeAppBarMetrics, resolveBackNavigation, type AppBarMetrics } from "./app-bar-metrics"; + +export function getAppBarMetrics() { + const windowInfo = typeof Taro.getWindowInfo === "function" ? Taro.getWindowInfo() : Taro.getSystemInfoSync(); + const menuButtonRect = + typeof Taro.getMenuButtonBoundingClientRect === "function" ? Taro.getMenuButtonBoundingClientRect() : null; + + return computeAppBarMetrics({ + menuButtonRect, + statusBarHeight: windowInfo.statusBarHeight, + windowWidth: windowInfo.windowWidth + }); +} + +export function navigateBackWithFallback(fallbackUrl: string) { + const action = resolveBackNavigation(Taro.getCurrentPages().length); + + if (action === "navigateBack") { + Taro.navigateBack({ delta: 1 }); + return; + } + + Taro.redirectTo({ url: fallbackUrl }); +} diff --git a/types/global.d.ts b/types/global.d.ts index 0292a33..abff0b5 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -1 +1,5 @@ declare module "*.scss"; +declare module "*.svg" { + const src: string; + export default src; +}