前言

之前的 3D 拆解分析是完全开放的——打开就能用,不需要任何身份验证。这在 demo 阶段没问题,但一旦要接入网络功能(发现页、AI 分析、历史记录),就需要知道"当前用户是谁"。

这次迭代做了两件事:

  1. 接入 AuthService,未登录用户强制跳转登录页
  2. 全面引入毛玻璃(frosted glass)视觉风格——半透明背景 + 全局渐变

登录守卫

AuthService 接入

import { isLoggedIn } from '../services/AuthService'

在这里插入图片描述

新增 AuthServiceisLoggedIn 方法——返回当前用户是否已登录。

aboutToAppear 的守卫

aboutToAppear(): void {
  if (!isLoggedIn()) {
    router.pushUrl({ url: 'pages/LoginPage' })
    return
  }
  this.loadDiscover()
}

在这里插入图片描述

App 启动时先检查登录状态:

  1. 未登录 → 跳转 LoginPage,return 不执行后续逻辑
  2. 已登录 → 正常加载发现页数据

onPageShow 的二次检查

onPageShow(): void {
  if (!isLoggedIn()) {
    router.pushUrl({ url: 'pages/LoginPage' })
  }
}

在这里插入图片描述

除了启动时检查,每次页面重新显示时也检查。这防止了一种情况:用户在 App 运行期间退出登录(比如在设置页点了退出),回到首页时应该被踢到登录页。

为什么用 router.pushUrl 而不是 router.replaceUrl?

router.pushUrl({ url: 'pages/LoginPage' })

pushUrl 会把 LoginPage 压入导航栈。用户登录成功后,可以 router.back() 回到首页。

如果用 replaceUrl,LoginPage 会替换首页。用户登录后无法返回——因为首页已经不在导航栈里了。

登录状态对功能的影响

@State loggedIn: boolean = false

虽然定义了 loggedIn 状态变量,但当前代码没有用它做条件渲染。所有功能(发现页、AI 分析)在登录后都可以使用。

后续如果要做"未登录只能看 3 个发现项,登录后看全部",可以用 loggedIn 控制。

全局渐变背景

旧版:纯色背景

.width('100%').height('100%').backgroundColor(BG)
// BG = '#E6E0F0'

新版:全局渐变

.width('100%').height('100%')
.linearGradient({ angle: 160,
  colors: [['#F0EDF8', 0], ['#E8F0FE', 0.5], ['#FFF5EE', 1]] })

在这里插入图片描述

三色渐变:

  • 起点(0%):#F0EDF8 —— 淡紫
  • 中间(50%):#E8F0FE —— 淡蓝
  • 终点(100%):#FFF5EE —— 淡橙

角度 160°——从左上到右下,不是正上方到正下方。

为什么是 160° 而不是 180°?

180° 是正上方到正下方,渐变会显得"太规整"。160° 稍微偏一点,让渐变方向和用户阅读方向(从左到右)更一致,视觉上更自然。

三种颜色的搭配逻辑:

