refactor: 收口公共AppBar组件
This commit is contained in:
19
AGENTS.md
19
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 设计稿顶部结构与微信小程序原生导航区域冲突时,优先保证小程序可用性和对齐关系,再尽量还原设计视觉。
|
||||
- 页面评审时,顶部区域要重点检查这几项:
|
||||
- 是否遮挡原生胶囊
|
||||
- 是否与原生胶囊水平对齐
|
||||
- 是否预留了不同机型的顶部安全区
|
||||
- 是否在真机上仍然成立
|
||||
|
||||
## 修改边界
|
||||
|
||||
- 可以新增或修改当前工作区内与本项目直接相关的文件。
|
||||
|
||||
40
scripts/tests/app-bar-source.test.cjs
Normal file
40
scripts/tests/app-bar-source.test.cjs
Normal file
@@ -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, /<Image[\s\S]*className="app-bar__back-icon"/);
|
||||
});
|
||||
|
||||
run("AppBar back action no longer renders a text arrow", () => {
|
||||
assert.doesNotMatch(appBarSource, /<|>\s*<\s*</);
|
||||
});
|
||||
|
||||
run("AppBar back icon uses a fixed mini-program sized constraint", () => {
|
||||
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\?:/);
|
||||
});
|
||||
45
scripts/tests/message-page-source.test.cjs
Normal file
45
scripts/tests/message-page-source.test.cjs
Normal file
@@ -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,
|
||||
/<AppBar[\s\S]*\/>\s*<View className="message-tabs">/
|
||||
);
|
||||
});
|
||||
11
src/assets/svg/add.svg
Normal file
11
src/assets/svg/add.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg viewBox="0 0 37.1797 37.1798" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="37.179688" height="37.179787" fill="none" customFrame="#000000">
|
||||
<g id="组 9850">
|
||||
<g id="组 9848">
|
||||
<circle id="椭圆 1364" cx="18.5898952" cy="18.5898952" r="17.5898952" stroke="rgb(255,255,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="0.000000" />
|
||||
</g>
|
||||
<g id="组 9849">
|
||||
<line id="直线 573" x1="10.6748047" x2="26.5057106" y1="18.5898972" y2="18.5898972" stroke="rgb(255,255,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
<line id="直线 574" x1="0" x2="15.830905" y1="0" y2="0" stroke="rgb(255,255,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" transform="matrix(0,1,-1,0,18.5898,10.6744)" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 841 B |
6
src/assets/svg/back.svg
Normal file
6
src/assets/svg/back.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 42 42" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="42.000000" height="42.000000" fill="none" customFrame="#000000">
|
||||
<g id="组 10388">
|
||||
<rect id="矩形 3479" width="42.000000" height="42.000000" x="0.000000" y="0.000000" />
|
||||
<path id="路径 15460" d="M28.2715 6.45681L13.7285 21L28.2715 35.5432" stroke="rgb(255,255,255)" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.000000" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 473 B |
65
src/components/app-bar/index.scss
Normal file
65
src/components/app-bar/index.scss
Normal file
@@ -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;
|
||||
}
|
||||
62
src/components/app-bar/index.tsx
Normal file
62
src/components/app-bar/index.tsx
Normal file
@@ -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<AppBarMetrics>(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 (
|
||||
<View className="app-bar" style={style}>
|
||||
<View className="app-bar__row">
|
||||
<View className="app-bar__side app-bar__side--left">
|
||||
{showBack ? (
|
||||
<View className="app-bar__action" onClick={onBack}>
|
||||
<Image className="app-bar__back-icon" src={backIcon} mode="aspectFit" />
|
||||
</View>
|
||||
) : (
|
||||
<View className="app-bar__action app-bar__action--placeholder" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="app-bar__content">
|
||||
<Text className="app-bar__title">{title}</Text>
|
||||
</View>
|
||||
|
||||
<View className="app-bar__side app-bar__side--right">
|
||||
<View className="app-bar__action app-bar__action--placeholder" />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
<View className="secondary-page__halo secondary-page__halo--large" />
|
||||
<View className="secondary-page__halo secondary-page__halo--small" />
|
||||
|
||||
<AppBar title={title} showBack onBack={() => navigateBackWithFallback("/pages/mine/index")} />
|
||||
|
||||
<View className="secondary-page__hero">
|
||||
<Text className="secondary-page__eyebrow">{eyebrow}</Text>
|
||||
<Text className="secondary-page__title">{title}</Text>
|
||||
<Text className="secondary-page__description">{description}</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
<View className="devices-page__halo devices-page__halo--large" />
|
||||
<View className="devices-page__halo devices-page__halo--small" />
|
||||
|
||||
<AppBar title="我的设备" showBack onBack={() => navigateBackWithFallback("/pages/mine/index")} />
|
||||
<Text className="devices-page__subtitle">已绑定设备与状态总览</Text>
|
||||
|
||||
<View className="devices-page__content">
|
||||
{deviceCards.map((card) => (
|
||||
<View
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.device-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, var(--color-bg-page-gradient-start) 0%, var(--color-bg-page-gradient-end) 100%);
|
||||
overflow: hidden;
|
||||
@@ -34,40 +34,48 @@
|
||||
.device-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
padding-right: calc(var(--menu-safe-width, 0px) + 12rpx);
|
||||
margin-bottom: 8rpx;
|
||||
min-height: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 36rpx + 6rpx);
|
||||
}
|
||||
|
||||
.device-header__top-actions {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 12rpx 0 18rpx;
|
||||
}
|
||||
|
||||
.device-header__login {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 92rpx;
|
||||
height: var(--menu-height, 32px);
|
||||
min-height: 56rpx;
|
||||
padding: 0 24rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%);
|
||||
color: var(--color-text-white);
|
||||
font-size: 22rpx;
|
||||
line-height: var(--menu-height, 32px);
|
||||
text-align: center;
|
||||
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);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border: 2rpx solid var(--color-border-strong);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.device-header__login-text {
|
||||
color: var(--color-text-white);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.device-header__add-text {
|
||||
color: var(--color-text-white);
|
||||
font-size: 30rpx;
|
||||
line-height: 30rpx;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.device-summary {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 AppBar from "../../components/app-bar";
|
||||
import BottomTabbar from "../../components/bottom-tabbar";
|
||||
import PanelCard from "../../components/panel-card";
|
||||
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
||||
@@ -49,24 +49,9 @@ function parseDeviceCode(result?: string) {
|
||||
export default function Index() {
|
||||
const [deviceCount, setDeviceCount] = useState(0);
|
||||
const [bluetoothStatus, setBluetoothStatus] = useState<BluetoothStatus>("idle");
|
||||
const [topSafeHeight, setTopSafeHeight] = 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);
|
||||
|
||||
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 (
|
||||
<View className="device-page" style={pageStyle}>
|
||||
<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">
|
||||
<AppBar title="" />
|
||||
<View className="device-header__top-actions">
|
||||
<View className="device-header__login" onClick={handleLogin}>
|
||||
登录
|
||||
<Text className="device-header__login-text">登录</Text>
|
||||
</View>
|
||||
|
||||
<View className="device-header__add" onClick={handleAdd}>
|
||||
+
|
||||
<Text className="device-header__add-text">+</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<MessageTabKey>("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 (
|
||||
<View className="message-page" style={pageStyle}>
|
||||
<View className="message-page">
|
||||
<View className="message-page__glow message-page__glow--top" />
|
||||
<View className="message-page__glow message-page__glow--side" />
|
||||
|
||||
<View className="message-header">
|
||||
<View className="message-tabs">
|
||||
{tabs.map((item) => {
|
||||
const isActive = item.key === activeTab;
|
||||
<AppBar title="" />
|
||||
|
||||
return (
|
||||
<View className="message-tabs__item" key={item.key} onClick={() => setActiveTab(item.key)}>
|
||||
<View className="message-tabs">
|
||||
{tabs.map((item) => {
|
||||
const isActive = item.key === activeTab;
|
||||
|
||||
return (
|
||||
<View className="message-tabs__item" key={item.key} onClick={() => setActiveTab(item.key)}>
|
||||
<View className="message-tabs__content">
|
||||
<Text className={`message-tabs__label ${isActive ? "message-tabs__label--active" : ""}`}>{item.label}</Text>
|
||||
{item.hasDot ? <View className="message-tabs__dot" /> : null}
|
||||
{isActive ? <View className="message-tabs__line" /> : null}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
{isActive ? <View className="message-tabs__line" /> : null}
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View className="message-list">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<View className="mine-page" style={pageStyle}>
|
||||
<View className="mine-page">
|
||||
<View className="mine-page__halo mine-page__halo--large" />
|
||||
<View className="mine-page__halo mine-page__halo--small" />
|
||||
|
||||
<AppBar title="" />
|
||||
<View className="mine-page__top-actions">
|
||||
<View className="mine-page__icon-actions">
|
||||
<View className="mine-page__circle-action mine-page__circle-action--support" onClick={() => openPage("/pages/support/index")} />
|
||||
<View className="mine-page__circle-action mine-page__circle-action--settings" onClick={() => openPage("/pages/settings/index")} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className="mine-page__header-card">
|
||||
<View className="mine-page__header-main">
|
||||
<View className="mine-page__avatar">
|
||||
@@ -111,11 +99,6 @@ export default function MinePage() {
|
||||
</View>
|
||||
|
||||
<View className="mine-page__header-actions">
|
||||
<View className="mine-page__icon-actions">
|
||||
<View className="mine-page__circle-action mine-page__circle-action--support" onClick={() => openPage("/pages/support/index")} />
|
||||
<View className="mine-page__circle-action mine-page__circle-action--settings" onClick={() => openPage("/pages/settings/index")} />
|
||||
</View>
|
||||
|
||||
<Text className="mine-page__profile-link" onClick={() => openPage("/pages/profile/index")}>
|
||||
个人信息
|
||||
</Text>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
<View className="repair-detail-page__glow repair-detail-page__glow--left" />
|
||||
<View className="repair-detail-page__glow repair-detail-page__glow--right" />
|
||||
|
||||
<AppBar title="报修详情" showBack onBack={() => navigateBackWithFallback("/pages/repair/index")} />
|
||||
<Text className="repair-detail-page__header-note">提交结果与工单信息</Text>
|
||||
|
||||
<View className="repair-detail-page__hero">
|
||||
<View className="repair-detail-page__success-ring">
|
||||
<View className="repair-detail-page__success-check" />
|
||||
@@ -91,7 +96,7 @@ export default function RepairDetailPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Button className="repair-detail-page__button" onClick={() => Taro.navigateBack()}>
|
||||
<Button className="repair-detail-page__button" onClick={() => navigateBackWithFallback("/pages/repair/index")}>
|
||||
返回上一页
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
<View className="repair-page__glow repair-page__glow--left" />
|
||||
<View className="repair-page__glow repair-page__glow--right" />
|
||||
|
||||
<View className="repair-page__toolbar">
|
||||
<View className="repair-page__toolbar-spacer" />
|
||||
|
||||
<View className="repair-page__history" onClick={() => showToast("历史记录功能待开放")}>
|
||||
<View className="repair-page__clock">
|
||||
<View className="repair-page__clock-hand repair-page__clock-hand--hour" />
|
||||
<View className="repair-page__clock-hand repair-page__clock-hand--minute" />
|
||||
</View>
|
||||
<Text className="repair-page__history-text">历史记录</Text>
|
||||
</View>
|
||||
<AppBar
|
||||
title="申请报修"
|
||||
showBack
|
||||
onBack={() => navigateBackWithFallback("/pages/mine/index")}
|
||||
/>
|
||||
<View className="repair-page__header-row">
|
||||
<Text className="repair-page__header-subtitle">填写设备问题并上传附件</Text>
|
||||
<Text className="repair-page__header-action" onClick={() => showToast("历史记录功能待开放")}>
|
||||
历史记录
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="repair-page__tabs">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<ScrollView className="report-page" scrollY style={pageStyle}>
|
||||
<ScrollView className="report-page" scrollY>
|
||||
<View className="report-page__glow report-page__glow--one" />
|
||||
<View className="report-page__glow report-page__glow--two" />
|
||||
|
||||
<View className="report-page__content">
|
||||
<ReportPageHeader
|
||||
<AppBar
|
||||
title={currentRecord.babyName}
|
||||
subtitle={currentRecord.dateLabel}
|
||||
onBack={() => Taro.navigateBack({ delta: 1 })}
|
||||
onShare={() =>
|
||||
Taro.showToast({
|
||||
title: "分享功能待接入",
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
showBack
|
||||
onBack={() => navigateBackWithFallback("/pages/index/index")}
|
||||
/>
|
||||
<View className="report-page__meta-bar">
|
||||
<Text className="report-page__date-label">{currentRecord.dateLabel}</Text>
|
||||
<Text
|
||||
className="report-page__share-action"
|
||||
onClick={() =>
|
||||
Taro.showToast({
|
||||
title: "分享功能待接入",
|
||||
icon: "none"
|
||||
})
|
||||
}
|
||||
>
|
||||
分享
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View className="report-toolbar">
|
||||
<ReportChipGroup options={reportDimensions} value={dimension} onChange={handleDimensionChange} />
|
||||
|
||||
38
src/utils/app-bar-metrics.ts
Normal file
38
src/utils/app-bar-metrics.ts
Normal file
@@ -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";
|
||||
}
|
||||
27
src/utils/app-bar.ts
Normal file
27
src/utils/app-bar.ts
Normal file
@@ -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 });
|
||||
}
|
||||
4
types/global.d.ts
vendored
4
types/global.d.ts
vendored
@@ -1 +1,5 @@
|
||||
declare module "*.scss";
|
||||
declare module "*.svg" {
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user