fix: 顶栏颜色跟随小程序主题并保留跟随系统

This commit is contained in:
2026-08-06 19:54:05 +08:00
parent 9716148f15
commit f4f54a8720
84 changed files with 484 additions and 124 deletions
+9 -1
View File
@@ -1,4 +1,6 @@
export default defineAppConfig({
// darkmode 仅作为“跟随系统”的探测通道(onThemeChange/getAppBaseInfo().theme 需要它),
// 顶部/窗口颜色不直接依赖它;每个页面用 PageMeta + NavigationBar 按小程序内设置强覆盖。
darkmode: true,
themeLocation: 'theme.json',
pages: [
@@ -23,7 +25,13 @@ export default defineAppConfig({
window: {
backgroundTextStyle: 'dark',
navigationStyle: 'custom',
// 应用启动时先保证浅色页面的 iOS 状态栏为黑字;页面 Hook 会在主题变化时覆盖它。
// 顶部/窗口颜色先显式固定为小程序浅色主题,避免 iOS 顶部区域跟随系统深色;
// 应用启动后页面 Hook 会按小程序内主题实时覆盖上面的值。
navigationBarBackgroundColor: '#ffffff',
backgroundColor: '#ffffff',
backgroundColorTop: '#ffffff',
backgroundColorBottom: '#ffffff',
// 状态栏文字默认黑字;各页面 useStatusBar 会按小程序主题覆盖它。
navigationBarTextStyle: 'black',
},
tabBar: {
+20
View File
@@ -256,3 +256,23 @@ page {
border-top: var(--line-card);
box-sizing: border-box;
}
/* ==========================================================
系统主题探测器(跟随系统模式)
以官方 onThemeChange/getAppBaseInfo 为主,这里是 CSS 兜底通道。
========================================================== */
.system-theme-detector {
position: fixed;
left: -9999px;
top: 0;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
@media (prefers-color-scheme: dark) {
.system-theme-detector {
opacity: 1;
}
}
+34 -38
View File
@@ -1,56 +1,52 @@
import Taro from '@tarojs/taro'
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback } from 'react'
import { View, Text } from '@tarojs/components'
import { getMe, logout } from '../../utils/api'
import { getUserInfoRaw, clearUserInfo, isMockOpenid } from '../../utils/store'
import { isAuthVerified } from '../../utils/authState'
import { THEME_CHANGE_EVENT } from '../../context/ThemeContext'
type GuardState = 'checking' | 'ok' | 'denied'
/**
* 登录守卫:不仅看本地是否有 openid,还要求本地用户与云端校验一致。
* - 本地 openid 是历史 mock 残留 -> 判定不一致,清掉并引导重新登录
* - 本地有 openid 但后端 getMe 校验失败/用户不存在 -> 判定不一致,清掉并引导重新登录
* - 本地与云端 ID/openid 一致 -> 放行
* 登录守卫:首次进入做一次云端校验并缓存结果,
* 之后同一 openid 直接读取缓存放行,不再重复请求后端。
* 每次 Tab 切换/重新显示时再核对一次,避免登录完成后还停留在旧的“未登录”态。
*/
export default function LoginGuard(props: { children: React.ReactNode }) {
const [state, setState] = useState<GuardState>('checking')
useEffect(() => {
const localUser = getUserInfoRaw()
// 本地根本没登录信息
if (!localUser?.openid) {
setState('denied')
return
}
// 本地是旧版 mock 残留:与真实云端不一致,强制重新登录
if (isMockOpenid(localUser.openid)) {
clearUserInfo()
logout()
setState('denied')
return
}
// 本地有真实 openid:到后端校验是否与云端一致
getMe()
.then((me) => {
const consistent = !!me && !!me.id && me.openid === localUser.openid
if (consistent) {
setState('ok')
} else {
clearUserInfo()
logout()
setState('denied')
}
const verify = useCallback(() => {
let cancelled = false
isAuthVerified()
.then((ok) => {
if (!cancelled) setState(ok ? 'ok' : 'denied')
})
.catch(() => {
// 后端不可用时的谨慎处理:不静默放行,也不抹掉本地,
// 返回 denied 让用户尝试重新登录(后端恢复后即可通过)
setState('denied')
if (!cancelled) setState('denied')
})
return () => {
cancelled = true
}
}, [])
useEffect(() => {
const cancel = verify()
const onTab = () => {
verify()
}
const onTheme = () => {
verify()
}
Taro.eventCenter.on('tabBarChange', onTab)
Taro.eventCenter.on(THEME_CHANGE_EVENT, onTheme)
Taro.eventCenter.on('authStateChanged', onTab)
return () => {
cancel()
Taro.eventCenter.off('tabBarChange', onTab)
Taro.eventCenter.off(THEME_CHANGE_EVENT, onTheme)
Taro.eventCenter.off('authStateChanged', onTab)
}
}, [verify])
if (state === 'checking') {
return (
<View className='login-guard'>
+26
View File
@@ -0,0 +1,26 @@
import { PageMeta, NavigationBar } from '@tarojs/components'
import { useThemeContext } from '../../context/ThemeContext'
interface ThemedPageMetaProps {
/** 强制顶部使用深色背景(用于浅色主题但顶部是深色大图的页面) */
darkTop?: boolean
}
export default function ThemedPageMeta({ darkTop = false }: ThemedPageMetaProps) {
const { resolvedTheme } = useThemeContext()
const isDark = resolvedTheme === 'dark' || darkTop
const bg = isDark ? '#191919' : '#ffffff'
const frontColor = isDark ? '#ffffff' : '#000000'
return (
<PageMeta
backgroundColor={bg}
backgroundColorTop={bg}
backgroundColorBottom={bg}
backgroundTextStyle={isDark ? 'light' : 'dark'}
rootBackgroundColor={bg}
>
<NavigationBar frontColor={frontColor} backgroundColor={bg} />
</PageMeta>
)
}
+66 -31
View File
@@ -1,6 +1,7 @@
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { getTheme, setTheme as saveTheme, resolveTheme, type ThemeMode } from '../utils/store'
import { View } from '@tarojs/components'
import { getTheme, setTheme as saveTheme, resolveTheme, setDetectedSystemTheme, type ThemeMode } from '../utils/store'
import { applyPageBackground } from '../utils/themeBackground'
interface ThemeContextValue {
@@ -19,16 +20,14 @@ const ThemeContext = createContext<ThemeContextValue>({
export { ThemeContext }
type ThemeChangeListener = (result: { theme?: string }) => void
type ThemeChangeApi = {
onThemeChange?: (listener: ThemeChangeListener) => void
offThemeChange?: (listener: ThemeChangeListener) => void
}
/** 主题广播事件名:自定义 tabBar 等无法继承 React Context 的独立组件通过它同步主题 */
export const THEME_CHANGE_EVENT = 'themeChange'
type ThemeChangeApi = {
onThemeChange?: (listener: (res: { theme?: string }) => void) => void
offThemeChange?: (listener: (res: { theme?: string }) => void) => void
}
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, set] = useState<ThemeMode>(getTheme())
const [resolvedTheme, setResolved] = useState<'light' | 'dark'>(resolveTheme(getTheme()))
@@ -48,6 +47,57 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
setTheme(next)
}, [resolvedTheme, setTheme])
/**
* darkmode 只作为系统主题探测通道;选中“跟随系统”时才跟着系统切换,
* 固定浅色/深色时由 setTheme 直接决定,不被动绑定系统。
*/
const updateFromSystem = useCallback((t: 'light' | 'dark') => {
setDetectedSystemTheme(t)
if (getTheme() === 'auto') {
setResolved(t)
}
}, [])
const detectSystemTheme = useCallback(() => {
try {
const info = Taro.getAppBaseInfo()
updateFromSystem(info.theme === 'dark' ? 'dark' : 'light')
} catch {
/* ignore */
}
// CSS 探测器作为兜底:个别机型 getAppBaseInfo().theme 延迟时再读一次
try {
Taro.createSelectorQuery()
.select('.system-theme-detector')
.fields({ computedStyle: ['opacity'] })
.exec((res: any) => {
const opacity = res && res[0] && res[0].computedStyle && res[0].computedStyle.opacity
updateFromSystem(opacity === '1' ? 'dark' : 'light')
})
} catch {
/* ignore */
}
}, [updateFromSystem])
useEffect(() => {
detectSystemTheme()
}, [detectSystemTheme])
// 系统实时切换深浅色时更新“跟随系统”模式
useEffect(() => {
const listener = (res: { theme?: string }) => {
if (res && (res.theme === 'dark' || res.theme === 'light')) {
updateFromSystem(res.theme)
}
}
const themeApi = Taro as typeof Taro & ThemeChangeApi
themeApi.onThemeChange?.(listener)
return () => {
themeApi.offThemeChange?.(listener)
}
}, [updateFromSystem])
// 广播实际生效主题,供 custom-tab-bar 等独立组件订阅
useEffect(() => {
Taro.eventCenter.trigger(THEME_CHANGE_EVENT, resolvedTheme)
@@ -59,35 +109,20 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
applyPageBackground(resolvedTheme)
}, [resolvedTheme])
// 页面每次显示时(从小程序后台切回前台)重新测系统主题
// 页面每次显示时(从小程序后台切回前台)重新测系统主题
useDidShow(() => {
if (theme === 'auto') {
setResolved(resolveTheme('auto'))
detectSystemTheme()
}
})
// auto模式下监听系统主题变化
useEffect(() => {
if (theme === 'auto') {
// 先同步一次当前系统主题
setResolved(resolveTheme('auto'))
const listener: ThemeChangeListener = (res) => {
setResolved(res.theme === 'dark' ? 'dark' : 'light')
}
const themeApi = Taro as typeof Taro & ThemeChangeApi
themeApi.onThemeChange?.(listener)
return () => {
themeApi.offThemeChange?.(listener)
}
} else {
updateResolved(theme)
}
}, [theme, updateResolved])
return (
<ThemeContext.Provider value={{ theme, resolvedTheme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
<>
<View className='system-theme-detector' />
<ThemeContext.Provider value={{ theme, resolvedTheme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
</>
)
}
+17 -13
View File
@@ -3,14 +3,16 @@
bottom: 0;
left: 0;
right: 0;
height: 120px;
background: rgba(255, 255, 255, 0.75);
height: calc(96px + constant(safe-area-inset-bottom));
height: calc(96px + env(safe-area-inset-bottom));
box-sizing: border-box;
background: rgba(255, 255, 255, 0.78);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
display: flex;
justify-content: space-around;
align-items: center;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.08);
box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.08);
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
z-index: 1000;
@@ -18,10 +20,10 @@
/* 自定义组件样式隔离:两套主题配色在组件内自包含,不依赖全局 app.wxss */
.custom-tab-bar.theme-dark {
background: rgba(25, 25, 25, 0.75);
background: rgba(25, 25, 25, 0.78);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.5);
box-shadow: 0 -8px 20px rgba(0, 0, 0, 0.45);
}
.tab-item {
@@ -29,28 +31,31 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px 20px;
padding: 0 12px;
position: relative;
flex: 1;
max-width: 160px;
}
.tab-icon-img {
width: 28px;
height: 28px;
margin-bottom: 4px;
width: 40px;
height: 40px;
margin-bottom: 2px;
transition: transform 0.2s;
opacity: 0.6;
opacity: 0.8;
}
.tab-item.active .tab-icon-img {
transform: scale(1.15);
transform: scale(1.08);
opacity: 1;
}
.tab-label {
font-size: 20px;
color: #b08d8d;
font-weight: 600;
font-weight: 700;
transition: color 0.2s;
line-height: 22px;
}
.custom-tab-bar.theme-dark .tab-label {
@@ -59,7 +64,6 @@
.tab-item.active .tab-label {
color: #ff9a9e;
font-weight: 700;
}
.custom-tab-bar.theme-dark .tab-item.active .tab-label {
+33 -10
View File
@@ -7,18 +7,28 @@ import { applyPageBackground } from '../utils/themeBackground'
import './index.scss'
const TABS = [
{ pagePath: '/pages/index/index', text: '首页', icon: '/icon/首页.svg', iconActive: '/icon/首页-fill.svg' },
{ pagePath: '/pages/shop/index', text: '商品', icon: '/icon/商品.svg', iconActive: '/icon/商品-fill.svg' },
{ pagePath: '/pages/designList/index', text: '设计清单', icon: '/icon/调色盘.svg', iconActive: '/icon/调色盘-fill.svg' },
{ pagePath: '/pages/orders/index', text: '订单', icon: '/icon/包裹.svg', iconActive: '/icon/包裹-fill.svg' },
{ pagePath: '/pages/profile/index', text: '我的', icon: '/icon/个人.svg', iconActive: '/icon/个人-fill.svg' }
{ pagePath: '/pages/index/index', label: '首页', icon: '/icon/首页.svg', iconActive: '/icon/首页-fill.svg' },
{ pagePath: '/pages/shop/index', label: '商品', icon: '/icon/商品.svg', iconActive: '/icon/商品-fill.svg' },
{ pagePath: '/pages/designList/index', label: '设计清单', icon: '/icon/调色盘.svg', iconActive: '/icon/调色盘-fill.svg' },
{ pagePath: '/pages/orders/index', label: '订单', icon: '/icon/包裹.svg', iconActive: '/icon/包裹-fill.svg' },
{ pagePath: '/pages/profile/index', label: '我的', icon: '/icon/个人.svg', iconActive: '/icon/个人-fill.svg' }
]
function matchTab(path: string): string {
const idx = TABS.findIndex((t) => path.endsWith(t.pagePath))
return TABS[idx >= 0 ? idx : 0].pagePath
}
function currentTabPath(): string {
const path = Taro.getCurrentInstance().router?.path || 'pages/index/index'
return matchTab(path)
}
export default function CustomTabBar() {
// 自定义 tabBar 是独立组件,无法继承页面里的 React Context
// 初始值从本地主题配置读取,之后通过 eventCenter 跟随主题切换
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>(resolveTheme(getTheme()))
const currentPath = `/${Taro.getCurrentInstance().router?.path || 'pages/index/index'}`
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
useEffect(() => {
const handler = (t: 'light' | 'dark') => setResolvedTheme(t)
@@ -28,6 +38,17 @@ export default function CustomTabBar() {
}
}, [])
// 其它入口(如页面内部调用 switchTab)也会回到 tabBar,收到事件后重新同步选中态
useEffect(() => {
const handler = (path?: string) => {
setActiveTab(matchTab(path || Taro.getCurrentInstance().router?.path || 'pages/index/index'))
}
Taro.eventCenter.on('tabBarChange', handler)
return () => {
Taro.eventCenter.off('tabBarChange', handler)
}
}, [])
// tabBar 是 tab 页切换时必定会挂载/更新的组件:在这里也刷新一次页面背景,
// 兜底处理“登录后切到受保护页面”时自定义导航区背景,避免残留白色
useEffect(() => {
@@ -35,13 +56,15 @@ export default function CustomTabBar() {
}, [resolvedTheme])
const switchTab = (url: string) => {
setActiveTab(matchTab(url))
Taro.switchTab({ url })
Taro.eventCenter?.trigger('tabBarChange', url)
}
return (
<View className={`custom-tab-bar theme-${resolvedTheme}`}>
{TABS.map((tab) => {
const isActive = currentPath === tab.pagePath
const isActive = tab.pagePath === activeTab
return (
<View
key={tab.pagePath}
@@ -49,7 +72,7 @@ export default function CustomTabBar() {
onTap={() => switchTab(tab.pagePath)}
>
<Image className={`tab-icon-img ${isActive ? 'active' : ''}`} src={isActive ? tab.iconActive : tab.icon} mode='aspectFit' />
<Text className='tab-label'>{tab.text}</Text>
<Text className='tab-label'>{tab.label}</Text>
</View>
)
})}
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '收货地址'
})
+2
View File
@@ -7,6 +7,7 @@ import type { AddressItem } from '../../types'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function AddressPage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -93,6 +94,7 @@ export default function AddressPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='address-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '定制协议'
})
+2
View File
@@ -3,6 +3,7 @@ import './index.scss'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function AgreementPage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -34,6 +35,7 @@ export default function AgreementPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='agreement-page'>
<View className='agreement-content' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='agreement-title'></Text>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '确认下单'
})
+3
View File
@@ -6,6 +6,7 @@ import { getDesignList, designToOrder, getDefaultAddress, getAddressList, type A
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function CheckoutPage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -59,6 +60,7 @@ export default function CheckoutPage() {
if (!design) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='checkout-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='page-title'></Text>
@@ -79,6 +81,7 @@ export default function CheckoutPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='checkout-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '设计清单'
})
+2
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import LoginGuard from '../../components/LoginGuard'
import ThemedPageMeta from '../../components/ThemedPageMeta'
const STATUS_TABS = [
{ code: 'all', label: '全部' },
@@ -115,6 +116,7 @@ export default function DesignListPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<LoginGuard>
<View className='design-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: 'DIY工作台'
})
+3
View File
@@ -7,6 +7,7 @@ import { getDesignList, setDesignList, updateDesign, type DesignItem, type Stick
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function DIYPage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -257,6 +258,7 @@ export default function DIYPage() {
if (!category) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='diy-page'>
<Text className='page-title'>...</Text>
</View>
@@ -266,6 +268,7 @@ export default function DIYPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='diy-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '编辑贴纸'
})
+2
View File
@@ -7,6 +7,7 @@ import { getDesignList, setDesignList, updateDesign, type DesignItem, type Stick
import { useThemeContext } from '../../../context/ThemeContext'
import { useSafeArea } from '../../../hooks/useSafeArea'
import { useStatusBar } from '../../../hooks/useStatusBar'
import ThemedPageMeta from '../../../components/ThemedPageMeta'
/* ============================================================
使用 Canvas 实现贴纸编辑(亮度/色相/线稿)
@@ -304,6 +305,7 @@ export default function StickerEditPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='sticker-edit-page'>
{/* Header */}
<View className='edit-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '智绘微刻'
})
+2
View File
@@ -6,6 +6,7 @@ import { CATEGORIES } from '../../utils/productConfig'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
// 成品展示配图(从 img 里取对应实物图)
const SHOWCASE_LIST = [
@@ -48,6 +49,7 @@ export default function Index() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='index-page'>
{/* 顶部标题栏 */}
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '订单详情'
})
+3
View File
@@ -5,6 +5,7 @@ import './index.scss'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store'
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
@@ -51,6 +52,7 @@ export default function OrderDetailPage() {
if (!order) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='order-detail-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='page-title'></Text>
@@ -62,6 +64,7 @@ export default function OrderDetailPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='order-detail-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '订单列表'
})
+2
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import LoginGuard from '../../components/LoginGuard'
import ThemedPageMeta from '../../components/ThemedPageMeta'
const STATUS_TABS = [
{ code: 'all', label: '全部' },
@@ -75,6 +76,7 @@ export default function OrdersPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<LoginGuard>
<View className='orders-page'>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '商品详情'
})
+3
View File
@@ -7,6 +7,7 @@ import { addDesign } from '../../utils/store'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
import { getProductIconImg } from '../../utils/productConfig'
const HERO_COLORS = ['#FFE4EC', '#FFF0F5', '#FCE4EC']
@@ -25,6 +26,7 @@ export default function ProductPage() {
if (!product) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='product-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<Text className='page-title'></Text>
@@ -58,6 +60,7 @@ export default function ProductPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='product-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '个人中心'
})
+2
View File
@@ -6,6 +6,7 @@ import { getOrderList, getUserInfo, setUserInfo, getDesignList } from '../../uti
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
import { login, getMe, updateProfile, getToken } from '../../utils/api'
import { safeHideLoading } from '../../utils/request'
@@ -276,6 +277,7 @@ export default function ProfilePage() {
return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta />
<View className='profile-page'>
{/* 用户信息头部 */}
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '客服中心'
})
+2
View File
@@ -4,6 +4,7 @@ import './index.scss'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
const FAQ_ITEMS = [
{ q: '定制周期需要多久?', a: '通常下单后3-5个工作日内发货,批量订单(50件以上)可能需要7-10个工作日。' },
@@ -19,6 +20,7 @@ export default function ServicePage() {
return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta />
<View className='service-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
<View className='star-badge' />
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '设置'
})
+2
View File
@@ -5,6 +5,7 @@ import './index.scss'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
import { getUserInfo, setUserInfo, clearUserInfo, getAddressList, getDefaultAddress, type AddressItem } from '../../utils/store'
const THEME_OPTIONS: { label: string; value: 'light' | 'dark' | 'auto' }[] = [
@@ -66,6 +67,7 @@ export default function SettingsPage() {
return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta />
<View className='settings-page'>
<View className='settings-area'>
+1
View File
@@ -1,5 +1,6 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
// 顶部为深色商品大图,状态栏文字设为白色,确保 iOS 时间/电量可见
navigationBarTextStyle: 'white'
})
+3
View File
@@ -7,6 +7,7 @@ import { ProductCategory } from '../../../types'
import { useSafeArea } from '../../../hooks/useSafeArea'
import { useStatusBar } from '../../../hooks/useStatusBar'
import { useThemeContext } from '../../../context/ThemeContext'
import ThemedPageMeta from '../../../components/ThemedPageMeta'
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
@@ -107,6 +108,7 @@ export default function ShopDetailPage() {
if (!product) {
return (
<View className="shop-detail-page">
<ThemedPageMeta darkTop />
<Text style={{ color: '#fff', padding: '40rpx' }}></Text>
</View>
)
@@ -114,6 +116,7 @@ export default function ShopDetailPage() {
return (
<View className="shop-detail-page">
<ThemedPageMeta darkTop />
{/* Fixed Hero */}
<View className="shop-hero-fixed">
<View
+2 -1
View File
@@ -1,3 +1,4 @@
export default definePageConfig({
navigationStyle: 'custom'
navigationStyle: 'custom',
enablePageMeta: true,
})
+2
View File
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
import './index.scss'
import { PRODUCTS } from '../../utils/productConfig'
@@ -17,6 +18,7 @@ export default function ShopPage() {
return (
<View className={`shop-page theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className="shop-list">
<View className="shop-header" style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
<Text className="shop-header-title"></Text>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '用户数据库'
})
+3
View File
@@ -13,6 +13,7 @@ import {
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function UserDatabasePage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -82,6 +83,7 @@ export default function UserDatabasePage() {
if (!isUnlocked) {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='udb-page'>
<View className='udb-lock-card dashed-card mt-20'>
<View className='star-badge' />
@@ -110,6 +112,7 @@ export default function UserDatabasePage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='udb-page'>
<View className='page-header dashed-card mt-20' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
+1
View File
@@ -1,4 +1,5 @@
export default definePageConfig({
navigationStyle: 'custom',
enablePageMeta: true,
navigationBarTitleText: '词云生成'
})
+2
View File
@@ -5,6 +5,7 @@ import './index.scss'
import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta'
export default function WordCloudPage() {
const { theme, resolvedTheme } = useThemeContext()
@@ -61,6 +62,7 @@ export default function WordCloudPage() {
return (
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<View className='wordcloud-page'>
<View className='page-header flex-between' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
+112
View File
@@ -0,0 +1,112 @@
import Taro from '@tarojs/taro'
import { getMe, logout, getToken } from './api'
import { getUserInfoRaw, clearUserInfo, isMockOpenid } from './store'
const CACHE_KEY = 'smart_auth_verified'
const TTL = 30 * 60 * 1000
interface CacheEntry {
openid: string
ok: boolean
at: number
}
/**
* 登录态全局缓存:首次校验通过后用内存+本地双缓存记住结果,
* 之后进设计清单/订单等受保护页面不再重复请求后端。
* 登录、退出登录、切换账号都会通过 invalidateAuthCache() 清掉缓存。
*/
let verifiedCache: CacheEntry | null = null
let inflight: Promise<boolean> | null = null
export function invalidateAuthCache(): void {
verifiedCache = null
inflight = null
try {
Taro.removeStorageSync(CACHE_KEY)
} catch {
/* ignore */
}
}
function readCache(openid: string): boolean | null {
const entry = verifiedCache || (() => {
try {
const raw = Taro.getStorageSync(CACHE_KEY)
return raw && typeof raw === 'object' ? raw as CacheEntry : null
} catch {
return null
}
})()
if (!entry || entry.openid !== openid) return null
if (Date.now() - entry.at > TTL) return null
verifiedCache = entry
return entry.ok
}
function writeCache(openid: string, ok: boolean): void {
const entry: CacheEntry = { openid, ok, at: Date.now() }
verifiedCache = entry
try {
Taro.setStorageSync(CACHE_KEY, entry)
} catch {
/* ignore */
}
}
/**
* 本地已有真实 openid + token 时视为“本地已登录”。
* 后端校验失败(网络抖动/服务端暂时不可用)不直接锁死页面,
* 由后续 401 或主动登出再重置状态。
*/
function hasLocalSession(): boolean {
const user = getUserInfoRaw()
return !!user?.openid && !isMockOpenid(user.openid) && !!user.accessToken && !!getToken()
}
export async function isAuthVerified(): Promise<boolean> {
const localUser = getUserInfoRaw()
if (!localUser?.openid) {
console.log('[AuthGuard] 无本地用户,判定未登录')
return false
}
console.log('[AuthGuard] 本地用户 openid=', localUser.openid)
if (isMockOpenid(localUser.openid)) {
clearUserInfo()
logout()
invalidateAuthCache()
return false
}
const cached = readCache(localUser.openid)
if (cached !== null) {
console.log('[AuthGuard] 命中本地缓存 ok=', cached)
return cached
}
if (!inflight) {
inflight = getMe()
.then((me) => {
console.log('[AuthGuard] getMe 返回:', JSON.stringify(me))
const consistent = !!me && !!me.id && me.openid === localUser.openid
console.log('[AuthGuard] 一致性=', consistent)
if (consistent) {
writeCache(localUser.openid, true)
return true
}
clearUserInfo()
logout()
invalidateAuthCache()
return false
})
.catch((e) => {
console.error('[AuthGuard] getMe 失败,走本地会话兜底:', e)
return hasLocalSession()
})
.finally(() => {
inflight = null
})
}
return inflight
}
+17 -6
View File
@@ -2,6 +2,7 @@ import Taro from '@tarojs/taro'
import type { DesignItem, OrderItem, AddressItem, StickerItem } from '../types'
import { PRODUCT_ICON_MAP } from './productConfig'
import { invalidateAuthCache } from './authState'
// 重新导出类型,供各页面从 store 直接引用(修正原「声明但不导出」的编译错误)
export type { DesignItem, OrderItem, AddressItem, StickerItem }
@@ -66,6 +67,8 @@ export function setUserInfoRaw(info: any) {
Taro.setStorageSync(`user_info_${info.openid}`, info)
saveToRegistry(info.openid)
}
invalidateAuthCache()
Taro.eventCenter?.trigger('authStateChanged', { openid: info?.openid || '' })
}
export function getUserInfoByOpenid(openid: string): any | null {
@@ -82,6 +85,8 @@ export function clearUserInfo() {
// 仅退出登录,不删除用户数据
Taro.removeStorageSync(USER_KEY)
Taro.removeStorageSync(ACTIVE_USER_KEY)
invalidateAuthCache()
Taro.eventCenter?.trigger('authStateChanged', { openid: '' })
}
export function setUserInfo(info: any) {
@@ -259,17 +264,23 @@ export function setTheme(theme: ThemeMode) {
Taro.setStorageSync(THEME_KEY, theme)
}
// 跟随系统模式下,系统主题由 onThemeChange/getAppBaseInfo 写入缓存,供 resolveTheme 使用
let detectedSystemTheme: 'light' | 'dark' | null = null
export function setDetectedSystemTheme(t: 'light' | 'dark'): void {
detectedSystemTheme = t
}
/** 根据自动模式解析实际生效主题 */
export function resolveTheme(mode: ThemeMode): 'light' | 'dark' {
if (mode === 'auto') {
// 需 app.json 开启 darkmode 后 theme 字段才有效;getSystemInfoSync 已废弃,改用 getAppBaseInfo
// darkmode 开启后 getAppBaseInfo().theme 能拿到系统主题;缓存仅用于启动时加速
if (detectedSystemTheme) return detectedSystemTheme
try {
const info = Taro.getAppBaseInfo()
return info.theme === 'dark' ? 'dark' : 'light'
} catch {
const info = Taro.getSystemInfoSync()
return info.theme === 'dark' ? 'dark' : 'light'
}
if (info.theme === 'dark' || info.theme === 'light') return info.theme
} catch { /* ignore */ }
return 'light'
}
return mode
}