Merge branch 'feat/r4-upload-wordcloud' into master
This commit is contained in:
@@ -18,7 +18,8 @@
|
||||
"Bash(npm ls *)",
|
||||
"Bash(npm install *)",
|
||||
"Bash(node compress-images.js)",
|
||||
"Bash(cd \"F:\\\\\\\\weixin_wordc\")"
|
||||
"Bash(cd \"F:\\\\\\\\weixin_wordc\")",
|
||||
"Bash(find ~/.claude /Users/broccoli/Project/wechat_wc/.claude -path '*skills*' -iname 'SKILL.md' 2>/dev/null | xargs grep -l -i cloudbase 2>/dev/null; echo \"---dirs---\"; find ~/.claude /Users/broccoli/Project/wechat_wc/.claude -type d -iname '*cloudbase*' 2>/dev/null)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
{"darkmode":true,"themeLocation":"theme.json","pages":["pages/index/index","pages/shop/index","pages/shop/detail/index","pages/product/index","pages/diy/index","pages/diy/stickerEdit/index","pages/checkout/index","pages/designList/index","pages/orders/index","pages/profile/index","pages/service/index","pages/wordcloud/index","pages/orderDetail/index","pages/address/index","pages/settings/index","pages/agreement/index","pages/userDatabase/index"],"window":{"backgroundTextStyle":"dark","navigationStyle":"custom","navigationBarBackgroundColor":"#faf7f2","backgroundColor":"#faf7f2","backgroundColorTop":"#faf7f2","backgroundColorBottom":"#faf7f2","navigationBarTextStyle":"black"},"tabBar":{"custom":true,"list":[{"pagePath":"pages/index/index","text":"首页"},{"pagePath":"pages/shop/index","text":"商品"},{"pagePath":"pages/designList/index","text":"设计清单"},{"pagePath":"pages/orders/index","text":"订单"},{"pagePath":"pages/profile/index","text":"我的"}]}}
|
||||
{"darkmode":true,"themeLocation":"theme.json","pages":["pages/index/index","pages/shop/index","pages/shop/detail/index","pages/product/index","pages/diy/index","pages/diy/stickerEdit/index","pages/checkout/index","pages/designList/index","pages/orders/index","pages/profile/index","pages/service/index","pages/wordcloud/index","pages/orderDetail/index","pages/address/index","pages/settings/index","pages/agreement/index","pages/userDatabase/index"],"window":{"navigationStyle":"custom"},"tabBar":{"custom":true,"list":[{"pagePath":"pages/index/index","text":"首页"},{"pagePath":"pages/shop/index","text":"商品"},{"pagePath":"pages/designList/index","text":"设计清单"},{"pagePath":"pages/orders/index","text":"订单"},{"pagePath":"pages/profile/index","text":"我的"}]}}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -1 +1 @@
|
||||
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{653:function(e,n,t){var a=t(7842),i=t(5544),c=t(118),o=t(758),r=t.n(o),s=t(6540),u=t(1457),f=t(9703),g=t(6606),h=t(8199),l=t(4848),v=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}];function p(e){var n=v.findIndex(function(n){return e.endsWith(n.pagePath)});return v[n>=0?n:0].pagePath}function b(){var e=r().getCurrentInstance().router,n=e&&e.path?e.path:"pages/index/index";return p(n)}function m(){var e=(0,s.useState)((0,u.eW)((0,u.O4)())),n=(0,i.A)(e,2),t=n[0],a=n[1],o=(0,s.useState)(b),h=(0,i.A)(o,2),m=h[0],d=h[1];(0,s.useEffect)(function(){var e=function(e){return a(e)};return r().eventCenter.on(f.Wo,e),function(){r().eventCenter.off(f.Wo,e)}},[]),(0,s.useEffect)(function(){var e=function(e){!e||"dark"!==e.theme&&"light"!==e.theme||((0,u.Rq)(e.theme),"auto"===(0,u.O4)()&&a(e.theme))},n=r();return n.onThemeChange&&n.onThemeChange(e),function(){n.offThemeChange&&n.offThemeChange(e)}},[]),(0,s.useEffect)(function(){var e=function(e){var n=r().getCurrentInstance().router,t=n&&n.path?n.path:"pages/index/index";d(p(e||t))};return r().eventCenter.on("tabBarChange",e),function(){r().eventCenter.off("tabBarChange",e)}},[]),(0,s.useEffect)(function(){(0,g.G)(t)},[t]);var x=function(e){d(p(e)),r().switchTab({url:e}),r().eventCenter.trigger("tabBarChange",e)};return(0,l.jsxs)(c.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,l.jsx)(c.Ss,{className:"tab-bar-gradient"}),(0,l.jsx)(c.Ss,{className:"custom-tab-bar theme-".concat(t),children:v.map(function(e){var n=e.pagePath===m;return(0,l.jsxs)(c.Ss,{className:"tab-item ".concat(n?"active":""),onTap:function(){return x(e.pagePath)},children:[n&&(0,l.jsx)(c.Ss,{className:"tab-indicator"}),(0,l.jsx)(c._V,{className:"tab-icon-img ".concat(n?"active":""),src:n?e.iconActive:e.icon,mode:"aspectFit"}),(0,l.jsx)(c.EY,{className:"tab-label",children:e.label})]},e.pagePath)})})]})}Component((0,a.createComponentConfig)(m,"custom-tab-bar/index"))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(653)});e.O()}]);
|
||||
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[777],{7914:function(e,n,t){var a=t(7842),c=t(5544),r=t(118),u=t(758),i=t.n(u),o=t(6540),s=t(1457),l=t(9703),f=t(6606),h=t(8199);function g(e,n,t){return Number.isFinite(e)?Math.min(t,Math.max(n,e)):n}function v(e){return g(e,0,100)}function d(e,n){return!Number.isFinite(e)||n<=0?0:g(Math.floor(v(e)/100*n),0,n-1)}function m(e,n,t){return Math.abs(n-e)>=t}function p(e){var n=e/750;return{left:36*n,width:Math.max(0,e-72*n)}}var b=t(4848),x=[{pagePath:"/pages/index/index",label:"\u9996\u9875",icon:(0,h.uq)("/icon/\u9996\u9875.svg"),iconActive:(0,h.uq)("/icon/\u9996\u9875-fill.svg")},{pagePath:"/pages/shop/index",label:"\u5546\u54c1",icon:(0,h.uq)("/icon/\u5546\u54c1.svg"),iconActive:(0,h.uq)("/icon/\u5546\u54c1-fill.svg")},{pagePath:"/pages/designList/index",label:"\u8bbe\u8ba1\u6e05\u5355",icon:(0,h.uq)("/icon/\u8c03\u8272\u76d8.svg"),iconActive:(0,h.uq)("/icon/\u8c03\u8272\u76d8-fill.svg")},{pagePath:"/pages/orders/index",label:"\u8ba2\u5355",icon:(0,h.uq)("/icon/\u5305\u88f9.svg"),iconActive:(0,h.uq)("/icon/\u5305\u88f9-fill.svg")},{pagePath:"/pages/profile/index",label:"\u6211\u7684",icon:(0,h.uq)("/icon/\u4e2a\u4eba.svg"),iconActive:(0,h.uq)("/icon/\u4e2a\u4eba-fill.svg")}],C=100/x.length,S=12;function P(e){var n=x.findIndex(function(n){return e.endsWith(n.pagePath)});return x[n>=0?n:0].pagePath}function T(){var e=i().getCurrentInstance().router,n=e&&e.path?e.path:"pages/index/index";return P(n)}function q(){var e=(0,o.useState)((0,s.eW)((0,s.O4)())),n=(0,c.A)(e,2),t=n[0],a=n[1],u=(0,o.useState)(T),h=(0,c.A)(u,2),g=h[0],q=h[1],N=(0,o.useState)(null),w=(0,c.A)(N,2),A=w[0],j=w[1],M=(0,o.useState)(!1),E=(0,c.A)(M,2),I=E[0],R=E[1],W=(0,o.useRef)(!1),k=(0,o.useRef)(null),O=(0,o.useRef)(0),X=(0,o.useRef)(null);(0,o.useEffect)(function(){var e=function(e){return a(e)};return i().eventCenter.on(l.Wo,e),function(){i().eventCenter.off(l.Wo,e)}},[]),(0,o.useEffect)(function(){var e=function(e){!e||"dark"!==e.theme&&"light"!==e.theme||((0,s.Rq)(e.theme),"auto"===(0,s.O4)()&&a(e.theme))},n=i();return n.onThemeChange&&n.onThemeChange(e),function(){n.offThemeChange&&n.offThemeChange(e)}},[]),(0,o.useEffect)(function(){var e=function(e){var n=i().getCurrentInstance().router,t=n&&n.path?n.path:"pages/index/index";q(P(e||t))};return i().eventCenter.on("tabBarChange",e),function(){i().eventCenter.off("tabBarChange",e)}},[]),(0,o.useEffect)(function(){(0,f.G)(t)},[t]);var y=function(e){q(P(e)),i().switchTab({url:e}),i().eventCenter.trigger("tabBarChange",e)},B=function(e){var n=e.touches&&e.touches[0];if(n){var t=p(i().getSystemInfoSync().windowWidth);X.current=t,O.current=n.clientX,W.current=!1,R(!1),k.current=null,j(null)}},F=function(e){var n=X.current,t=e.touches&&e.touches[0];if(n&&t){if(!W.current){if(!m(O.current,t.clientX,S))return;W.current=!0,R(!0)}var a=v((t.clientX-n.left)/n.width*100);k.current=a,j(a)}},J=function(e){var n=X.current;if(n){var t;if(W.current){var a;t=d(null!==(a=k.current)&&void 0!==a?a:0,x.length),W.current=!1,R(!1),i().nextTick(function(){k.current=null,j(null)})}else{var c=e.changedTouches&&e.changedTouches[0],r=c?c.clientX:O.current;t=d(v((r-n.left)/n.width*100),x.length)}var u=x[t];u&&u.pagePath!==g&&y(u.pagePath),X.current=null,O.current=0}},G=function(){X.current=null,O.current=0,W.current=!1,R(!1),k.current=null,j(null)},L=Math.max(0,x.findIndex(function(e){return e.pagePath===g}));return(0,b.jsxs)(r.Ss,{className:"tab-bar-root theme-".concat(t),children:[(0,b.jsx)(r.Ss,{className:"tab-bar-gradient"}),(0,b.jsxs)(r.Ss,{className:"custom-tab-bar theme-".concat(t),onTouchStart:B,onTouchMove:F,onTouchEnd:J,onTouchCancel:G,catchMove:!0,children:[(0,b.jsx)(r.Ss,{className:"tab-bar-caustic"}),(0,b.jsx)(r.Ss,{className:"tab-bar-rim"}),(0,b.jsx)(r.Ss,{className:"tab-indicator ".concat(I?"dragging":""),style:{left:"".concat(null===A?(L+.5)*C:A,"%")}}),x.map(function(e){var n=e.pagePath===g;return(0,b.jsxs)(r.Ss,{className:"tab-item ".concat(n?"active":""),children:[(0,b.jsx)(r._V,{className:"tab-icon-img ".concat(n?"active":""),src:n?e.iconActive:e.icon,mode:"aspectFit"}),(0,b.jsx)(r.EY,{className:"tab-label",children:e.label})]},e.pagePath)})]})]})}Component((0,a.createComponentConfig)(q,"custom-tab-bar/index"))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(7914)});e.O()}]);
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-9
@@ -23,16 +23,9 @@ export default defineAppConfig({
|
||||
'pages/userDatabase/index'
|
||||
],
|
||||
window: {
|
||||
backgroundTextStyle: 'dark',
|
||||
// 初始窗口/导航背景交给 theme.json 按系统外观自动选择,
|
||||
// 避免首帧先固定成浅色再被 JS 切到深色而闪烁。
|
||||
navigationStyle: 'custom',
|
||||
// 顶部/窗口颜色先显式固定为小程序浅色主题,避免 iOS 顶部区域跟随系统深色;
|
||||
// 应用启动后页面 Hook 会按小程序内主题实时覆盖上面的值。
|
||||
navigationBarBackgroundColor: '#faf7f2',
|
||||
backgroundColor: '#faf7f2',
|
||||
backgroundColorTop: '#faf7f2',
|
||||
backgroundColorBottom: '#faf7f2',
|
||||
// 状态栏文字默认黑字;各页面 useStatusBar 会按小程序主题覆盖它。
|
||||
navigationBarTextStyle: 'black',
|
||||
},
|
||||
tabBar: {
|
||||
custom: true,
|
||||
|
||||
+28
-8
@@ -209,19 +209,39 @@ page {
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
border-radius: 50% !important;
|
||||
background: rgba(255, 255, 255, 0.55) !important;
|
||||
backdrop-filter: blur(32px) saturate(1.2) !important;
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.2) !important;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.55) !important;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.18), 0 4rpx 12rpx rgba(0, 0, 0, 0.08) !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.55) 0%,
|
||||
rgba(255, 255, 255, 0.28) 45%,
|
||||
rgba(255, 255, 255, 0.42) 100%
|
||||
) !important;
|
||||
backdrop-filter: blur(24px) saturate(1.7) brightness(1.05) !important;
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.7) brightness(1.05) !important;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.35) !important;
|
||||
box-shadow:
|
||||
inset 0 2rpx 3rpx rgba(255, 255, 255, 0.65),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.06),
|
||||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.18),
|
||||
0 12rpx 40rpx rgba(0, 0, 0, 0.18),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.08) !important;
|
||||
}
|
||||
|
||||
.theme-dark .back-btn,
|
||||
.theme-dark .edit-back,
|
||||
.theme-dark .shop-back-btn {
|
||||
background: rgba(25, 25, 25, 0.55) !important;
|
||||
border-color: rgba(255, 255, 255, 0.14) !important;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.5), 0 4rpx 12rpx rgba(0, 0, 0, 0.3) !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.16) 0%,
|
||||
rgba(255, 255, 255, 0.06) 45%,
|
||||
rgba(255, 255, 255, 0.12) 100%
|
||||
) !important;
|
||||
border-color: rgba(255, 255, 255, 0.18) !important;
|
||||
box-shadow:
|
||||
inset 0 2rpx 3rpx rgba(255, 255, 255, 0.22),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.28),
|
||||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.1),
|
||||
0 12rpx 40rpx rgba(0, 0, 0, 0.5),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================
|
||||
|
||||
@@ -7,6 +7,11 @@ import { refreshSession, ensureSessionFresh } from './utils/session'
|
||||
import './app.scss'
|
||||
|
||||
class App extends Component<PropsWithChildren> {
|
||||
componentWillMount() {
|
||||
// 首帧前先把原生窗口背景切成当前主题,避免深色/浅色下先闪系统默认色
|
||||
applyPageBackground(resolveTheme(getTheme()))
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// 启动最早时机同步页面背景,避免自定义导航区在深色模式下闪白
|
||||
applyPageBackground(resolveTheme(getTheme()))
|
||||
|
||||
@@ -44,17 +44,37 @@
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(32px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.18), 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.55) 0%,
|
||||
rgba(255, 255, 255, 0.28) 45%,
|
||||
rgba(255, 255, 255, 0.42) 100%
|
||||
);
|
||||
backdrop-filter: blur(24px) saturate(1.7) brightness(1.05);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.7) brightness(1.05);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
inset 0 2rpx 3rpx rgba(255, 255, 255, 0.65),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.06),
|
||||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.18),
|
||||
0 12rpx 40rpx rgba(0, 0, 0, 0.18),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.theme-dark .scroll-top-back {
|
||||
background: rgba(25, 25, 25, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.5), 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.16) 0%,
|
||||
rgba(255, 255, 255, 0.06) 45%,
|
||||
rgba(255, 255, 255, 0.12) 100%
|
||||
);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
box-shadow:
|
||||
inset 0 2rpx 3rpx rgba(255, 255, 255, 0.22),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.28),
|
||||
inset 0 0 0 1rpx rgba(255, 255, 255, 0.1),
|
||||
0 12rpx 40rpx rgba(0, 0, 0, 0.5),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.scroll-top-back-arrow {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
|
||||
import { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react'
|
||||
import Taro, { useDidShow } from '@tarojs/taro'
|
||||
import { View } from '@tarojs/components'
|
||||
import { getTheme, setTheme as saveTheme, resolveTheme, setDetectedSystemTheme, type ThemeMode } from '../utils/store'
|
||||
@@ -31,6 +31,7 @@ type ThemeChangeApi = {
|
||||
export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [theme, set] = useState<ThemeMode>(getTheme())
|
||||
const [resolvedTheme, setResolved] = useState<'light' | 'dark'>(resolveTheme(getTheme()))
|
||||
const systemThemeRef = useRef<'light' | 'dark' | null>(null)
|
||||
|
||||
const updateResolved = useCallback((mode: ThemeMode) => {
|
||||
setResolved(resolveTheme(mode))
|
||||
@@ -42,6 +43,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
*/
|
||||
const updateFromSystem = useCallback((t: 'light' | 'dark') => {
|
||||
setDetectedSystemTheme(t)
|
||||
systemThemeRef.current = t
|
||||
if (getTheme() === 'auto') {
|
||||
setResolved(t)
|
||||
}
|
||||
@@ -102,6 +104,23 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
}, [updateFromSystem])
|
||||
|
||||
// 部分机型/工具上 onThemeChange 不回调,自动模式再轮询兜底,保证系统切换能跟上
|
||||
useEffect(() => {
|
||||
if (theme !== 'auto') return
|
||||
const timer = setInterval(() => {
|
||||
try {
|
||||
const info = Taro.getAppBaseInfo()
|
||||
const next = info.theme === 'dark' ? 'dark' : 'light'
|
||||
if (next !== systemThemeRef.current) {
|
||||
updateFromSystem(next)
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, 2000)
|
||||
return () => clearInterval(timer)
|
||||
}, [theme, updateFromSystem])
|
||||
|
||||
// 广播实际生效主题,供 custom-tab-bar 等独立组件订阅
|
||||
useEffect(() => {
|
||||
Taro.eventCenter.trigger(THEME_CHANGE_EVENT, resolvedTheme)
|
||||
|
||||
@@ -32,25 +32,84 @@
|
||||
border-radius: 64rpx;
|
||||
height: 116rpx;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(32px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.2);
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.18), 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.14);
|
||||
box-shadow:
|
||||
0 24rpx 60rpx rgba(20, 14, 10, 0.22),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.08),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.35);
|
||||
pointer-events: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 自定义组件样式隔离:两套主题配色在组件内自包含,不依赖全局 app.wxss */
|
||||
.custom-tab-bar.theme-dark {
|
||||
background: rgba(25, 25, 25, 0.55);
|
||||
backdrop-filter: blur(32px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.2);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.5), 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
background: rgba(25, 25, 25, 0.24);
|
||||
backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.7) brightness(1.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 24rpx 60rpx rgba(0, 0, 0, 0.55),
|
||||
0 4rpx 12rpx rgba(0, 0, 0, 0.3),
|
||||
inset 0 2rpx 0 rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* 顶部弯月面聚光带 */
|
||||
.tab-bar-caustic {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56rpx;
|
||||
border-radius: 64rpx 64rpx 0 0;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.32), rgba(255, 255, 255, 0));
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.55);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.custom-tab-bar.theme-dark .tab-bar-caustic {
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0));
|
||||
box-shadow: inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
/* 非均匀银丝玻璃边缘 */
|
||||
.tab-bar-rim {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1rpx;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.6) 0%,
|
||||
rgba(255, 255, 255, 0.18) 32%,
|
||||
rgba(255, 255, 255, 0.03) 68%,
|
||||
rgba(255, 255, 255, 0.24) 100%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.custom-tab-bar.theme-dark .tab-bar-rim {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.34) 0%,
|
||||
rgba(255, 255, 255, 0.12) 32%,
|
||||
rgba(255, 255, 255, 0.02) 68%,
|
||||
rgba(255, 255, 255, 0.16) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
@@ -63,23 +122,38 @@
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
max-width: 160px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 6rpx;
|
||||
width: 120rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50rpx;
|
||||
background: rgba(0, 0, 0, 0.14);
|
||||
box-shadow: 0 0 6rpx rgba(0, 0, 0, 0.07);
|
||||
pointer-events: none;
|
||||
z-index: 4;
|
||||
transition: left 0.25s ease;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.16));
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.25);
|
||||
box-shadow:
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.35),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.06),
|
||||
0 4rpx 10rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.custom-tab-bar.theme-dark .tab-indicator {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 0 6rpx rgba(255, 255, 255, 0.07);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.16));
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
box-shadow:
|
||||
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.12),
|
||||
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.3),
|
||||
0 4rpx 10rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.tab-indicator.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.tab-icon-img {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getTheme, resolveTheme, setDetectedSystemTheme } from '../utils/store'
|
||||
import { THEME_CHANGE_EVENT } from '../context/ThemeContext'
|
||||
import { applyPageBackground } from '../utils/themeBackground'
|
||||
import { assetUrl } from '../utils/asset'
|
||||
import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from './tabBarGeometry'
|
||||
import './index.scss'
|
||||
|
||||
const TABS = [
|
||||
@@ -15,6 +16,9 @@ const TABS = [
|
||||
{ pagePath: '/pages/profile/index', label: '我的', icon: assetUrl('/icon/个人.svg'), iconActive: assetUrl('/icon/个人-fill.svg') }
|
||||
]
|
||||
|
||||
const TAB_CENTER_STEP = 100 / TABS.length
|
||||
const DRAG_THRESHOLD_PX = 12
|
||||
|
||||
function matchTab(path: string): string {
|
||||
const idx = TABS.findIndex((t) => path.endsWith(t.pagePath))
|
||||
return TABS[idx >= 0 ? idx : 0].pagePath
|
||||
@@ -36,6 +40,12 @@ export default function CustomTabBar() {
|
||||
|
||||
// 微信自定义 tabBar 不会自动感知 page 路由变化,用 state 保持选中态响应式
|
||||
const [activeTab, setActiveTab] = useState<string>(currentTabPath)
|
||||
const [dragPercent, setDragPercent] = useState<number | null>(null)
|
||||
const [isDragging, setIsDragging] = useState<boolean>(false)
|
||||
const isDraggingRef = useRef<boolean>(false)
|
||||
const dragPercentRef = useRef<number | null>(null)
|
||||
const startXRef = useRef<number>(0)
|
||||
const barFrameRef = useRef<{ left: number; width: number } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (t: 'light' | 'dark') => setResolvedTheme(t)
|
||||
@@ -91,19 +101,87 @@ export default function CustomTabBar() {
|
||||
Taro.eventCenter.trigger('tabBarChange', url)
|
||||
}
|
||||
|
||||
const startDrag = (e: any) => {
|
||||
const touch = e.touches && e.touches[0]
|
||||
if (!touch) return
|
||||
const frame = getTabBarFrame(Taro.getSystemInfoSync().windowWidth)
|
||||
barFrameRef.current = frame
|
||||
startXRef.current = touch.clientX
|
||||
isDraggingRef.current = false
|
||||
setIsDragging(false)
|
||||
dragPercentRef.current = null
|
||||
setDragPercent(null)
|
||||
}
|
||||
|
||||
const moveDrag = (e: any) => {
|
||||
const frame = barFrameRef.current
|
||||
const touch = e.touches && e.touches[0]
|
||||
if (!frame || !touch) return
|
||||
if (!isDraggingRef.current) {
|
||||
if (!hasDragged(startXRef.current, touch.clientX, DRAG_THRESHOLD_PX)) return
|
||||
isDraggingRef.current = true
|
||||
setIsDragging(true)
|
||||
}
|
||||
const nextPercent = clampPercent(((touch.clientX - frame.left) / frame.width) * 100)
|
||||
dragPercentRef.current = nextPercent
|
||||
setDragPercent(nextPercent)
|
||||
}
|
||||
|
||||
const endDrag = (e: any) => {
|
||||
const frame = barFrameRef.current
|
||||
if (!frame) return
|
||||
let targetIndex: number
|
||||
if (isDraggingRef.current) {
|
||||
targetIndex = tabIndexAtPercent(dragPercentRef.current ?? 0, TABS.length)
|
||||
isDraggingRef.current = false
|
||||
setIsDragging(false)
|
||||
Taro.nextTick(() => {
|
||||
dragPercentRef.current = null
|
||||
setDragPercent(null)
|
||||
})
|
||||
} else {
|
||||
const touch = e.changedTouches && e.changedTouches[0]
|
||||
const x = touch ? touch.clientX : startXRef.current
|
||||
targetIndex = tabIndexAtPercent(clampPercent(((x - frame.left) / frame.width) * 100), TABS.length)
|
||||
}
|
||||
const target = TABS[targetIndex]
|
||||
if (target && target.pagePath !== activeTab) switchTab(target.pagePath)
|
||||
barFrameRef.current = null
|
||||
startXRef.current = 0
|
||||
}
|
||||
|
||||
const cancelDrag = () => {
|
||||
barFrameRef.current = null
|
||||
startXRef.current = 0
|
||||
isDraggingRef.current = false
|
||||
setIsDragging(false)
|
||||
dragPercentRef.current = null
|
||||
setDragPercent(null)
|
||||
}
|
||||
|
||||
const activeIndex = Math.max(0, TABS.findIndex((t) => t.pagePath === activeTab))
|
||||
|
||||
return (
|
||||
<View className={`tab-bar-root theme-${resolvedTheme}`}>
|
||||
<View className='tab-bar-gradient' />
|
||||
<View className={`custom-tab-bar theme-${resolvedTheme}`}>
|
||||
<View
|
||||
className={`custom-tab-bar theme-${resolvedTheme}`}
|
||||
onTouchStart={startDrag}
|
||||
onTouchMove={moveDrag}
|
||||
onTouchEnd={endDrag}
|
||||
onTouchCancel={cancelDrag}
|
||||
catchMove
|
||||
>
|
||||
<View className='tab-bar-caustic' />
|
||||
<View className='tab-bar-rim' />
|
||||
<View
|
||||
className={`tab-indicator ${isDragging ? 'dragging' : ''}`}
|
||||
style={{ left: dragPercent === null ? `${(activeIndex + 0.5) * TAB_CENTER_STEP}%` : `${dragPercent}%` }}
|
||||
/>
|
||||
{TABS.map((tab) => {
|
||||
const isActive = tab.pagePath === activeTab
|
||||
return (
|
||||
<View
|
||||
key={tab.pagePath}
|
||||
className={`tab-item ${isActive ? 'active' : ''}`}
|
||||
onTap={() => switchTab(tab.pagePath)}
|
||||
>
|
||||
{isActive && <View className='tab-indicator' />}
|
||||
<View key={tab.pagePath} className={`tab-item ${isActive ? 'active' : ''}`}>
|
||||
<Image className={`tab-icon-img ${isActive ? 'active' : ''}`} src={isActive ? tab.iconActive : tab.icon} mode='aspectFit' />
|
||||
<Text className='tab-label'>{tab.label}</Text>
|
||||
</View>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function clamp(value, min, max) {
|
||||
if (!Number.isFinite(value)) return min
|
||||
return Math.min(max, Math.max(min, value))
|
||||
}
|
||||
|
||||
export function clampPercent(value) {
|
||||
return clamp(value, 0, 100)
|
||||
}
|
||||
|
||||
export function tabIndexAtPercent(percent, tabCount) {
|
||||
if (!Number.isFinite(percent) || tabCount <= 0) return 0
|
||||
return clamp(Math.floor((clampPercent(percent) / 100) * tabCount), 0, tabCount - 1)
|
||||
}
|
||||
|
||||
export function hasDragged(startX, currentX, threshold) {
|
||||
return Math.abs(currentX - startX) >= threshold
|
||||
}
|
||||
|
||||
export function getTabBarFrame(windowWidth) {
|
||||
const rpx = windowWidth / 750
|
||||
return { left: 36 * rpx, width: Math.max(0, windowWidth - 72 * rpx) }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
import { clampPercent, getTabBarFrame, hasDragged, tabIndexAtPercent } from '../src/custom-tab-bar/tabBarGeometry.js'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
|
||||
test('maps a drag percent to the tab under the finger', () => {
|
||||
assert.equal(tabIndexAtPercent(10, 5), 0)
|
||||
assert.equal(tabIndexAtPercent(30, 5), 1)
|
||||
assert.equal(tabIndexAtPercent(50, 5), 2)
|
||||
assert.equal(tabIndexAtPercent(70, 5), 3)
|
||||
assert.equal(tabIndexAtPercent(90, 5), 4)
|
||||
})
|
||||
|
||||
test('clamps percent and maps out-of-range to edge tabs', () => {
|
||||
assert.equal(clampPercent(-5), 0)
|
||||
assert.equal(clampPercent(120), 100)
|
||||
assert.equal(clampPercent(Number.NaN), 0)
|
||||
assert.equal(tabIndexAtPercent(-5, 5), 0)
|
||||
assert.equal(tabIndexAtPercent(120, 5), 4)
|
||||
})
|
||||
|
||||
test('only treats a move as a drag beyond the threshold', () => {
|
||||
assert.equal(hasDragged(100, 110, 12), false)
|
||||
assert.equal(hasDragged(100, 112, 12), true)
|
||||
assert.equal(hasDragged(100, 88, 12), true)
|
||||
})
|
||||
|
||||
test('derives the floating bar frame from window width', () => {
|
||||
assert.deepEqual(getTabBarFrame(375), { left: 18, width: 339 })
|
||||
assert.deepEqual(getTabBarFrame(750), { left: 36, width: 678 })
|
||||
})
|
||||
|
||||
test('enables the slide transition before snapping the indicator to the target', () => {
|
||||
const source = readFileSync(path.join(root, 'src/custom-tab-bar/index.tsx'), 'utf8')
|
||||
assert.match(
|
||||
source,
|
||||
/isDraggingRef\.current = false\s+setIsDragging\(false\)\s+Taro\.nextTick\(\(\) => \{[\s\S]*?setDragPercent\(null\)/
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const read = (rel) => readFileSync(path.join(root, rel), 'utf8')
|
||||
|
||||
test('custom tab bar renders the liquid-glass overlay layers', () => {
|
||||
const source = read('src/custom-tab-bar/index.tsx')
|
||||
assert.match(source, /tab-bar-caustic/)
|
||||
assert.match(source, /tab-bar-rim/)
|
||||
})
|
||||
|
||||
test('custom tab bar uses bright glass material and the silver rim', () => {
|
||||
const css = read('src/custom-tab-bar/index.scss')
|
||||
assert.match(css, /background: rgba\(255, 255, 255, 0\.24\)/)
|
||||
assert.match(css, /background: rgba\(25, 25, 25, 0\.24\)/)
|
||||
assert.match(css, /backdrop-filter: blur\(32px\) saturate\(1\.7\) brightness\(1\.05\)/)
|
||||
assert.match(css, /-webkit-backdrop-filter: blur\(32px\) saturate\(1\.7\) brightness\(1\.05\)/)
|
||||
assert.match(css, /mask-composite: exclude/)
|
||||
assert.match(css, /linear-gradient\(\s*135deg/)
|
||||
})
|
||||
|
||||
test('global back buttons use the liquid-glass gradient and inner shading', () => {
|
||||
const css = read('src/app.scss')
|
||||
assert.match(css, /background: linear-gradient\(\s*135deg/)
|
||||
assert.match(css, /backdrop-filter: blur\(24px\) saturate\(1\.7\) brightness\(1\.05\)/)
|
||||
assert.match(css, /\.theme-dark \.back-btn/)
|
||||
})
|
||||
|
||||
test('scroll top back button matches the global liquid-glass back style', () => {
|
||||
const css = read('src/components/ScrollTopMask/index.scss')
|
||||
assert.match(css, /background: linear-gradient\(\s*135deg/)
|
||||
assert.match(css, /backdrop-filter: blur\(24px\) saturate\(1\.7\) brightness\(1\.05\)/)
|
||||
})
|
||||
|
||||
test('app sets page background before first render without a blinking full fade', () => {
|
||||
const appSource = read('src/app.tsx')
|
||||
const appCss = read('src/app.scss')
|
||||
assert.match(appSource, /componentWillMount/)
|
||||
assert.doesNotMatch(appCss, /animation:\s*page-fade-in/)
|
||||
})
|
||||
|
||||
test('delegates initial background to themeLocation instead of hardcoding light', () => {
|
||||
const config = read('src/app.config.ts')
|
||||
assert.match(config, /themeLocation:\s*'theme\.json'/)
|
||||
assert.doesNotMatch(config, /navigationBarBackgroundColor:\s*'#faf7f2'/)
|
||||
assert.doesNotMatch(config, /backgroundColorTop:\s*'#faf7f2'/)
|
||||
})
|
||||
|
||||
test('polls the system theme while following the system', () => {
|
||||
const ctx = read('src/context/ThemeContext.tsx')
|
||||
assert.match(ctx, /setInterval/)
|
||||
assert.match(ctx, /getAppBaseInfo\(\)/)
|
||||
})
|
||||
Reference in New Issue
Block a user