refactor: 收口公共AppBar组件
This commit is contained in:
19
AGENTS.md
19
AGENTS.md
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
- 保持项目结构简单、易懂、易修改
|
- 保持项目结构简单、易懂、易修改
|
||||||
- 使用 `Taro + React + TypeScript` 作为核心技术栈
|
- 使用 `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 { Text, View } from "@tarojs/components";
|
||||||
|
import { navigateBackWithFallback } from "../../utils/app-bar";
|
||||||
|
import AppBar from "../app-bar";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
|
|
||||||
export type SecondaryPageItem = {
|
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--large" />
|
||||||
<View className="secondary-page__halo secondary-page__halo--small" />
|
<View className="secondary-page__halo secondary-page__halo--small" />
|
||||||
|
|
||||||
|
<AppBar title={title} showBack onBack={() => navigateBackWithFallback("/pages/mine/index")} />
|
||||||
|
|
||||||
<View className="secondary-page__hero">
|
<View className="secondary-page__hero">
|
||||||
<Text className="secondary-page__eyebrow">{eyebrow}</Text>
|
<Text className="secondary-page__eyebrow">{eyebrow}</Text>
|
||||||
<Text className="secondary-page__title">{title}</Text>
|
|
||||||
<Text className="secondary-page__description">{description}</Text>
|
<Text className="secondary-page__description">{description}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.devices-page {
|
.devices-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 26rpx 24rpx 44rpx;
|
padding: 0 24rpx 44rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
@@ -36,6 +36,16 @@
|
|||||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 70%);
|
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 {
|
.devices-page__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Text, View } from "@tarojs/components";
|
import { Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
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 { getDeviceCards, type DeviceCard } from "./device-data";
|
||||||
import "./index.scss";
|
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--large" />
|
||||||
<View className="devices-page__halo devices-page__halo--small" />
|
<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">
|
<View className="devices-page__content">
|
||||||
{deviceCards.map((card) => (
|
{deviceCards.map((card) => (
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.device-page {
|
.device-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx;
|
padding: 0 24rpx 156rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, var(--color-bg-page-gradient-start) 0%, var(--color-bg-page-gradient-end) 100%);
|
background: linear-gradient(180deg, var(--color-bg-page-gradient-start) 0%, var(--color-bg-page-gradient-end) 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -34,40 +34,48 @@
|
|||||||
.device-header {
|
.device-header {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
}
|
||||||
padding-right: calc(var(--menu-safe-width, 0px) + 12rpx);
|
|
||||||
margin-bottom: 8rpx;
|
.device-header__top-actions {
|
||||||
min-height: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 36rpx + 6rpx);
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 12rpx 0 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header__login {
|
.device-header__login {
|
||||||
position: absolute;
|
display: inline-flex;
|
||||||
left: 0;
|
align-items: center;
|
||||||
top: calc(var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx);
|
justify-content: center;
|
||||||
min-width: 92rpx;
|
min-width: 92rpx;
|
||||||
height: var(--menu-height, 32px);
|
min-height: 56rpx;
|
||||||
padding: 0 24rpx;
|
padding: 0 24rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
background: linear-gradient(90deg, var(--color-brand-start) 0%, var(--color-brand-end) 100%);
|
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);
|
box-shadow: 0 12rpx 24rpx var(--color-brand-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-header__add {
|
.device-header__add {
|
||||||
position: absolute;
|
display: inline-flex;
|
||||||
right: calc(var(--menu-safe-width, 0px) + 2rpx);
|
align-items: center;
|
||||||
top: calc((var(--menu-top, 0px) - var(--top-safe-height, 0px) - 24rpx) + var(--menu-height, 32px) + 6rpx);
|
justify-content: center;
|
||||||
width: 36rpx;
|
width: 36rpx;
|
||||||
height: 36rpx;
|
height: 36rpx;
|
||||||
border: 2rpx solid var(--color-border-strong);
|
border: 2rpx solid var(--color-border-strong);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-header__login-text {
|
||||||
|
color: var(--color-text-white);
|
||||||
|
font-size: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-header__add-text {
|
||||||
color: var(--color-text-white);
|
color: var(--color-text-white);
|
||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
line-height: 30rpx;
|
line-height: 1;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-summary {
|
.device-summary {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Text, View } from "@tarojs/components";
|
import { Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import type { CSSProperties } from "react";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import ActionButton from "../../components/action-button";
|
import ActionButton from "../../components/action-button";
|
||||||
|
import AppBar from "../../components/app-bar";
|
||||||
import BottomTabbar from "../../components/bottom-tabbar";
|
import BottomTabbar from "../../components/bottom-tabbar";
|
||||||
import PanelCard from "../../components/panel-card";
|
import PanelCard from "../../components/panel-card";
|
||||||
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
||||||
@@ -49,24 +49,9 @@ function parseDeviceCode(result?: string) {
|
|||||||
export default function Index() {
|
export default function Index() {
|
||||||
const [deviceCount, setDeviceCount] = useState(0);
|
const [deviceCount, setDeviceCount] = useState(0);
|
||||||
const [bluetoothStatus, setBluetoothStatus] = useState<BluetoothStatus>("idle");
|
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);
|
const discoveryTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
return () => {
|
||||||
if (discoveryTimerRef.current) {
|
if (discoveryTimerRef.current) {
|
||||||
clearTimeout(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 (
|
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--large" />
|
||||||
<View className="device-page__halo device-page__halo--small" />
|
<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}>
|
<View className="device-header__login" onClick={handleLogin}>
|
||||||
登录
|
<Text className="device-header__login-text">登录</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="device-header__add" onClick={handleAdd}>
|
<View className="device-header__add" onClick={handleAdd}>
|
||||||
+
|
<Text className="device-header__add-text">+</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.message-page {
|
.message-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: calc(var(--top-safe-height, 0px) + 24rpx) 24rpx 156rpx;
|
padding: 0 24rpx 156rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, #1d2331 0%, #171d29 100%);
|
background: linear-gradient(180deg, #1d2331 0%, #171d29 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -29,37 +29,39 @@
|
|||||||
background: radial-gradient(circle, rgba(86, 116, 184, 0.12), transparent 72%);
|
background: radial-gradient(circle, rgba(86, 116, 184, 0.12), transparent 72%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-header {
|
.message-tabs {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
width: 100%;
|
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;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
margin-top: 12rpx;
|
||||||
gap: 110rpx;
|
margin-bottom: 24rpx;
|
||||||
min-height: inherit;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs__item {
|
.message-tabs__item {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
height: 64rpx;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 120rpx;
|
}
|
||||||
|
|
||||||
|
.message-tabs__content {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs__label {
|
.message-tabs__label {
|
||||||
color: #8f97ac;
|
color: #8f97ac;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs__label--active {
|
.message-tabs__label--active {
|
||||||
@@ -68,13 +70,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs__dot {
|
.message-tabs__dot {
|
||||||
position: absolute;
|
|
||||||
top: 6rpx;
|
|
||||||
right: -10rpx;
|
|
||||||
width: 10rpx;
|
width: 10rpx;
|
||||||
height: 10rpx;
|
height: 10rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #ff4d4f;
|
background: #ff4d4f;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transform: translateY(-8rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-tabs__line {
|
.message-tabs__line {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Text, View } from "@tarojs/components";
|
import { Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import { useState } from "react";
|
||||||
import type { CSSProperties } from "react";
|
import AppBar from "../../components/app-bar";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import BottomTabbar from "../../components/bottom-tabbar";
|
import BottomTabbar from "../../components/bottom-tabbar";
|
||||||
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
@@ -87,54 +86,32 @@ const fieldLabels = {
|
|||||||
|
|
||||||
export default function MessagePage() {
|
export default function MessagePage() {
|
||||||
const [activeTab, setActiveTab] = useState<MessageTabKey>("vital");
|
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);
|
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 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 (
|
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--top" />
|
||||||
<View className="message-page__glow message-page__glow--side" />
|
<View className="message-page__glow message-page__glow--side" />
|
||||||
|
|
||||||
<View className="message-header">
|
<AppBar title="" />
|
||||||
<View className="message-tabs">
|
|
||||||
{tabs.map((item) => {
|
|
||||||
const isActive = item.key === activeTab;
|
|
||||||
|
|
||||||
return (
|
<View className="message-tabs">
|
||||||
<View className="message-tabs__item" key={item.key} onClick={() => setActiveTab(item.key)}>
|
{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>
|
<Text className={`message-tabs__label ${isActive ? "message-tabs__label--active" : ""}`}>{item.label}</Text>
|
||||||
{item.hasDot ? <View className="message-tabs__dot" /> : null}
|
{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>
|
</View>
|
||||||
|
|
||||||
<View className="message-list">
|
<View className="message-list">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.mine-page {
|
.mine-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: calc(var(--top-safe-height, 0px) + 34rpx) 24rpx 170rpx;
|
padding: 0 24rpx 170rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, #1f2534 0%, #171d29 100%);
|
background: linear-gradient(180deg, #1f2534 0%, #171d29 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -31,6 +31,14 @@
|
|||||||
background: radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 70%);
|
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 {
|
.mine-page__header-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -132,7 +140,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
padding-left: 18rpx;
|
padding-left: 18rpx;
|
||||||
padding-right: calc(var(--menu-safe-width, 0px) + 6rpx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine-page__icon-actions {
|
.mine-page__icon-actions {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Text, View } from "@tarojs/components";
|
import { Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import type { CSSProperties } from "react";
|
import AppBar from "../../components/app-bar";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import BottomTabbar from "../../components/bottom-tabbar";
|
import BottomTabbar from "../../components/bottom-tabbar";
|
||||||
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
import { createMainTabItems, handleMainTabNavigation } from "../../utils/main-tabbar";
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
@@ -40,20 +39,6 @@ const featureItems: FeatureItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function MinePage() {
|
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) => {
|
const showToast = (title: string) => {
|
||||||
Taro.showToast({
|
Taro.showToast({
|
||||||
title,
|
title,
|
||||||
@@ -76,16 +61,19 @@ export default function MinePage() {
|
|||||||
|
|
||||||
const tabItems = createMainTabItems("mine");
|
const tabItems = createMainTabItems("mine");
|
||||||
|
|
||||||
const pageStyle = {
|
|
||||||
"--top-safe-height": `${topSafeHeight}px`,
|
|
||||||
"--menu-safe-width": `${menuSafeWidth}px`
|
|
||||||
} as CSSProperties;
|
|
||||||
|
|
||||||
return (
|
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--large" />
|
||||||
<View className="mine-page__halo mine-page__halo--small" />
|
<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-card">
|
||||||
<View className="mine-page__header-main">
|
<View className="mine-page__header-main">
|
||||||
<View className="mine-page__avatar">
|
<View className="mine-page__avatar">
|
||||||
@@ -111,11 +99,6 @@ export default function MinePage() {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="mine-page__header-actions">
|
<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 className="mine-page__profile-link" onClick={() => openPage("/pages/profile/index")}>
|
||||||
个人信息
|
个人信息
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.repair-detail-page {
|
.repair-detail-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 28rpx 24rpx 72rpx;
|
padding: 0 24rpx 72rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%);
|
background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -36,6 +36,16 @@
|
|||||||
z-index: 1;
|
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 {
|
.repair-detail-page__hero {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Button, Text, View } from "@tarojs/components";
|
import { Button, Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
|
import AppBar from "../../components/app-bar";
|
||||||
|
import { navigateBackWithFallback } from "../../utils/app-bar";
|
||||||
import {
|
import {
|
||||||
REPAIR_DEVICE_TYPE_LABELS,
|
REPAIR_DEVICE_TYPE_LABELS,
|
||||||
REPAIR_DRAFT_STORAGE_KEY,
|
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--left" />
|
||||||
<View className="repair-detail-page__glow repair-detail-page__glow--right" />
|
<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__hero">
|
||||||
<View className="repair-detail-page__success-ring">
|
<View className="repair-detail-page__success-ring">
|
||||||
<View className="repair-detail-page__success-check" />
|
<View className="repair-detail-page__success-check" />
|
||||||
@@ -91,7 +96,7 @@ export default function RepairDetailPage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button className="repair-detail-page__button" onClick={() => Taro.navigateBack()}>
|
<Button className="repair-detail-page__button" onClick={() => navigateBackWithFallback("/pages/repair/index")}>
|
||||||
返回上一页
|
返回上一页
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.repair-page {
|
.repair-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 28rpx 24rpx 72rpx;
|
padding: 0 24rpx 72rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%);
|
background: linear-gradient(180deg, #0b1220 0%, #121a2c 100%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
background: radial-gradient(circle, rgba(76, 118, 214, 0.14) 0%, rgba(76, 118, 214, 0) 72%);
|
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__tabs,
|
||||||
.repair-page__card,
|
.repair-page__card,
|
||||||
.repair-page__add-card,
|
.repair-page__add-card,
|
||||||
@@ -38,56 +37,27 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repair-page__toolbar {
|
.repair-page__header-row {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 24rpx;
|
gap: 20rpx;
|
||||||
|
margin: 12rpx 0 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repair-page__toolbar-spacer {
|
.repair-page__header-subtitle {
|
||||||
width: 1rpx;
|
flex: 1;
|
||||||
height: 1rpx;
|
color: rgba(255, 255, 255, 0.58);
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repair-page__history {
|
.repair-page__header-action {
|
||||||
display: flex;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
color: #35e5b3;
|
||||||
gap: 12rpx;
|
font-size: 24rpx;
|
||||||
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__tabs {
|
.repair-page__tabs {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Button, Image, Input, Text, Textarea, View } from "@tarojs/components";
|
import { Button, Image, Input, Text, Textarea, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import AppBar from "../../components/app-bar";
|
||||||
|
import { navigateBackWithFallback } from "../../utils/app-bar";
|
||||||
import {
|
import {
|
||||||
REPAIR_DESCRIPTION_LIMIT,
|
REPAIR_DESCRIPTION_LIMIT,
|
||||||
REPAIR_DEVICE_TYPE_LABELS,
|
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--left" />
|
||||||
<View className="repair-page__glow repair-page__glow--right" />
|
<View className="repair-page__glow repair-page__glow--right" />
|
||||||
|
|
||||||
<View className="repair-page__toolbar">
|
<AppBar
|
||||||
<View className="repair-page__toolbar-spacer" />
|
title="申请报修"
|
||||||
|
showBack
|
||||||
<View className="repair-page__history" onClick={() => showToast("历史记录功能待开放")}>
|
onBack={() => navigateBackWithFallback("/pages/mine/index")}
|
||||||
<View className="repair-page__clock">
|
/>
|
||||||
<View className="repair-page__clock-hand repair-page__clock-hand--hour" />
|
<View className="repair-page__header-row">
|
||||||
<View className="repair-page__clock-hand repair-page__clock-hand--minute" />
|
<Text className="repair-page__header-subtitle">填写设备问题并上传附件</Text>
|
||||||
</View>
|
<Text className="repair-page__header-action" onClick={() => showToast("历史记录功能待开放")}>
|
||||||
<Text className="repair-page__history-text">历史记录</Text>
|
历史记录
|
||||||
</View>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="repair-page__tabs">
|
<View className="repair-page__tabs">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: calc(var(--report-top-gap, 0px) + 20rpx) 24rpx 56rpx;
|
padding: 0 24rpx 56rpx;
|
||||||
box-sizing: border-box;
|
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%);
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 24rpx;
|
gap: 20rpx;
|
||||||
|
margin: 12rpx 0 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-header__action {
|
.report-page__date-label {
|
||||||
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 {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0 20rpx;
|
color: #91a3b8;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-header__title {
|
.report-page__share-action {
|
||||||
display: block;
|
flex-shrink: 0;
|
||||||
color: #f7fbff;
|
color: #36e4aa;
|
||||||
font-size: 34rpx;
|
font-size: 24rpx;
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.report-header__subtitle {
|
|
||||||
display: block;
|
|
||||||
margin-top: 6rpx;
|
|
||||||
color: #8ea0b6;
|
|
||||||
font-size: 22rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.report-toolbar {
|
.report-toolbar {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ScrollView, Text, View } from "@tarojs/components";
|
import { ScrollView, Text, View } from "@tarojs/components";
|
||||||
import Taro from "@tarojs/taro";
|
import Taro from "@tarojs/taro";
|
||||||
import type { CSSProperties } from "react";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
import AppBar from "../../components/app-bar";
|
||||||
import { ReportChartCard } from "../../components/report/chart-card";
|
import { ReportChartCard } from "../../components/report/chart-card";
|
||||||
import { ReportChipGroup } from "../../components/report/chip-group";
|
import { ReportChipGroup } from "../../components/report/chip-group";
|
||||||
import { ReportDaytimePrediction } from "../../components/report/daytime-prediction";
|
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 { ReportInsightList } from "../../components/report/insight-list";
|
||||||
import { ReportKvList } from "../../components/report/kv-list";
|
import { ReportKvList } from "../../components/report/kv-list";
|
||||||
import { ReportMetricGrid } from "../../components/report/metric-grid";
|
import { ReportMetricGrid } from "../../components/report/metric-grid";
|
||||||
import { ReportPageHeader } from "../../components/report/page-header";
|
|
||||||
import { ReportScoreOverview } from "../../components/report/score-overview";
|
import { ReportScoreOverview } from "../../components/report/score-overview";
|
||||||
import { ReportSummaryBlock } from "../../components/report/summary-block";
|
import { ReportSummaryBlock } from "../../components/report/summary-block";
|
||||||
|
import { navigateBackWithFallback } from "../../utils/app-bar";
|
||||||
import { reportDimensions, reportOptions, reportRecords } from "./mock";
|
import { reportDimensions, reportOptions, reportRecords } from "./mock";
|
||||||
import { getSleepLevel, pickReportRecord } from "./report-utils";
|
import { getSleepLevel, pickReportRecord } from "./report-utils";
|
||||||
import type { ReportDimension } from "./types";
|
import type { ReportDimension } from "./types";
|
||||||
@@ -81,27 +81,31 @@ export default function ReportPage() {
|
|||||||
resetSelections(dimension, dateKey, roomKey, nextDevice);
|
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 (
|
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--one" />
|
||||||
<View className="report-page__glow report-page__glow--two" />
|
<View className="report-page__glow report-page__glow--two" />
|
||||||
|
|
||||||
<View className="report-page__content">
|
<View className="report-page__content">
|
||||||
<ReportPageHeader
|
<AppBar
|
||||||
title={currentRecord.babyName}
|
title={currentRecord.babyName}
|
||||||
subtitle={currentRecord.dateLabel}
|
showBack
|
||||||
onBack={() => Taro.navigateBack({ delta: 1 })}
|
onBack={() => navigateBackWithFallback("/pages/index/index")}
|
||||||
onShare={() =>
|
|
||||||
Taro.showToast({
|
|
||||||
title: "分享功能待接入",
|
|
||||||
icon: "none"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
<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">
|
<View className="report-toolbar">
|
||||||
<ReportChipGroup options={reportDimensions} value={dimension} onChange={handleDimensionChange} />
|
<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 "*.scss";
|
||||||
|
declare module "*.svg" {
|
||||||
|
const src: string;
|
||||||
|
export default src;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user