紫色(#F0EDF8)→ 蓝色(#E8F0FE)→ 橙色(#FFF5EE),是色轮上的相邻色。从冷色到暖色的过渡,给用户一种"从理性到感性"的心理暗示——左边是数据(紫色=科技),右边是体验(橙色=温暖)。

毛玻璃视觉体系

核心概念

毛玻璃(frosted glass)的本质是:半透明白色背景 + 极淡边框 + 微弱阴影。

// 统一的毛玻璃样式
.backgroundColor('rgba(255,255,255,0.65)')  // 65% 不透明度
.border({ width: 1, color: 'rgba(138,93,250,0.08)' })  // 极淡紫色边框
.shadow({ radius: 18, color: 'rgba(138,93,250,0.06)', offsetY: 3 })  // 紫色阴影

各组件的毛玻璃实现

统计卡片:

// 旧版
.backgroundColor(CW)  // 纯白

// 新版
.backgroundColor('rgba(255,255,255,0.70)')  // 70% 白

控制面板:

// 旧版
.backgroundColor(CW)

// 新版
.backgroundColor('rgba(255,255,255,0.80)')
.border({ width: 1, color: 'rgba(138,93,250,0.06)' })
.shadow({ radius: 10, color: 'rgba(138,93,250,0.04)', offsetY: 2 })

功能按钮:

// 旧版
.backgroundColor(this.list ? AL : '#F5F5F7')

// 新版(FeatureBtn)
.backgroundColor(active ? bgColor : '#F5F5F7')

部件清单:

// 旧版
.backgroundColor(CW)

// 新版
.backgroundColor('rgba(255,255,255,0.60)')
.border({ width: 0.5, color: 'rgba(138,93,250,0.06)' })

透明度的层次

不同组件用了不同的透明度:

组件 透明度 为什么
统计卡片 70% 需要足够不透明才能看清文字
控制面板 80% 滑块和按钮需要高对比度
工作区 65% 需要透出背景渐变
部件清单 60% 内容密度高,需要更多透明度减轻视觉重量

越需要"看清内容"的组件,透明度越高(越接近纯白)。越需要"融入背景"的组件,透明度越低(越透明)。

点阵网格回归

旧版的点阵

之前有过 DotData 的点阵模式,后来被粒子场替代。这次又加回来了——但实现方式不同。

新版的点阵

// 生成点阵数据
const dlist: DotData[] = []
for (let r: number = 0; r < 12; r++) {
  for (let c: number = 0; c < 8; c++) {
    dlist.push({
      x: (c * 12.5 + 6).toString() + '%',
      y: (r * 8.3 + 4).toString() + '%'
    })
  }
}
this.dots = dlist

8 列 × 12 行 = 96 个点,用百分比定位。

点阵的渲染

ForEach(this.dots, (d: DotData) => {
  Circle({ width: 2, height: 2 })
    .fill('rgba(138,93,250,0.15)')
    .position({ x: d.x, y: d.y })
})

每个点是 2×2 的紫色圆形,15% 透明度。

为什么点阵比粒子场更适合?

  1. 性能:点阵是静态的,不消耗 CPU 做动画
  2. 可控:点的位置和密度可以精确控制
  3. 简洁:不会干扰用户对内容的注意力

粒子场虽然酷炫,但作为工作区背景太"吵"了——用户需要专注看爆炸效果,不需要背景在动。

FeatureBtn:功能按钮的统一

旧版:三个独立按钮

Row() {
  // AI 分析按钮
  Row() {
    Text('🤖').fontSize(13).margin({ right: 2 })
    Text(this.aiLoading ? '分析中...' : 'AI 分析').fontSize(11)...
  }.padding(...).backgroundColor(...).onClick(...)

  // 部件清单按钮
  Row() {
    Text('☰').fontSize(12).fontColor(...)
    Text(' 部件').fontSize(11)...
  }.padding(...).backgroundColor(...).onClick(...)

  Blank()

  // 重置按钮
  Row() {
    Text('🔄 重置').fontSize(11)...
  }.padding(...).backgroundColor(...).onClick(...)
}

每个按钮的样式都手写一遍,代码重复。

新版:FeatureBtn Builder

@Builder FeatureBtn(icon: string, label: string, active: boolean,
  bgColor: string, action: () => void) {
  Row() {
    Text(icon).fontSize(12)
    Text(' ' + label).fontSize(10).fontWeight(FontWeight.Medium)
      .fontColor(active ? A : T2)
  }.padding({ left: 10, right: 10, top: 6, bottom: 6 })
    .backgroundColor(active ? bgColor : '#F5F5F7')
    .borderRadius(12).layoutWeight(1)
    .justifyContent(FlexAlign.Center).onClick(action)
}

调用方式:

Row({ space: 6 }) {
  this.FeatureBtn('🤖', this.aiLoading ? '分析中' : 'AI 识别',
    this.aiLoading, AL, () => { if (!this.aiLoading) { this.runAiAnalysis() } })
  this.FeatureBtn('📋', '部件清单', this.list, AL,
    () => { this.list = !this.list })
  this.FeatureBtn('🔄', '重置', false, '#F5F5F7',
    () => { this.setV(0); this.cg(3, 3) })
}

FeatureBtn 的设计:

  1. layoutWeight(1) —— 三个按钮等宽
  2. active 控制颜色——激活时用主题色背景,否则用灰色
  3. bgColor 参数——不同按钮可以有不同的激活背景色

重置按钮的特殊处理

this.FeatureBtn('🔄', '重置', false, '#F5F5F7',
  () => { this.setV(0); this.cg(3, 3) })

重置按钮的 active 永远是 false——它不会处于"激活"状态。bgColor'#F5F5F7'(灰色),因为重置不需要强调色。

拆解页 Header 的变化

Tag 新增爆炸状态

Row({ space: 6 }) {
  this.Tag('#8A5DFA', '#F4F1FC', this.count + '部件')
  this.Tag('#00B894', '#E8F8F5', this.cols + '×' + this.rows)
  if (this.on) { this.Tag('#FF6B6B', '#FFF0F0', Math.round(this.val * 100) + '%') }
}

爆炸时新增第三个 Tag:红色背景 + 百分比数字。用户一眼就能看到"当前爆炸力度是多少"。

空状态的数字放大

// 旧版
Text(this.count.toString()).fontSize(44)

// 新版
Text(this.count.toString()).fontSize(48)

从 44px 增大到 48px——空状态的数字是视觉焦点,大一点更有冲击力。

网格预览的样式微调

// 旧版
.backgroundColor('rgba(138,93,250,0.08)')
.border({ width: 1.5, color: 'rgba(138,93,250,0.15)' })

// 新版
.backgroundColor('rgba(138,93,250,0.06)')
.border({ width: 1.5, color: 'rgba(138,93,250,0.12)' })

背景和边框都降低了约 25% 的不透明度——点阵网格已经提供了背景纹理,网格预览不需要那么显眼。

踩坑记录

1. isLoggedIn 的调用时机

aboutToAppear(): void {
  if (!isLoggedIn()) {
    router.pushUrl({ url: 'pages/LoginPage' })
    return
  }
  this.loadDiscover()
}

isLoggedIn() 是同步调用——它读取本地存储的登录状态。如果后续改成异步(比如需要验证 token 有效性),需要用 async/await

aboutToAppear(): async void {
  const loggedIn = await isLoggedIn()
  if (!loggedIn) {
    router.pushUrl({ url: 'pages/LoginPage' })
    return
  }
  this.loadDiscover()
}

2. onPageShow 的重复检查

onPageShow 在每次页面显示时都会调用。如果用户快速切换 Tab,isLoggedIn() 会被频繁调用。

优化方案:在 aboutToAppear 中缓存登录状态,onPageShow 只在状态变化时检查。

3. 毛玻璃在低端设备上的性能

半透明背景(rgba(255,255,255,0.65))需要 GPU 做 alpha 混合。在低端设备上,如果同时有多个半透明层叠,可能导致掉帧。

建议:在性能敏感的场景(比如爆炸动画进行中),可以把工作区背景改成纯白,减少 alpha 混合的层数。

4. 点阵的 ForEach 性能

96 个点用 ForEach 渲染——每个点都是一个独立的 Circle 组件。如果点阵更密(比如 20×20 = 400 个点),可能影响渲染性能。

优化方案:把点阵画在一张 Canvas 上,而不是用 96 个独立组件。

写在最后

登录守卫让 App 从"公开工具"变成了"用户产品"。用户需要注册才能使用,这意味着后续可以做个性化功能——保存分析历史、分享到发现页、AI 分析的调用次数限制。

毛玻璃视觉体系让整个 App 的视觉语言统一了。从首页的统计卡片到拆解页的控制面板,所有组件都用同样的半透明白色 + 极淡紫色边框。用户在不同页面之间切换时,视觉感受是一致的。

点阵网格的回归说明了一个道理:酷炫的动效(粒子场)不一定比静态的纹理(点阵)好。在需要用户专注的场景,安静的背景比吵闹的背景更合适。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