feat: add scroll top mask, unify page top spacing and docs

This commit is contained in:
2026-08-08 03:57:47 +08:00
parent 38045073b0
commit a3c3005c04
73 changed files with 1350 additions and 169 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.bottom-action-bar{-ms-flex-align:center;-webkit-align-items:center;align-items:center;backdrop-filter:blur(32rpx) saturate(1.2);-webkit-backdrop-filter:blur(32rpx) saturate(1.2);background:hsla(0,0%,100%,.55);border:1rpx solid hsla(0,0%,100%,.55);border-radius:64rpx;bottom:36rpx;-webkit-box-shadow:0 12rpx 40rpx rgba(46,34,34,.18),0 4rpx 12rpx rgba(46,34,34,.08);box-shadow:0 12rpx 40rpx rgba(46,34,34,.18),0 4rpx 12rpx rgba(46,34,34,.08);-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:24rpx;left:36rpx;padding:24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));position:fixed;right:36rpx;z-index:500}.theme-dark .bottom-action-bar{background:rgba(25,25,25,.55);border-color:hsla(0,0%,100%,.14);-webkit-box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3);box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3)} .scroll-top-mask{left:0;pointer-events:none;position:fixed;right:0;top:0;-webkit-transform:translateZ(0);transform:translateZ(0);z-index:40}.scroll-top-blur,.scroll-top-gradient{height:100%;left:0;pointer-events:none;position:absolute;right:0;top:0}.scroll-top-gradient{height:80%}.scroll-top-title{bottom:38%;color:var(--text-primary);font-size:36rpx;font-weight:600;left:0;letter-spacing:0;pointer-events:none;position:absolute;right:0;text-align:center}.scroll-top-back{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;backdrop-filter:blur(32rpx) saturate(1.2);-webkit-backdrop-filter:blur(32rpx) saturate(1.2);background:hsla(0,0%,100%,.55);border:1rpx solid hsla(0,0%,100%,.55);border-radius:50%;-webkit-box-shadow:0 12rpx 40rpx rgba(0,0,0,.18),0 4rpx 12rpx rgba(0,0,0,.08);box-shadow:0 12rpx 40rpx rgba(0,0,0,.18),0 4rpx 12rpx rgba(0,0,0,.08);display:-webkit-flex;display:-ms-flexbox;display:flex;height:80rpx;-webkit-justify-content:center;justify-content:center;left:30rpx;pointer-events:auto;position:absolute;width:80rpx}.theme-dark .scroll-top-back{background:rgba(25,25,25,.55);border-color:hsla(0,0%,100%,.14);-webkit-box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3);box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3)}.scroll-top-back-arrow{color:var(--text-primary);font-size:44rpx;font-weight:600;line-height:1}.scroll-top-blur-mid{backdrop-filter:blur(12rpx);-webkit-backdrop-filter:blur(12rpx);-webkit-mask-image:-webkit-gradient(linear,left top,left bottom,from(#000),color-stop(42%,rgba(0,0,0,.8)),color-stop(76%,rgba(0,0,0,0)));-webkit-mask-image:-webkit-linear-gradient(top,#000,rgba(0,0,0,.8) 42%,rgba(0,0,0,0) 76%);mask-image:-webkit-gradient(linear,left top,left bottom,from(#000),color-stop(42%,rgba(0,0,0,.8)),color-stop(76%,rgba(0,0,0,0)));mask-image:linear-gradient(180deg,#000 0,rgba(0,0,0,.8) 42%,rgba(0,0,0,0) 76%)}.scroll-top-blur-soft{backdrop-filter:blur(6rpx);-webkit-backdrop-filter:blur(6rpx);-webkit-mask-image:-webkit-gradient(linear,left top,left bottom,from(#000),color-stop(52%,rgba(0,0,0,.65)),to(rgba(0,0,0,0)));-webkit-mask-image:-webkit-linear-gradient(top,#000,rgba(0,0,0,.65) 52%,rgba(0,0,0,0));mask-image:-webkit-gradient(linear,left top,left bottom,from(#000),color-stop(52%,rgba(0,0,0,.65)),to(rgba(0,0,0,0)));mask-image:linear-gradient(180deg,#000 0,rgba(0,0,0,.65) 52%,rgba(0,0,0,0))}.theme-light .scroll-top-gradient{background:-webkit-gradient(linear,left top,left bottom,from(hsla(38,44%,96%,.94)),color-stop(50%,hsla(38,44%,96%,.9)),color-stop(78%,hsla(38,44%,96%,.58)),to(hsla(38,44%,96%,0)));background:-webkit-linear-gradient(top,hsla(38,44%,96%,.94),hsla(38,44%,96%,.9) 50%,hsla(38,44%,96%,.58) 78%,hsla(38,44%,96%,0));background:linear-gradient(180deg,hsla(38,44%,96%,.94) 0,hsla(38,44%,96%,.9) 50%,hsla(38,44%,96%,.58) 78%,hsla(38,44%,96%,0))}.theme-dark .scroll-top-gradient{background:-webkit-gradient(linear,left top,left bottom,from(rgba(25,25,25,.94)),color-stop(50%,rgba(25,25,25,.9)),color-stop(78%,rgba(25,25,25,.58)),to(rgba(25,25,25,0)));background:-webkit-linear-gradient(top,rgba(25,25,25,.94),rgba(25,25,25,.9) 50%,rgba(25,25,25,.58) 78%,rgba(25,25,25,0));background:linear-gradient(180deg,rgba(25,25,25,.94) 0,rgba(25,25,25,.9) 50%,rgba(25,25,25,.58) 78%,rgba(25,25,25,0))}.bottom-action-bar{-ms-flex-align:center;-webkit-align-items:center;align-items:center;backdrop-filter:blur(32rpx) saturate(1.2);-webkit-backdrop-filter:blur(32rpx) saturate(1.2);background:hsla(0,0%,100%,.55);border:1rpx solid hsla(0,0%,100%,.55);border-radius:64rpx;bottom:36rpx;-webkit-box-shadow:0 12rpx 40rpx rgba(46,34,34,.18),0 4rpx 12rpx rgba(46,34,34,.08);box-shadow:0 12rpx 40rpx rgba(46,34,34,.18),0 4rpx 12rpx rgba(46,34,34,.08);-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:24rpx;left:36rpx;padding:24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));position:fixed;right:36rpx;z-index:500}.theme-dark .bottom-action-bar{background:rgba(25,25,25,.55);border-color:hsla(0,0%,100%,.14);-webkit-box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3);box-shadow:0 12rpx 40rpx rgba(0,0,0,.5),0 4rpx 12rpx rgba(0,0,0,.3)}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[301],{6855:function(e,n,t){var a=t(7842),s=t(118),c=t(9703),r=t(9201),i=t(5414),l=t(9674),o=t(4848);function m(){var e=(0,c.wR)(),n=(e.theme,e.resolvedTheme),t=(0,r.c)();(0,i.l)(n);var a=[{title:"\u4e00\u3001\u7528\u6237\u6743\u8d23",content:"\u7528\u6237\u786e\u8ba4\u63d0\u4ea4\u7684\u8bbe\u8ba1\u5185\u5bb9\u4e0d\u4fb5\u72af\u7b2c\u4e09\u65b9\u77e5\u8bc6\u4ea7\u6743\uff0c\u5b9a\u5236\u5546\u54c1\u4e00\u7ecf\u786e\u8ba4\u4e0b\u5355\u5373\u8fdb\u5165\u751f\u4ea7\u6d41\u7a0b\uff0c\u975e\u8d28\u91cf\u95ee\u9898\u4e0d\u652f\u6301\u9000\u6362\u3002"},{title:"\u4e8c\u3001\u9690\u79c1\u4fdd\u62a4",content:"\u6211\u4eec\u4e25\u683c\u4fdd\u62a4\u7528\u6237\u4e2a\u4eba\u4fe1\u606f\uff0c\u5934\u50cf\u4e0e\u6635\u79f0\u4ec5\u7528\u4e8e\u63d0\u5347\u670d\u52a1\u4f53\u9a8c\uff0c\u4e0d\u4e0e\u7b2c\u4e09\u65b9\u5206\u4eab\u3002"},{title:"\u4e09\u3001\u552e\u540e\u8bf4\u660e",content:"\u6536\u5230\u5546\u54c1\u540e\u5982\u6709\u8d28\u91cf\u95ee\u9898\uff0c\u8bf7\u57287\u5929\u5185\u8054\u7cfb\u5ba2\u670d\uff0c\u6211\u4eec\u5c06\u4e3a\u60a8\u514d\u8d39\u91cd\u505a\u6216\u9000\u6b3e\u3002"},{title:"\u56db\u3001\u5b9a\u5236\u6d41\u7a0b",content:"1. \u9009\u62e9\u5546\u54c1\u54c1\u7c7b\u5e76\u52a0\u5165\u8bbe\u8ba1\u6e05\u5355\n2. \u5728DIY\u5de5\u4f5c\u53f0\u4e0a\u4f20\u56fe\u7247\u6216\u8f93\u5165\u6587\u5b57\n3. \u786e\u8ba4\u8bbe\u8ba1\u6548\u679c\u5e76\u63d0\u4ea4\u8ba2\u5355\n4. \u652f\u4ed8\u540e\u8fdb\u5165\u751f\u4ea7\uff0c\u7ea63-7\u4e2a\u5de5\u4f5c\u65e5\u53d1\u8d27\n5. \u7269\u6d41\u8ddf\u8e2a\u81f3\u786e\u8ba4\u6536\u8d27"}];return(0,o.jsxs)(s.Ss,{className:"theme-".concat(n),children:[(0,o.jsx)(l.A,{}),(0,o.jsx)(s.Ss,{className:"agreement-page",children:(0,o.jsxs)(s.Ss,{className:"agreement-content",style:{marginTop:"".concat(t.headerPaddingTop,"px")},children:[(0,o.jsx)(s.EY,{className:"agreement-title",children:"\u5b9a\u5236\u534f\u8bae"}),a.map(function(e,n){return(0,o.jsxs)(s.Ss,{className:"agreement-section surface-card",children:[(0,o.jsx)(s.EY,{className:"agreement-h3",children:e.title}),(0,o.jsx)(s.EY,{className:"agreement-p",children:e.content})]},n)})]})})]})}var g={navigationStyle:"custom",enablePageMeta:!0,navigationBarTitleText:"\u5b9a\u5236\u534f\u8bae"};Page((0,a.createPageConfig)(m,"pages/agreement/index",{root:{cn:[]}},g||{}))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(6855)});e.O()}]); "use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[301],{6855:function(e,t,n){var a=n(7842),s=n(118),c=n(9703),r=n(9201),i=n(5414),l=n(9674),o=n(3053),m=n(4848);function g(){var e=(0,c.wR)(),t=(e.theme,e.resolvedTheme),n=(0,r.c)();(0,i.l)(t);var a=[{title:"\u4e00\u3001\u7528\u6237\u6743\u8d23",content:"\u7528\u6237\u786e\u8ba4\u63d0\u4ea4\u7684\u8bbe\u8ba1\u5185\u5bb9\u4e0d\u4fb5\u72af\u7b2c\u4e09\u65b9\u77e5\u8bc6\u4ea7\u6743\uff0c\u5b9a\u5236\u5546\u54c1\u4e00\u7ecf\u786e\u8ba4\u4e0b\u5355\u5373\u8fdb\u5165\u751f\u4ea7\u6d41\u7a0b\uff0c\u975e\u8d28\u91cf\u95ee\u9898\u4e0d\u652f\u6301\u9000\u6362\u3002"},{title:"\u4e8c\u3001\u9690\u79c1\u4fdd\u62a4",content:"\u6211\u4eec\u4e25\u683c\u4fdd\u62a4\u7528\u6237\u4e2a\u4eba\u4fe1\u606f\uff0c\u5934\u50cf\u4e0e\u6635\u79f0\u4ec5\u7528\u4e8e\u63d0\u5347\u670d\u52a1\u4f53\u9a8c\uff0c\u4e0d\u4e0e\u7b2c\u4e09\u65b9\u5206\u4eab\u3002"},{title:"\u4e09\u3001\u552e\u540e\u8bf4\u660e",content:"\u6536\u5230\u5546\u54c1\u540e\u5982\u6709\u8d28\u91cf\u95ee\u9898\uff0c\u8bf7\u57287\u5929\u5185\u8054\u7cfb\u5ba2\u670d\uff0c\u6211\u4eec\u5c06\u4e3a\u60a8\u514d\u8d39\u91cd\u505a\u6216\u9000\u6b3e\u3002"},{title:"\u56db\u3001\u5b9a\u5236\u6d41\u7a0b",content:"1. \u9009\u62e9\u5546\u54c1\u54c1\u7c7b\u5e76\u52a0\u5165\u8bbe\u8ba1\u6e05\u5355\n2. \u5728DIY\u5de5\u4f5c\u53f0\u4e0a\u4f20\u56fe\u7247\u6216\u8f93\u5165\u6587\u5b57\n3. \u786e\u8ba4\u8bbe\u8ba1\u6548\u679c\u5e76\u63d0\u4ea4\u8ba2\u5355\n4. \u652f\u4ed8\u540e\u8fdb\u5165\u751f\u4ea7\uff0c\u7ea63-7\u4e2a\u5de5\u4f5c\u65e5\u53d1\u8d27\n5. \u7269\u6d41\u8ddf\u8e2a\u81f3\u786e\u8ba4\u6536\u8d27"}];return(0,m.jsxs)(s.Ss,{className:"theme-".concat(t),children:[(0,m.jsx)(l.A,{}),(0,m.jsx)(o.A,{title:"\u5b9a\u5236\u534f\u8bae",targetSelector:".agreement-title"}),(0,m.jsx)(s.Ss,{className:"agreement-page",children:(0,m.jsxs)(s.Ss,{className:"agreement-content",style:{marginTop:"".concat(n.statusBarHeight+12,"px")},children:[(0,m.jsx)(s.EY,{className:"agreement-title",children:"\u5b9a\u5236\u534f\u8bae"}),a.map(function(e,t){return(0,m.jsxs)(s.Ss,{className:"agreement-section surface-card",children:[(0,m.jsx)(s.EY,{className:"agreement-h3",children:e.title}),(0,m.jsx)(s.EY,{className:"agreement-p",children:e.content})]},t)})]})})]})}var u={navigationStyle:"custom",enablePageMeta:!0,navigationBarTitleText:"\u5b9a\u5236\u534f\u8bae"};Page((0,a.createPageConfig)(g,"pages/agreement/index",{root:{cn:[]}},u||{}))}},function(e){var t=function(t){return e(e.s=t)};e.O(0,[907,96,76],function(){return t(6855)});e.O()}]);
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.service-page{-webkit-box-sizing:border-box;box-sizing:border-box;min-height:100vh;padding:0 32rpx calc(24rpx + env(safe-area-inset-bottom))}.service-page .page-header.service-header{margin:0;padding-bottom:32rpx;padding-left:0;padding-right:0;text-align:center}.service-header .page-title{color:var(--text-primary);font-size:44rpx;font-weight:700;letter-spacing:0;line-height:1.2}.service-hero{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:56rpx 40rpx;text-align:center}.service-hero-icon{height:88rpx;margin-bottom:24rpx;width:88rpx}.service-title{color:var(--text-primary);display:block;font-size:36rpx;font-weight:700;letter-spacing:0;margin-bottom:12rpx}.service-desc{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.5;margin-bottom:32rpx}.service-btn{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:28rpx;font-weight:600;height:80rpx;-webkit-justify-content:center;justify-content:center;min-width:280rpx;padding:0 48rpx}.service-page .section-title{color:var(--text-primary);display:block;font-size:32rpx;font-weight:700;letter-spacing:0;margin-bottom:24rpx}.contact-list.surface-card{overflow:hidden;padding:0 32rpx}.contact-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;border-bottom:1rpx solid var(--border);display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:112rpx;padding:24rpx 0}.contact-item:last-child{border-bottom:none}.contact-item-link{-webkit-transition:background .16s ease;transition:background .16s ease}.contact-item-link:active{background:var(--bg-muted)}.contact-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:48rpx;margin-right:24rpx;width:48rpx}.contact-info{-webkit-flex:1;-ms-flex:1;flex:1;min-width:0}.contact-label{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.4;margin-bottom:4rpx}.contact-value{color:var(--text-primary);display:block;font-size:28rpx;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-arrow{-ms-flex-negative:0;color:var(--text-muted);-webkit-flex-shrink:0;flex-shrink:0;font-size:32rpx;line-height:1;margin-left:16rpx}.faq-card{display:block;margin-bottom:24rpx;padding:32rpx}.faq-q{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;letter-spacing:0;line-height:1.45;margin-bottom:12rpx}.faq-a{color:var(--text-secondary);display:block;font-size:26rpx;line-height:1.7}.enterprise-service{padding:32rpx}.enterprise-service .flex-center{-ms-flex-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;margin-bottom:12rpx}.enterprise-icon{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:40rpx;margin-right:12rpx;width:40rpx}.enterprise-title{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;letter-spacing:0}.enterprise-desc{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.6;margin-bottom:24rpx}.enterprise-btn{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:28rpx;font-weight:600;height:80rpx;-webkit-justify-content:center;justify-content:center} .service-page{-webkit-box-sizing:border-box;box-sizing:border-box;min-height:100vh;padding:0 32rpx calc(24rpx + env(safe-area-inset-bottom))}.service-page .page-header.service-header{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;margin:0;padding-bottom:32rpx;padding-left:0;padding-right:0}.service-header .back-btn{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;background:var(--bg-card);border:1rpx solid var(--border-strong);border-radius:50%;-webkit-box-shadow:var(--shadow-card);box-shadow:var(--shadow-card);-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;height:88rpx;-webkit-justify-content:center;justify-content:center;width:88rpx}.service-header .back-btn:active{-webkit-transform:scale(.96);-ms-transform:scale(.96);transform:scale(.96)}.service-header .back-arrow{color:var(--text-primary);font-size:36rpx;line-height:1}.service-header .page-title{color:var(--text-primary);-webkit-flex:1;-ms-flex:1;flex:1;font-size:44rpx;font-weight:700;letter-spacing:0;line-height:1.2;min-width:0;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap}.service-header .header-spacer{-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:88rpx}.service-hero{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:56rpx 40rpx;text-align:center}.service-hero-icon{height:88rpx;margin-bottom:24rpx;width:88rpx}.service-title{color:var(--text-primary);display:block;font-size:36rpx;font-weight:700;letter-spacing:0;margin-bottom:12rpx}.service-desc{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.5;margin-bottom:32rpx}.service-btn{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:28rpx;font-weight:600;height:80rpx;-webkit-justify-content:center;justify-content:center;min-width:280rpx;padding:0 48rpx}.service-page .section-title{color:var(--text-primary);display:block;font-size:32rpx;font-weight:700;letter-spacing:0;margin-bottom:24rpx}.contact-list.surface-card{overflow:hidden;padding:0 32rpx}.contact-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;border-bottom:1rpx solid var(--border);display:-webkit-flex;display:-ms-flexbox;display:flex;min-height:112rpx;padding:24rpx 0}.contact-item:last-child{border-bottom:none}.contact-item-link{-webkit-transition:background .16s ease;transition:background .16s ease}.contact-item-link:active{background:var(--bg-muted)}.contact-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:48rpx;margin-right:24rpx;width:48rpx}.contact-info{-webkit-flex:1;-ms-flex:1;flex:1;min-width:0}.contact-label{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.4;margin-bottom:4rpx}.contact-value{color:var(--text-primary);display:block;font-size:28rpx;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.contact-arrow{-ms-flex-negative:0;color:var(--text-muted);-webkit-flex-shrink:0;flex-shrink:0;font-size:32rpx;line-height:1;margin-left:16rpx}.faq-card{display:block;margin-bottom:24rpx;padding:32rpx}.faq-q{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;letter-spacing:0;line-height:1.45;margin-bottom:12rpx}.faq-a{color:var(--text-secondary);display:block;font-size:26rpx;line-height:1.7}.enterprise-service{padding:32rpx}.enterprise-service .flex-center{-ms-flex-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;margin-bottom:12rpx}.enterprise-icon{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:40rpx;margin-right:12rpx;width:40rpx}.enterprise-title{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;letter-spacing:0}.enterprise-desc{color:var(--text-secondary);display:block;font-size:24rpx;line-height:1.6;margin-bottom:24rpx}.enterprise-btn{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:28rpx;font-weight:600;height:80rpx;-webkit-justify-content:center;justify-content:center}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[205],{6296:function(s,e,a){var c=a(7842),n=a(118),r=a(758),i=a.n(r),o=a(9703),d=a(9201),l=a(5414),h=a(9674),p=a(5234),t=a(4848);function m(){var s=(0,o.wR)(),e=s.resolvedTheme,a=(0,d.c)();(0,l.l)(e);var c=function(s){i().navigateTo({url:"/pages/shop/detail/index?id=".concat(s)})};return(0,t.jsxs)(n.Ss,{className:"shop-page theme-".concat(e),children:[(0,t.jsx)(h.A,{}),(0,t.jsxs)(n.Ss,{className:"shop-list",children:[(0,t.jsxs)(n.Ss,{className:"shop-header",style:{paddingTop:"".concat(a.headerPaddingTop,"px")},children:[(0,t.jsx)(n.EY,{className:"shop-header-title",children:"\u667a\u7ed8\u7cbe\u9009"}),(0,t.jsx)(n.EY,{className:"shop-header-sub",children:"\u6fc0\u5149\u5fae\u96d5\u5b9a\u5236\u597d\u7269"})]}),(0,t.jsx)(n.Ss,{className:"shop-grid",children:p.zv.map(function(s){var e=s.images[0]||s.iconImg||"";return(0,t.jsxs)(n.Ss,{className:"shop-card",onTap:function(){return c(s.id)},children:[(0,t.jsx)(n.Ss,{className:"shop-card-img-wrap",children:(0,t.jsx)(n._V,{className:"shop-card-img",src:e,mode:"aspectFill",lazyLoad:!0})}),(0,t.jsxs)(n.Ss,{className:"shop-card-body",children:[(0,t.jsx)(n.EY,{className:"shop-card-name",children:s.name}),(0,t.jsx)(n.EY,{className:"shop-card-desc",children:s.desc||""}),(0,t.jsxs)(n.EY,{className:"shop-card-price",children:["\xa5",s.price]})]})]},s.id)})})]})]})}var x={navigationStyle:"custom",enablePageMeta:!0};Page((0,c.createPageConfig)(m,"pages/shop/index",{root:{cn:[]}},x||{}))}},function(s){var e=function(e){return s(s.s=e)};s.O(0,[907,96,76],function(){return e(6296)});s.O()}]); "use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[205],{6296:function(s,e,a){var c=a(7842),n=a(118),r=a(758),i=a.n(r),o=a(9703),t=a(9201),l=a(5414),d=a(9674),h=a(3053),p=a(5234),m=a(4848);function x(){var s=(0,o.wR)(),e=s.resolvedTheme,a=(0,t.c)();(0,l.l)(e);var c=function(s){i().navigateTo({url:"/pages/shop/detail/index?id=".concat(s)})};return(0,m.jsxs)(n.Ss,{className:"shop-page theme-".concat(e),children:[(0,m.jsx)(d.A,{}),(0,m.jsx)(h.A,{title:"\u667a\u7ed8\u7cbe\u9009",targetSelector:".shop-header"}),(0,m.jsxs)(n.Ss,{className:"shop-list",children:[(0,m.jsxs)(n.Ss,{className:"shop-header",style:{paddingTop:"".concat(a.statusBarHeight+12,"px")},children:[(0,m.jsx)(n.EY,{className:"shop-header-title",children:"\u667a\u7ed8\u7cbe\u9009"}),(0,m.jsx)(n.EY,{className:"shop-header-sub",children:"\u6fc0\u5149\u5fae\u96d5\u5b9a\u5236\u597d\u7269"})]}),(0,m.jsx)(n.Ss,{className:"shop-grid",children:p.zv.map(function(s){var e=s.images[0]||s.iconImg||"";return(0,m.jsxs)(n.Ss,{className:"shop-card",onTap:function(){return c(s.id)},children:[(0,m.jsx)(n.Ss,{className:"shop-card-img-wrap",children:(0,m.jsx)(n._V,{className:"shop-card-img",src:e,mode:"aspectFill",lazyLoad:!0})}),(0,m.jsxs)(n.Ss,{className:"shop-card-body",children:[(0,m.jsx)(n.EY,{className:"shop-card-name",children:s.name}),(0,m.jsx)(n.EY,{className:"shop-card-desc",children:s.desc||""}),(0,m.jsxs)(n.EY,{className:"shop-card-price",children:["\xa5",s.price]})]})]},s.id)})})]})]})}var u={navigationStyle:"custom",enablePageMeta:!0};Page((0,c.createPageConfig)(x,"pages/shop/index",{root:{cn:[]}},u||{}))}},function(s){var e=function(e){return s(s.s=e)};s.O(0,[907,96,76],function(){return e(6296)});s.O()}]);
+1 -1
View File
@@ -1 +1 @@
.shop-list{padding:0 32rpx 24rpx}.shop-header{padding:48rpx 0 32rpx;text-align:center}.shop-header-title{color:var(--text-primary);display:block;font-size:44rpx;font-weight:700;letter-spacing:0}.shop-header-sub{color:var(--text-secondary);display:block;font-size:24rpx;margin-top:8rpx}.shop-grid{display:grid;gap:24rpx;grid-template-columns:1fr 1fr}.shop-card{background:var(--bg-card);border:1rpx solid var(--border);border-radius:24rpx;-webkit-box-shadow:var(--shadow-card);box-shadow:var(--shadow-card);overflow:hidden;-webkit-transition:-webkit-transform .15s ease;transition:-webkit-transform .15s ease;transition:transform .15s ease;transition:transform .15s ease,-webkit-transform .15s ease}.shop-card:active{-webkit-transform:scale(.98);-ms-transform:scale(.98);transform:scale(.98)}.shop-card-img-wrap{aspect-ratio:1/1;background:var(--bg-muted);overflow:hidden}.shop-card-img{height:100%;-o-object-fit:cover;object-fit:cover;-webkit-transition:-webkit-transform .4s ease;transition:-webkit-transform .4s ease;transition:transform .4s ease;transition:transform .4s ease,-webkit-transform .4s ease;width:100%}.shop-card-body{padding:20rpx 24rpx 24rpx}.shop-card-name{-webkit-line-clamp:2;-webkit-box-orient:vertical;color:var(--text-primary);display:-webkit-box;font-size:28rpx;font-weight:700;line-height:1.4;overflow:hidden}.shop-card-desc{-webkit-line-clamp:2;-webkit-box-orient:vertical;color:var(--text-secondary);display:-webkit-box;font-size:24rpx;line-height:1.4;margin-top:8rpx;overflow:hidden}.shop-card-price{color:var(--accent-primary);display:block;font-size:32rpx;font-variant:tabular-nums;font-weight:700;margin-top:12rpx}@media (min-width:750rpx){.shop-page{-ms-flex-pack:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;justify-content:center}} .shop-list{padding:0 32rpx calc(220rpx + env(safe-area-inset-bottom))}.shop-header{padding:48rpx 0 32rpx;text-align:center}.shop-header-title{color:var(--text-primary);display:block;font-size:44rpx;font-weight:700;letter-spacing:0}.shop-header-sub{color:var(--text-secondary);display:block;font-size:24rpx;margin-top:8rpx}.shop-grid{display:grid;gap:24rpx;grid-template-columns:1fr 1fr}.shop-card{background:var(--bg-card);border:1rpx solid var(--border);border-radius:24rpx;-webkit-box-shadow:var(--shadow-card);box-shadow:var(--shadow-card);overflow:hidden;-webkit-transition:-webkit-transform .15s ease;transition:-webkit-transform .15s ease;transition:transform .15s ease;transition:transform .15s ease,-webkit-transform .15s ease}.shop-card:active{-webkit-transform:scale(.98);-ms-transform:scale(.98);transform:scale(.98)}.shop-card-img-wrap{aspect-ratio:1/1;background:var(--bg-muted);overflow:hidden}.shop-card-img{height:100%;-o-object-fit:cover;object-fit:cover;-webkit-transition:-webkit-transform .4s ease;transition:-webkit-transform .4s ease;transition:transform .4s ease;transition:transform .4s ease,-webkit-transform .4s ease;width:100%}.shop-card-body{padding:20rpx 24rpx 24rpx}.shop-card-name{-webkit-line-clamp:2;-webkit-box-orient:vertical;color:var(--text-primary);display:-webkit-box;font-size:28rpx;font-weight:700;line-height:1.4;overflow:hidden}.shop-card-desc{-webkit-line-clamp:2;-webkit-box-orient:vertical;color:var(--text-secondary);display:-webkit-box;font-size:24rpx;line-height:1.4;margin-top:8rpx;overflow:hidden}.shop-card-price{color:var(--accent-primary);display:block;font-size:32rpx;font-variant:tabular-nums;font-weight:700;margin-top:12rpx}@media (min-width:750rpx){.shop-page{-ms-flex-pack:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;justify-content:center}}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+184
View File
@@ -0,0 +1,184 @@
# 智绘微刻小程序 - UI 覆层组件文档
本文档说明两个全局 UI 覆层能力:**顶部滚动遮罩(ScrollTopMask** 和 **自定义 Tab BarCustomTabBar**
---
## 一、顶部滚动遮罩 ScrollTopMask
### 作用
信息流页面上下滚动时,在页面顶部形成分层模糊 + 渐变透明遮罩:
- 上滑到大标题接近状态栏区域时遮罩淡入,状态栏时间/电量更清晰;
- 遮罩内水平居中显示当前页面标题,标题位于遮罩下 1/4 区域(`bottom: 38%`);
- 下拉回顶时按相同进度淡出;
- 大标题本身仍随页面自然滚动,不做吸顶/缩放/变色。
### 组件位置
```text
src/components/ScrollTopMask/
├── index.tsx # 组件逻辑
└── index.scss # 模糊层 / 渐变层 / 标题样式
```
### 使用方式
在任意页面的根 View`theme-*` 容器)内、`<ThemedPageMeta />` 之后放置:
```tsx
import ScrollTopMask from '../../components/ScrollTopMask'
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<ScrollTopMask title="页面标题" targetSelector=".page-header" />
{/* 页面内容 */}
</View>
```
> 二级页面(`src/pages/shop/detail`、`src/pages/diy/stickerEdit` 等)相对路径为 `../../../components/ScrollTopMask`。
二级页面开启返回按钮(`showBack`),遮罩随滚动淡入时,左上角会显示 `←` 返回按钮,点击回调 `Taro.navigateBack()`
```tsx
<ScrollTopMask title="商品详情" targetSelector=".page-header" showBack />
```
当前开启 `showBack` 的页面:商品详情、产品详情、设计工作台、贴纸编辑、结算、订单详情、收货地址、设置、AI 词云。
### Props
| Prop | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| `title` | `string` | 是 | - | 遮罩内居中显示的标题文本 |
| `targetSelector` | `string` | 否 | `.page-header` | 顶部测量节点,用于计算遮罩出现的滚动阈值 |
| `showBack` | `boolean` | 否 | `false` | 是否在遮罩左上方显示返回按钮(二级页面用) |
`targetSelector` 按页面页头选择:
| 页面 | title | targetSelector |
|---|---|---|
| 首页 `index` | 智绘微刻 | `.home-header` |
| 商品 `shop/index` | 智绘精选 | `.shop-header` |
| 设计清单 `designList` | 我的设计清单 | `.page-header.design-header` |
| 订单 `orders` | 我的订单 | `.orders-page .page-header` |
| 我的 `profile` | 我的 | `.profile-header` |
| 商品详情 `shop/detail` | 商品详情 | `.shop-back-btn` |
| 产品详情 `product` | 商品详情 | `.page-header` |
| 订单详情 `orderDetail` | 订单详情 | `.page-header` |
| 设计工作台 `diy` | 设计工作台 | `.page-header.surface-card` |
| 贴纸编辑 `diy/stickerEdit` | 编辑贴纸 | `.edit-header` |
| 结算 `checkout` | 设计效果确认 | `.page-header` |
| 收货地址 `address` | 收货地址 | `.page-header` |
| 设置 `settings` | 设置 | `.page-header.settings-header` |
| 客服 `service` | 联系客服 | `.page-header.service-header` |
| 定制协议 `agreement` | 定制协议 | `.agreement-title` |
| AI 词云 `wordcloud` | AI词云生成 | `.page-header.cloud-header` |
| 用户数据库 `userDatabase` | 本地用户数据库 | `.page-header.udb-header` |
### 实现原理
1. `useSafeArea()` 提供状态栏 / 胶囊按钮安全区;
2. `Taro.usePageScroll` 监听页面滚动(Taro 3 子组件可通过页面 Context 注册);
3. `createSelectorQuery().select(targetSelector)` 测量页头位置;
4. `getTopMaskStartY` 把「视口相对 bottom + 当前 scrollTop」换算成文档相对阈值,避免提前滚动导致阈值塌缩;
5. `getTopMaskProgress` 输出 0~1 遮罩进度,节流到定时器内更新;
6. 标题淡入使用二次映射:遮罩进度 0.5 后才开始出现。
### 视觉参数(当前值)
| 参数 | 值 | 位置 |
|---|---|---|
| 中模糊层 | `blur(12rpx)`,纵向淡出到 76% | `index.scss` |
| 弱模糊层 | `blur(6rpx)`,纵向淡出到 100% | `index.scss` |
| 渐变层高度 | mask 高度的 80% | `index.scss` |
| 渐变顶部不透明度 | `0.94` | `index.scss` |
| 渐变 50% 处 | `0.9` | `index.scss` |
| 渐变 78% 处 | `0.58`,之后到 0 | `index.scss` |
| 标题位置 | `bottom: 38%`,水平居中 | `index.scss` |
| 遮罩高度 | `max(menuButtonTop+menuButtonHeight+18, statusBarHeight+52) + 24(px)` | `index.tsx` |
### 注意事项
- 遮罩 `pointer-events: none`,不影响点击;二级页左侧返回按钮仍可操作;
- 多层 `backdrop-filter` + `mask-image` 真机兼容性需要实测,不支持的设备会退回纯渐变;
- 修改视觉参数只需改 `src/components/ScrollTopMask/index.scss`,无需逐页调整。
---
## 二、自定义 Tab Bar CustomTabBar
### 作用
微信自定义 tabBar,提供毛玻璃悬浮胶囊导航 + 浅色/深色主题自适应 + 路由切换联动。
### 组件位置
```text
src/custom-tab-bar/
├── index.json
├── index.tsx # tab 配置与切换逻辑
└── index.scss # 毛玻璃 / 渐变 / 主题样式
```
### 启用方式
`src/app.config.ts`
```ts
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: '我的' }
]
}
```
### Tab 配置
`src/custom-tab-bar/index.tsx` 顶部 `TABS` 数组驱动渲染:
```ts
const TABS = [
{ pagePath: '/pages/index/index', label: '首页', icon: assetUrl('/icon/首页.svg'), iconActive: assetUrl('/icon/首页-fill.svg') },
// ...
]
```
新增/调整 Tab 时需要同时同步:
1. `TABS` 数组(label、pagePath、icon、iconActive);
2. `src/app.config.ts``pages``tabBar.list`
3. 对应图标资源。
### 核心行为
- 选中态由内部 `activeTab` state 维护(自定义 tabBar 不在 React Tree 内,不依赖路由自动感知);
- `switchTab` 调用 `Taro.switchTab` 并触发 `tabBarChange` 事件,页面内部也监听该事件同步选中态;
- 主题:监听 `THEME_CHANGE_EVENT` + 系统 `onThemeChange``auto` 跟随系统,并调用 `applyPageBackground` 兜底刷新页面背景;
- 样式:底部渐变托底 + 悬浮胶囊,`backdrop-filter: blur(32px) saturate(1.2)`,浅/深主题各自配色。
### 注意事项
- `z-index`:渐变托底 `999`,胶囊 `1000`,浮于页面内容之上;
- 自定义 tabBar 与顶部 ScrollTopMask 互不冲突(遮罩 z-index 40,位于胶囊之下的页面层级);
- 修改样式只需改 `src/custom-tab-bar/index.scss`
---
## 相关文件索引
| 文件 | 作用 |
|---|---|
| `src/components/ScrollTopMask/index.tsx` | 顶部遮罩组件逻辑 |
| `src/components/ScrollTopMask/index.scss` | 顶部遮罩视觉参数 |
| `src/custom-tab-bar/index.tsx` | 自定义 Tab Bar 逻辑与 Tab 配置 |
| `src/custom-tab-bar/index.scss` | 自定义 Tab Bar 样式 |
| `src/utils/homeTopMask.js` | 遮罩进度 / 阈值纯函数 |
| `tools/homeTopMask.test.mjs` | 遮罩纯函数单测 |
| `src/app.config.ts` | 页面路由与 tabBar 注册 |
+33
View File
@@ -0,0 +1,33 @@
# 商品 页面设计规范
## 基本信息
- 路由:`pages/shop/index`
- 分类:`A类`
- 标题文本:智绘精选
- TabBar:是
## 组件调用
- **ScrollTopMask**
- `title="智绘精选"`
- `targetSelector=".shop-header"`
- `showBack``否`
- **CustomTabBar**:自动显示,页面无需手动调用。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 TabBar + env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.shop-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
商品网格信息流;标题带副文案,副文案随主标题上移后保留。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
+33
View File
@@ -0,0 +1,33 @@
# 我的 页面设计规范
## 基本信息
- 路由:`pages/profile/index`
- 分类:`A类`
- 标题文本:我的
- TabBar:是
## 组件调用
- **ScrollTopMask**
- `title="我的"`
- `targetSelector=".profile-header"`
- `showBack``否`
- **CustomTabBar**:自动显示,页面无需手动调用。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 TabBar + env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.profile-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
登录卡片/用户区;无大标题,TopMask 标题用“我的”。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
+33
View File
@@ -0,0 +1,33 @@
# 订单 页面设计规范
## 基本信息
- 路由:`pages/orders/index`
- 分类:`A类`
- 标题文本:我的订单
- TabBar:是
## 组件调用
- **ScrollTopMask**
- `title="我的订单"`
- `targetSelector=".orders-page .page-header"`
- `showBack``否`
- **CustomTabBar**:自动显示,页面无需手动调用。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 TabBar + env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.orders-page .page-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
ScrollView 列表 + tab 筛选;当前为 marginTop,统一时保持布局起点一致。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 设计清单 页面设计规范
## 基本信息
- 路由:`pages/designList/index`
- 分类:`A类`
- 标题文本:我的设计清单
- TabBar:是
## 组件调用
- **ScrollTopMask**
- `title="我的设计清单"`
- `targetSelector=".page-header.design-header"`
- `showBack``否`
- **CustomTabBar**:自动显示,页面无需手动调用。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 TabBar + env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.design-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
ScrollView 列表 + 顶部 tab 筛选;批量管理模式注意遮罩不影响顶部操作。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,46 @@
# A 类 - 根内容页(Tab)通用规范
## 页面列表
| 页面 | 路由 |
|---|---|
| 首页 | `pages/index/index` |
| 商品 | `pages/shop/index` |
| 设计清单 | `pages/designList/index` |
| 订单 | `pages/orders/index` |
| 我的 | `pages/profile/index` |
## 组件调用
- **TabBar**:显示。由 `app.config.ts``tabBar.custom: true` 自动挂载,页面内不需要调用组件,也不要渲染自己的底部栏。
- **TopMask**:调用,`showBack={false}`;标题使用页面左上角大标题文本。
- **useStatusBar / ThemedPageMeta**:按现有页面模式保留。
## 设计规范
- 顶部字距:`paddingTop(或 marginTop= statusBarHeight + 12px`,标题在左上角,不需要避开右上角胶囊。
- 大标题:页面左上角,字重与字号遵循各页面现有样式,不做渐变/缩放/吸顶。
- 底部:内容区必须为 TabBar 预留空间,滚动类页面底部保留 `padding-bottom`(含 `env(safe-area-inset-bottom)`)。
- 遮罩:滚动后顶部出现分层模糊 + 渐变,标题淡入在遮罩下 1/4 区域;无返回按钮。
## 接入示例
```tsx
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<ScrollTopMask title="页面标题" targetSelector=".xxx-header" />
<View className="page">
<View className="xxx-header" style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className="page-title"></Text>
</View>
</View>
</View>
```
## 验收清单
- [ ] 顶部字距为 `statusBarHeight + 12px`iOS/Android 视觉效果一致
- [ ] TabBar 正常显示,内容底部不被遮挡
- [ ] 滚动后 TopMask 淡入,标题居中淡入,无返回按钮
- [ ] 浅色/深色主题均正常
+33
View File
@@ -0,0 +1,33 @@
# 首页 页面设计规范
## 基本信息
- 路由:`pages/index/index`
- 分类:`A类`
- 标题文本:智绘微刻
- TabBar:是
## 组件调用
- **ScrollTopMask**
- `title="智绘微刻"`
- `targetSelector=".home-header"`
- `showBack``否`
- **CustomTabBar**:自动显示,页面无需手动调用。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已实现)。
- 底部安全区:避让 TabBar + env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.home-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
大标题左上角;轮播/搜索/分类卡片内容流较长,滚动后 TopMask 淡入。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# AI词云生成 页面设计规范
## 基本信息
- 路由:`pages/wordcloud/index`
- 分类:`B类`
- 标题文本:AI词云生成
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="AI词云生成"`
- `targetSelector=".page-header.cloud-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.cloud-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
步骤式工具页,内容较长可滚动;返回按钮 + TopMask 返回按钮。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 产品详情 页面设计规范
## 基本信息
- 路由:`pages/product/index`
- 分类:`B类`
- 标题文本:商品详情
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="商品详情"`
- `targetSelector=".page-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
加载/未找到/正常三个分支都有 page-header;返回按钮 + TopMask 返回按钮。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 收货地址 页面设计规范
## 基本信息
- 路由:`pages/address/index`
- 分类:`B类`
- 标题文本:收货地址
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="收货地址"`
- `targetSelector=".page-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
列表 + 新增/编辑表单;返回按钮 + TopMask 返回按钮。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
+33
View File
@@ -0,0 +1,33 @@
# 结算 页面设计规范
## 基本信息
- 路由:`pages/checkout/index`
- 分类:`B类`
- 标题文本:设计效果确认
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="设计效果确认"`
- `targetSelector=".page-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
加载/正常两个分支都有 page-header;确认底部操作条避让手势条。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 订单详情 页面设计规范
## 基本信息
- 路由:`pages/orderDetail/index`
- 分类:`B类`
- 标题文本:订单详情
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="订单详情"`
- `targetSelector=".page-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
未找到/正常两个分支;返回按钮 + TopMask 返回按钮。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
+33
View File
@@ -0,0 +1,33 @@
# 设置 页面设计规范
## 基本信息
- 路由:`pages/settings/index`
- 分类:`B类`
- 标题文本:设置
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="设置"`
- `targetSelector=".page-header.settings-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.settings-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
短内容页;TopMask 触发点由 .page-header 决定。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 设计工作台 页面设计规范
## 基本信息
- 路由:`pages/diy/index`
- 分类:`B类`
- 标题文本:设计工作台
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="设计工作台"`
- `targetSelector=".page-header.surface-card"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.surface-card` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
画布操作页,页头为卡片式;滚动场景少但仍接入 TopMask。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 贴纸编辑 页面设计规范
## 基本信息
- 路由:`pages/diy/stickerEdit`
- 分类:`B类`
- 标题文本:编辑贴纸
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="编辑贴纸"`
- `targetSelector=".edit-header"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.edit-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
横滑滑块/画布页;返回按钮样式为 edit-back,TopMask 返回按钮保持圆形毛玻璃。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,53 @@
# B 类 - 详情/操作二级页通用规范
## 页面列表
| 页面 | 路由 |
|---|---|
| 产品详情 | `pages/product/index` |
| 订单详情 | `pages/orderDetail/index` |
| 设计工作台 | `pages/diy/index` |
| 贴纸编辑 | `pages/diy/stickerEdit` |
| 结算 | `pages/checkout/index` |
| 收货地址 | `pages/address/index` |
| 设置 | `pages/settings/index` |
| AI词云生成 | `pages/wordcloud/index` |
## 组件调用
- **TabBar**:不显示。二级页无需处理。
- **TopMask**:调用,`showBack`;遮罩左上角显示圆形毛玻璃返回按钮,点击 `Taro.navigateBack()`
- **useStatusBar / ThemedPageMeta**:按现有页面模式保留。
## 设计规范
- 顶部:返回按钮 + 标题同一行,`paddingTop(或 marginTop= statusBarHeight + 12px`
- 返回按钮:圆形毛玻璃,与 TabBar 同材质(`blur(32px) saturate(1.2)` + 半透明底 + 阴影),浅/深主题各一套。
- 遮罩标题:页面标题文本(如“商品详情”),滚动进度过半后淡入,位于遮罩下 1/4 区域。
- 底部:不避让 TabBar,但需避让手势条 `env(safe-area-inset-bottom)`
- 页面内原生返回按钮与遮罩内返回按钮行为一致,均为返回上一页。
## 接入示例
```tsx
<View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta />
<ScrollTopMask title="页面标题" targetSelector=".page-header" showBack />
<View className="page">
<View className="page-header" style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className="back-btn" onTap={() => Taro.navigateBack()}>
<Text></Text>
</View>
<Text className="nav-title"></Text>
</View>
</View>
</View>
```
## 验收清单
- [ ] 顶部字距为 `statusBarHeight + 12px`
- [ ] 页面顶部有返回入口;滚动后遮罩左上角圆形返回按钮仍可点击
- [ ] TopMask 标题与页面标题一致
- [ ] 浅色/深色主题下返回按钮材质与 TabBar 一致
@@ -0,0 +1,33 @@
# 商品详情(沉浸式) 页面设计规范
## 基本信息
- 路由:`pages/shop/detail`
- 分类:`C类`
- 标题文本:商品详情
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="商品详情"`
- `targetSelector=".shop-back-btn"`
- `showBack``是`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:不套用普通字距(保留滚动 Hero)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.shop-back-btn` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
darkTop + 全屏 Hero;页面自带返回按钮与 TopMask 返回按钮双保险。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,28 @@
# C 类 - 沉浸式全屏页通用规范
## 页面列表
| 页面 | 路由 |
|---|---|
| 商品详情(沉浸式) | `pages/shop/detail` |
## 组件调用
- **TabBar**:不显示。
- **TopMask**:调用,`showBack`;标题为“商品详情”。
- **useStatusBar / ThemedPageMeta**:使用 `darkTop` 模式,顶部沉浸深色。
## 设计规范
- 顶部不套用普通「状态栏 + 固定间距」规则,保留页面自己的滚动 Hero 设计:
- 主图全屏滚动、顶部裁切、Hero 信息渐隐、内容透明过渡与返回按钮渐隐。
- 沉浸期自带深色遮罩与 back button;滚动进入内容区后 TopMask 负责承接层级感。
- 返回按钮:页面 Hero 自带 + TopMask 圆形毛玻璃返回按钮双保险,行为均为返回上一页。
- 底部:避让手势条 `env(safe-area-inset-bottom)`
## 验收清单
- [ ] Hero 沉浸滚动效果未受影响
- [ ] 滚动后 TopMask 淡入,返回按钮可用
- [ ] 深色主题下状态栏文字清晰
@@ -0,0 +1,33 @@
# 定制协议 页面设计规范
## 基本信息
- 路由:`pages/agreement/index`
- 分类:`D类`
- 标题文本:定制协议
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="定制协议"`
- `targetSelector=".agreement-title"`
- `showBack``否`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.agreement-title` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
长文本;无 page-headertargetSelector 用 .agreement-title。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 用户数据库 页面设计规范
## 基本信息
- 路由:`pages/userDatabase/index`
- 分类:`D类`
- 标题文本:本地用户数据库
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="本地用户数据库"`
- `targetSelector=".page-header.udb-header"`
- `showBack``否`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.udb-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
锁定页/列表页两分支;无返回入口,TopMask 不显示返回按钮。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,33 @@
# 联系客服 页面设计规范
## 基本信息
- 路由:`pages/service/index`
- 分类:`D类`
- 标题文本:联系客服
- TabBar:否
## 组件调用
- **ScrollTopMask**
- `title="联系客服"`
- `targetSelector=".page-header.service-header"`
- `showBack``是(本期补齐)`
- **CustomTabBar**:不显示。
## 设计要点
- 顶部字距:statusBarHeight + 12px(已统一)。
- 底部安全区:避让 env(safe-area-inset-bottom)。
- TopMask 出现阈值由 `.page-header.service-header` 测量决定;滚动后标题淡入,位置遵循所在分类通用规范。
## 页面特有说明
无返回入口,本期补齐返回按钮并开启 TopMask showBack。
## 验收要点
- [ ] 顶部字距符合 class 通用规范
- [ ] TopMask 标题/返回按钮符合预期
- [ ] 浅色/深色主题正常
- [ ] iOS / Android 真机表现一致
@@ -0,0 +1,32 @@
# D 类 - 工具/文字页通用规范
## 页面列表
| 页面 | 路由 |
|---|---|
| 联系客服 | `pages/service/index` |
| 定制协议 | `pages/agreement/index` |
| 用户数据库 | `pages/userDatabase/index` |
## 组件调用
- **TabBar**:不显示。
- **TopMask**:调用;是否开启 `showBack` 按页面能力决定:
- 联系客服:本期补齐返回能力 → `showBack`
- 定制协议 / 用户数据库:无返回入口 → `showBack={false}`
- **useStatusBar / ThemedPageMeta**:按现有页面模式保留。
## 设计规范
- 顶部字距:`paddingTop(或 marginTop= statusBarHeight + 12px`
- 工具/文字页内容通常较短:TopMask 仍随滚动淡入,但触发阈值由 `targetSelector` 决定。
- 底部:避让手势条 `env(safe-area-inset-bottom)`
- 文字页(协议)保证长文本阅读舒适,行距/字号遵循现有样式。
## 验收清单
- [ ] 顶部字距为 `statusBarHeight + 12px`
- [ ] 有返回能力的页面(联系客服)存在返回入口,TopMask 显示返回按钮
- [ ] 无返回能力的页面不显示返回按钮
- [ ] 滚动时 TopMask 正常淡入
+61
View File
@@ -0,0 +1,61 @@
# 页面设计规范 - 总览
> 本目录定义智绘微刻小程序的页面架构、页面分类与每类页面的统一设计规范。
## 1. 页面架构
小程序自定义导航(`navigationStyle: custom`),共 17 个页面。整体分两层:
- **一级页(Tab 根页)**:5 个,底部显示自定义 TabBar。
- **二级页(详情/操作/工具页)**:12 个,不显示 TabBar,通过 `navigateBack` 返回。
页面路由见 `src/app.config.ts`
## 2. 全局共用件
| 组件/工具 | 位置 | 用途 |
|---|---|---|
| `ScrollTopMask` | `src/components/ScrollTopMask` | 顶部滚动遮罩:分层模糊 + 渐变透明度 + 居中标题 + 可选返回按钮 |
| `CustomTabBar` | `src/custom-tab-bar` | 底部毛玻璃悬浮 TabBar,仅一级页显示 |
| `ThemedPageMeta` | `src/components/ThemedPageMeta` | 页面元信息与窗口背景主题 |
| `useSafeArea()` | `src/hooks/useSafeArea` | 状态栏/胶囊/安全区数据,全端自适应 |
| `useStatusBar()` | `src/hooks/useStatusBar` | 状态栏文字颜色 |
## 3. 通用规则
### 顶部字距
- 一级页与普通二级页使用 `paddingTop / marginTop = statusBarHeight + 12px`
- 标题都在左上角,无需让开右上角胶囊按钮,因此不再使用旧的 `headerPaddingTop`
`max(menuButtonTop + menuButtonHeight + 6, statusBarHeight + 44)`)。
- 该公式随 iOS / Android 状态栏高度自动变化,两端通用,不写死像素。
- 沉浸式页(`shop/detail`)除外,保留其滚动 Hero 设计。
### 顶部遮罩 ScrollTopMask
- 所有页面均调用。
- 一级页:`showBack={false}`
- 二级页带返回:`showBack`,遮罩左上角显示圆形毛玻璃返回按钮。
- 工具/文字页无返回:`showBack={false}`
### 底部安全区
- 一级页:内容底部避让 TabBar`padding-bottom` 含 TabBar 高度与安全区)。
- 二级页:底部避让手势条 `env(safe-area-inset-bottom)`
## 4. 页面分类
| 分类 | 层级 | 页面 | 特点 |
|---|---|---|---|
| A 类 | 一级页 | 首页、商品、设计清单、订单、我的 | TabBar 根页,内容流 |
| B 类 | 二级页 | 产品详情、订单详情、设计工作台、贴纸编辑、结算、收货地址、设置、AI词云 | 详情/操作,带返回 |
| C 类 | 二级页 | 商品详情(hero) | 沉浸式全屏滚动 |
| D 类 | 二级页 | 联系客服、定制协议、用户数据库 | 工具/文字页 |
## 5. 文档索引
- `A类/通用规范.md` + A 类各页面文档
- `B类/通用规范.md` + B 类各页面文档
- `C类/通用规范.md` + C 类页面文档
- `D类/通用规范.md` + D 类各页面文档
+120
View File
@@ -0,0 +1,120 @@
.scroll-top-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 40;
pointer-events: none;
transform: translateZ(0);
}
.scroll-top-blur,
.scroll-top-gradient {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
pointer-events: none;
}
.scroll-top-gradient {
height: 80%;
}
.scroll-top-title {
position: absolute;
left: 0;
right: 0;
bottom: 38%;
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: var(--text-primary);
letter-spacing: 0;
pointer-events: none;
}
.scroll-top-back {
position: absolute;
left: 30rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
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);
}
.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);
}
.scroll-top-back-arrow {
font-size: 44rpx;
font-weight: 600;
line-height: 1;
color: var(--text-primary);
}
.scroll-top-blur-mid {
backdrop-filter: blur(12rpx);
-webkit-backdrop-filter: blur(12rpx);
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 42%,
rgba(0, 0, 0, 0) 76%
);
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.8) 42%,
rgba(0, 0, 0, 0) 76%
);
}
.scroll-top-blur-soft {
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
-webkit-mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.65) 52%,
rgba(0, 0, 0, 0) 100%
);
mask-image: linear-gradient(
to bottom,
rgba(0, 0, 0, 1) 0%,
rgba(0, 0, 0, 0.65) 52%,
rgba(0, 0, 0, 0) 100%
);
}
.theme-light .scroll-top-gradient {
background: linear-gradient(
to bottom,
rgba(250, 247, 242, 0.94) 0%,
rgba(250, 247, 242, 0.9) 50%,
rgba(250, 247, 242, 0.58) 78%,
rgba(250, 247, 242, 0) 100%
);
}
.theme-dark .scroll-top-gradient {
background: linear-gradient(
to bottom,
rgba(25, 25, 25, 0.94) 0%,
rgba(25, 25, 25, 0.9) 50%,
rgba(25, 25, 25, 0.58) 78%,
rgba(25, 25, 25, 0) 100%
);
}
+121
View File
@@ -0,0 +1,121 @@
import { View, Text } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useSafeArea } from '../../hooks/useSafeArea'
import { getTopMaskProgress, getTopMaskStartY } from '../../utils/homeTopMask'
import './index.scss'
interface ScrollTopMaskProps {
title: string
targetSelector?: string
showBack?: boolean
}
export default function ScrollTopMask({ title, targetSelector = '.page-header', showBack = false }: ScrollTopMaskProps) {
const safe = useSafeArea()
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
const fallbackTitleMetrics = useMemo(() => ({
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
rangeY: Math.max(48, safe.headerPaddingTop * 0.45)
}), [safe.headerPaddingTop, safe.statusBarHeight])
const titleMetricsRef = useRef(fallbackTitleMetrics)
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const latestScrollTopRef = useRef(0)
const applyTopMaskProgress = useCallback((scrollTop: number) => {
latestScrollTopRef.current = scrollTop
if (scrollFrameRef.current !== null) {
return
}
scrollFrameRef.current = setTimeout(() => {
scrollFrameRef.current = null
const { startY, rangeY } = titleMetricsRef.current
setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY }))
}, 16)
}, [])
useEffect(() => {
let cancelled = false
let measureTimer: ReturnType<typeof setTimeout> | null = null
titleMetricsRef.current = fallbackTitleMetrics
type HeaderRect = Taro.NodesRef.BoundingClientRectCallbackResult
const measureTitle = () => {
if (cancelled) return
Taro.createSelectorQuery()
.select(targetSelector)
.boundingClientRect((rect: HeaderRect | HeaderRect[]) => {
if (cancelled) return
if (!rect || Array.isArray(rect) || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
titleMetricsRef.current = fallbackTitleMetrics
applyTopMaskProgress(latestScrollTopRef.current)
return
}
const startY = getTopMaskStartY({
viewportBottom: rect.bottom,
scrollTop: latestScrollTopRef.current,
statusBarHeight: safe.statusBarHeight
})
const rangeY = Math.max(32, rect.height * 0.7)
titleMetricsRef.current = { startY, rangeY }
applyTopMaskProgress(latestScrollTopRef.current)
})
.exec()
}
Taro.nextTick(() => {
if (cancelled) return
measureTimer = setTimeout(measureTitle, 80)
})
return () => {
cancelled = true
if (measureTimer !== null) {
clearTimeout(measureTimer)
}
if (scrollFrameRef.current !== null) {
clearTimeout(scrollFrameRef.current)
}
scrollFrameRef.current = null
}
}, [applyTopMaskProgress, fallbackTitleMetrics, targetSelector])
Taro.usePageScroll((e) => {
applyTopMaskProgress(e.scrollTop)
})
return (
<View
className='scroll-top-mask'
style={{
height: `${Math.max(safe.menuButtonTop + safe.menuButtonHeight + 18, safe.statusBarHeight + 52) + 24}px`,
opacity: topMaskOpacity
}}
>
<View className='scroll-top-blur scroll-top-blur-mid' />
<View className='scroll-top-blur scroll-top-blur-soft' />
<View className='scroll-top-gradient' />
{showBack && (
<View
className='scroll-top-back'
style={{ top: `${safe.menuButtonTop}px` }}
onTap={() => Taro.navigateBack()}
>
<Text className='scroll-top-back-arrow'></Text>
</View>
)}
<Text
className='scroll-top-title'
style={{
opacity: Math.max(0, Math.min(1, (topMaskOpacity - 0.5) / 0.5))
}}
>
{title}
</Text>
</View>
)
}
+3 -1
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import BottomActionBar from '../../components/BottomActionBar' import BottomActionBar from '../../components/BottomActionBar'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
@@ -97,9 +98,10 @@ export default function AddressPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="收货地址" targetSelector=".page-header" showBack />
<View className='address-page'> <View className='address-page'>
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
+3 -1
View File
@@ -4,6 +4,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
export default function AgreementPage() { export default function AgreementPage() {
const { theme, resolvedTheme } = useThemeContext() const { theme, resolvedTheme } = useThemeContext()
@@ -36,8 +37,9 @@ export default function AgreementPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="定制协议" targetSelector=".agreement-title" />
<View className='agreement-page'> <View className='agreement-page'>
<View className='agreement-content' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='agreement-content' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='agreement-title'></Text> <Text className='agreement-title'></Text>
{SECTIONS.map((sec, idx) => ( {SECTIONS.map((sec, idx) => (
<View key={idx} className='agreement-section surface-card'> <View key={idx} className='agreement-section surface-card'>
+5 -2
View File
@@ -7,6 +7,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import BottomActionBar from '../../components/BottomActionBar' import BottomActionBar from '../../components/BottomActionBar'
export default function CheckoutPage() { export default function CheckoutPage() {
@@ -67,8 +68,9 @@ export default function CheckoutPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="设计效果确认" targetSelector=".page-header" showBack />
<View className='checkout-page'> <View className='checkout-page'>
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
@@ -94,9 +96,10 @@ export default function CheckoutPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="设计效果确认" targetSelector=".page-header" showBack />
<View className='checkout-page'> <View className='checkout-page'>
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
+1 -1
View File
@@ -1,5 +1,5 @@
.design-page { .design-page {
padding: 0 32rpx 24rpx; padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
min-height: 100vh; min-height: 100vh;
} }
+3 -1
View File
@@ -10,6 +10,7 @@ import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import LoginGuard from '../../components/LoginGuard' import LoginGuard from '../../components/LoginGuard'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
const STATUS_TABS = [ const STATUS_TABS = [
{ code: 'all', label: '全部' }, { code: 'all', label: '全部' },
@@ -120,9 +121,10 @@ export default function DesignListPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="我的设计清单" targetSelector=".page-header.design-header" />
<LoginGuard> <LoginGuard>
<View className='design-page'> <View className='design-page'>
<View className='page-header design-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header design-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='header-spacer' /> <View className='header-spacer' />
<Text className='header-title'></Text> <Text className='header-title'></Text>
<View className='header-manage' onTap={handleManage}> <View className='header-manage' onTap={handleManage}>
+4 -1
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import BottomActionBar from '../../components/BottomActionBar' import BottomActionBar from '../../components/BottomActionBar'
export default function DIYPage() { export default function DIYPage() {
@@ -264,6 +265,7 @@ export default function DIYPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
<View className='diy-page'> <View className='diy-page'>
<Text className='page-title'>...</Text> <Text className='page-title'>...</Text>
</View> </View>
@@ -274,9 +276,10 @@ export default function DIYPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
<View className='diy-page'> <View className='diy-page'>
<View className='page-header surface-card mt-20' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header surface-card mt-20' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='flex-between' style={{ width: '100%' }}> <View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={goBack}></Text> <Text className='back-btn' onTap={goBack}></Text>
<Text className='page-title'></Text> <Text className='page-title'></Text>
+3 -1
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../../context/ThemeContext'
import { useSafeArea } from '../../../hooks/useSafeArea' import { useSafeArea } from '../../../hooks/useSafeArea'
import { useStatusBar } from '../../../hooks/useStatusBar' import { useStatusBar } from '../../../hooks/useStatusBar'
import ThemedPageMeta from '../../../components/ThemedPageMeta' import ThemedPageMeta from '../../../components/ThemedPageMeta'
import ScrollTopMask from '../../../components/ScrollTopMask'
import BottomActionBar from '../../../components/BottomActionBar' import BottomActionBar from '../../../components/BottomActionBar'
/* ============================================================ /* ============================================================
@@ -311,9 +312,10 @@ export default function StickerEditPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="编辑贴纸" targetSelector=".edit-header" showBack />
<View className='sticker-edit-page'> <View className='sticker-edit-page'>
{/* Header */} {/* Header */}
<View className='edit-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='edit-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='edit-back' onTap={() => Taro.navigateBack()}></Text> <Text className='edit-back' onTap={() => Taro.navigateBack()}></Text>
<Text className='edit-title'></Text> <Text className='edit-title'></Text>
<Text className='edit-save' onTap={handleSave}></Text> <Text className='edit-save' onTap={handleSave}></Text>
+10 -34
View File
@@ -1,5 +1,6 @@
.index-page { .index-page {
padding: 0 32rpx 24rpx; padding: 0 32rpx 24rpx;
padding-bottom: calc(220rpx + env(safe-area-inset-bottom));
box-sizing: border-box; box-sizing: border-box;
} }
@@ -27,6 +28,7 @@
border-radius: 999rpx; border-radius: 999rpx;
background: var(--bg-input); background: var(--bg-input);
border: 1rpx solid transparent; border: 1rpx solid transparent;
box-shadow: var(--shadow-card);
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease; transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
} }
@@ -54,7 +56,12 @@
} }
.search-input::placeholder { .search-input::placeholder {
color: var(--text-muted); color: var(--text-secondary);
font-size: 26rpx;
}
.search-input-placeholder {
color: var(--text-secondary);
font-size: 26rpx; font-size: 26rpx;
} }
@@ -118,10 +125,10 @@
max-width: 78%; max-width: 78%;
padding: 16rpx 24rpx; padding: 16rpx 24rpx;
border-radius: 24rpx; border-radius: 24rpx;
background: rgba(255, 255, 255, 0.72); background: var(--glass);
backdrop-filter: blur(24rpx); backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx); -webkit-backdrop-filter: blur(24rpx);
border: 1rpx solid rgba(255, 255, 255, 0.6); border: 1rpx solid var(--glass-line);
box-shadow: var(--shadow-card); box-shadow: var(--shadow-card);
} }
@@ -252,34 +259,3 @@
font-size: 24rpx; font-size: 24rpx;
color: var(--text-secondary); color: var(--text-secondary);
} }
.home-top-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 40;
pointer-events: none;
transform: translateZ(0);
}
.theme-light .home-top-mask {
background: linear-gradient(
to bottom,
rgba(250, 247, 242, 0.98) 0%,
rgba(250, 247, 242, 0.92) 38%,
rgba(250, 247, 242, 0.48) 70%,
rgba(250, 247, 242, 0) 100%
);
}
.theme-dark .home-top-mask {
background: linear-gradient(
to bottom,
rgba(25, 25, 25, 0.98) 0%,
rgba(25, 25, 25, 0.92) 38%,
rgba(25, 25, 25, 0.48) 70%,
rgba(25, 25, 25, 0) 100%
);
}
+6 -86
View File
@@ -1,6 +1,6 @@
import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components' import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components'
import Taro from '@tarojs/taro' import Taro from '@tarojs/taro'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useState } from 'react'
import './index.scss' import './index.scss'
import { CATEGORIES } from '../../utils/productConfig' import { CATEGORIES } from '../../utils/productConfig'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
@@ -8,7 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import { getTopMaskProgress, getTopMaskStartY } from '../../utils/homeTopMask' import ScrollTopMask from '../../components/ScrollTopMask'
// 成品展示配图(从 img 里取对应实物图) // 成品展示配图(从 img 里取对应实物图)
const SHOWCASE_LIST = [ const SHOWCASE_LIST = [
@@ -71,81 +71,6 @@ export default function Index() {
useStatusBar(resolvedTheme) useStatusBar(resolvedTheme)
const [searchKey, setSearchKey] = useState('') const [searchKey, setSearchKey] = useState('')
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
const fallbackTitleMetrics = useMemo(() => ({
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
rangeY: Math.max(48, safe.headerPaddingTop * 0.45)
}), [safe.headerPaddingTop, safe.statusBarHeight])
const titleMetricsRef = useRef(fallbackTitleMetrics)
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const latestScrollTopRef = useRef(0)
const applyTopMaskProgress = useCallback((scrollTop: number) => {
latestScrollTopRef.current = scrollTop
if (scrollFrameRef.current !== null) {
return
}
scrollFrameRef.current = setTimeout(() => {
scrollFrameRef.current = null
const { startY, rangeY } = titleMetricsRef.current
setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY }))
}, 16)
}, [])
useEffect(() => {
let cancelled = false
let measureTimer: ReturnType<typeof setTimeout> | null = null
titleMetricsRef.current = fallbackTitleMetrics
type HomeHeaderRect = Taro.NodesRef.BoundingClientRectCallbackResult
const measureTitle = () => {
if (cancelled) return
Taro.createSelectorQuery()
.select('.home-header')
.boundingClientRect((rect: HomeHeaderRect | HomeHeaderRect[]) => {
if (cancelled) return
if (!rect || Array.isArray(rect) || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
titleMetricsRef.current = fallbackTitleMetrics
applyTopMaskProgress(latestScrollTopRef.current)
return
}
const startY = getTopMaskStartY({
viewportBottom: rect.bottom,
scrollTop: latestScrollTopRef.current,
statusBarHeight: safe.statusBarHeight
})
const rangeY = Math.max(32, rect.height * 0.7)
titleMetricsRef.current = { startY, rangeY }
applyTopMaskProgress(latestScrollTopRef.current)
})
.exec()
}
Taro.nextTick(() => {
if (cancelled) return
measureTimer = setTimeout(measureTitle, 80)
})
return () => {
cancelled = true
if (measureTimer !== null) {
clearTimeout(measureTimer)
}
if (scrollFrameRef.current !== null) {
clearTimeout(scrollFrameRef.current)
}
scrollFrameRef.current = null
}
}, [applyTopMaskProgress, fallbackTitleMetrics])
Taro.usePageScroll((e) => {
applyTopMaskProgress(e.scrollTop)
})
const navigateToProduct = (categoryId: string) => { const navigateToProduct = (categoryId: string) => {
Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` }) Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` })
} }
@@ -174,17 +99,11 @@ export default function Index() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<View <ScrollTopMask title='智绘微刻' targetSelector='.home-header' />
className='home-top-mask'
style={{
height: `${Math.max(safe.menuButtonTop + safe.menuButtonHeight + 18, safe.statusBarHeight + 52)}px`,
opacity: topMaskOpacity
}}
/>
<View className='index-page'> <View className='index-page'>
{/* 页头 */} {/* 页头 */}
<View className='home-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='home-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='home-title'></Text> <Text className='home-title'></Text>
</View> </View>
@@ -194,7 +113,8 @@ export default function Index() {
<Input <Input
className='search-input' className='search-input'
type='text' type='text'
placeholder='搜索定制品类笔记本、杯垫、书灯...' placeholder='搜索定制品类,如笔记本、杯垫、书灯'
placeholderClass='search-input-placeholder'
value={searchKey} value={searchKey}
onInput={handleSearch} onInput={handleSearch}
confirmType='search' confirmType='search'
+5 -2
View File
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store' import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store'
import { PRODUCT_ICON_MAP } from '../../utils/productConfig' import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
@@ -55,8 +56,9 @@ export default function OrderDetailPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="订单详情" targetSelector=".page-header" showBack />
<View className='order-detail-page'> <View className='order-detail-page'>
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
@@ -73,9 +75,10 @@ export default function OrderDetailPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="订单详情" targetSelector=".page-header" showBack />
<View className='order-detail-page'> <View className='order-detail-page'>
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
+1 -1
View File
@@ -1,5 +1,5 @@
.orders-page { .orders-page {
padding: 0 32rpx 48rpx; padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
min-height: 100vh; min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
} }
+3 -1
View File
@@ -10,6 +10,7 @@ import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import LoginGuard from '../../components/LoginGuard' import LoginGuard from '../../components/LoginGuard'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
const STATUS_TABS = [ const STATUS_TABS = [
{ code: 'all', label: '全部' }, { code: 'all', label: '全部' },
@@ -84,10 +85,11 @@ export default function OrdersPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="我的订单" targetSelector=".orders-page .page-header" />
<LoginGuard> <LoginGuard>
<View className='orders-page'> <View className='orders-page'>
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='page-title'></Text> <Text className='page-title'></Text>
</View> </View>
+5 -2
View File
@@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import BottomActionBar from '../../components/BottomActionBar' import BottomActionBar from '../../components/BottomActionBar'
import { getProductIconImg } from '../../utils/productConfig' import { getProductIconImg } from '../../utils/productConfig'
@@ -27,8 +28,9 @@ export default function ProductPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="商品详情" targetSelector=".page-header" showBack />
<View className='product-page'> <View className='product-page'>
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
@@ -72,9 +74,10 @@ export default function ProductPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="商品详情" targetSelector=".page-header" showBack />
<View className='product-page'> <View className='product-page'>
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
<View className='page-header-inner'> <View className='page-header-inner'>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
+1 -1
View File
@@ -1,5 +1,5 @@
.profile-page { .profile-page {
padding: 0 32rpx 48rpx; padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
min-height: 100vh; min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
} }
+3 -1
View File
@@ -7,6 +7,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { login, getMe, updateProfile, getToken } from '../../utils/api' import { login, getMe, updateProfile, getToken } from '../../utils/api'
import { safeHideLoading } from '../../utils/request' import { safeHideLoading } from '../../utils/request'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
@@ -279,10 +280,11 @@ export default function ProfilePage() {
return ( return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}> <View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="我的" targetSelector=".profile-header" />
<View className='profile-page'> <View className='profile-page'>
{/* 用户信息头部 */} {/* 用户信息头部 */}
<View className='profile-header surface-card' style={{ marginTop: `${safe.headerPaddingTop}px` }}> <View className='profile-header surface-card' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
<View className='user-info'> <View className='user-info'>
{isLoggedIn && userInfo.avatarUrl ? ( {isLoggedIn && userInfo.avatarUrl ? (
<Image className='avatar' src={userInfo.avatarUrl} mode='aspectFill' /> <Image className='avatar' src={userInfo.avatarUrl} mode='aspectFill' />
+38 -1
View File
@@ -9,19 +9,56 @@
} }
.service-page .page-header.service-header { .service-page .page-header.service-header {
display: flex;
align-items: center;
gap: 16rpx;
margin: 0; margin: 0;
padding-left: 0; padding-left: 0;
padding-right: 0; padding-right: 0;
padding-bottom: 32rpx; padding-bottom: 32rpx;
text-align: center; }
.service-header .back-btn {
display: flex;
align-items: center;
justify-content: center;
width: 88rpx;
height: 88rpx;
flex: 0 0 auto;
border-radius: 50%;
background: var(--bg-card);
border: 1rpx solid var(--border-strong);
box-shadow: var(--shadow-card);
box-sizing: border-box;
}
.service-header .back-btn:active {
transform: scale(0.96);
}
.service-header .back-arrow {
font-size: 36rpx;
line-height: 1;
color: var(--text-primary);
} }
.service-header .page-title { .service-header .page-title {
flex: 1;
min-width: 0;
text-align: center;
font-size: 44rpx; font-size: 44rpx;
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
color: var(--text-primary); color: var(--text-primary);
letter-spacing: 0; letter-spacing: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.service-header .header-spacer {
width: 88rpx;
flex: 0 0 auto;
} }
/* 客服主入口 */ /* 客服主入口 */
+7 -1
View File
@@ -5,6 +5,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
const FAQ_ITEMS = [ const FAQ_ITEMS = [
@@ -22,9 +23,14 @@ export default function ServicePage() {
return ( return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}> <View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="联系客服" targetSelector=".page-header.service-header" showBack />
<View className='service-page'> <View className='service-page'>
<View className='page-header service-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header service-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text>
</View>
<Text className='page-title'></Text> <Text className='page-title'></Text>
<View className='header-spacer' />
</View> </View>
{/* 客服入口 */} {/* 客服入口 */}
+3 -1
View File
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { getUserInfo, setUserInfo, clearUserInfo, getAddressList, getDefaultAddress, type AddressItem } from '../../utils/store' import { getUserInfo, setUserInfo, clearUserInfo, getAddressList, getDefaultAddress, type AddressItem } from '../../utils/store'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
@@ -69,10 +70,11 @@ export default function SettingsPage() {
return ( return (
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}> <View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="设置" targetSelector=".page-header.settings-header" showBack />
<View className='settings-page'> <View className='settings-page'>
<View className='settings-area'> <View className='settings-area'>
<View className='page-header settings-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header settings-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='back-btn' onTap={() => Taro.navigateBack()}> <View className='back-btn' onTap={() => Taro.navigateBack()}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
</View> </View>
+3
View File
@@ -8,6 +8,7 @@ import { useSafeArea } from '../../../hooks/useSafeArea'
import { useStatusBar } from '../../../hooks/useStatusBar' import { useStatusBar } from '../../../hooks/useStatusBar'
import { useThemeContext } from '../../../context/ThemeContext' import { useThemeContext } from '../../../context/ThemeContext'
import ThemedPageMeta from '../../../components/ThemedPageMeta' import ThemedPageMeta from '../../../components/ThemedPageMeta'
import ScrollTopMask from '../../../components/ScrollTopMask'
import BottomActionBar from '../../../components/BottomActionBar' import BottomActionBar from '../../../components/BottomActionBar'
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max) const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
@@ -111,6 +112,7 @@ export default function ShopDetailPage() {
return ( return (
<View className="shop-detail-page theme-dark"> <View className="shop-detail-page theme-dark">
<ThemedPageMeta darkTop /> <ThemedPageMeta darkTop />
<ScrollTopMask title="商品详情" targetSelector=".shop-back-btn" showBack />
<Text style={{ color: '#fff', padding: '40rpx' }}></Text> <Text style={{ color: '#fff', padding: '40rpx' }}></Text>
</View> </View>
) )
@@ -119,6 +121,7 @@ export default function ShopDetailPage() {
return ( return (
<View className="shop-detail-page theme-dark"> <View className="shop-detail-page theme-dark">
<ThemedPageMeta darkTop /> <ThemedPageMeta darkTop />
<ScrollTopMask title="商品详情" targetSelector=".shop-back-btn" showBack />
{/* Fixed Hero */} {/* Fixed Hero */}
<View className="shop-hero-fixed"> <View className="shop-hero-fixed">
<View <View
+1 -1
View File
@@ -4,7 +4,7 @@
/* --- List Layer --- */ /* --- List Layer --- */
.shop-list { .shop-list {
padding: 0 32rpx 24rpx; padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
} }
.shop-header { .shop-header {
+3 -1
View File
@@ -4,6 +4,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import './index.scss' import './index.scss'
import { PRODUCTS } from '../../utils/productConfig' import { PRODUCTS } from '../../utils/productConfig'
@@ -19,8 +20,9 @@ export default function ShopPage() {
return ( return (
<View className={`shop-page theme-${resolvedTheme}`}> <View className={`shop-page theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="智绘精选" targetSelector=".shop-header" />
<View className="shop-list"> <View className="shop-list">
<View className="shop-header" style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className="shop-header" style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className="shop-header-title"></Text> <Text className="shop-header-title"></Text>
<Text className="shop-header-sub"></Text> <Text className="shop-header-sub"></Text>
</View> </View>
+5 -2
View File
@@ -14,6 +14,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
export default function UserDatabasePage() { export default function UserDatabasePage() {
@@ -86,7 +87,8 @@ export default function UserDatabasePage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<View className='udb-page udb-lock-page' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <ScrollTopMask title="本地用户数据库" targetSelector=".page-header.udb-header" />
<View className='udb-page udb-lock-page' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='udb-lock-card surface-card'> <View className='udb-lock-card surface-card'>
<Image className='udb-lock-icon' src={assetUrl('/icon/账号管理.png')} mode='aspectFit' /> <Image className='udb-lock-icon' src={assetUrl('/icon/账号管理.png')} mode='aspectFit' />
<Text className='udb-lock-title'></Text> <Text className='udb-lock-title'></Text>
@@ -114,9 +116,10 @@ export default function UserDatabasePage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="本地用户数据库" targetSelector=".page-header.udb-header" />
<View className='udb-page'> <View className='udb-page'>
<View className='page-header udb-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header udb-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<Text className='page-title'></Text> <Text className='page-title'></Text>
</View> </View>
+3 -1
View File
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
import { useSafeArea } from '../../hooks/useSafeArea' import { useSafeArea } from '../../hooks/useSafeArea'
import { useStatusBar } from '../../hooks/useStatusBar' import { useStatusBar } from '../../hooks/useStatusBar'
import ThemedPageMeta from '../../components/ThemedPageMeta' import ThemedPageMeta from '../../components/ThemedPageMeta'
import ScrollTopMask from '../../components/ScrollTopMask'
import { assetUrl } from '../../utils/asset' import { assetUrl } from '../../utils/asset'
export default function WordCloudPage() { export default function WordCloudPage() {
@@ -64,9 +65,10 @@ export default function WordCloudPage() {
return ( return (
<View className={`theme-${resolvedTheme}`}> <View className={`theme-${resolvedTheme}`}>
<ThemedPageMeta /> <ThemedPageMeta />
<ScrollTopMask title="AI词云生成" targetSelector=".page-header.cloud-header" showBack />
<View className='wordcloud-page'> <View className='wordcloud-page'>
<View className='page-header cloud-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}> <View className='page-header cloud-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
<View className='back-btn' onTap={goBack}> <View className='back-btn' onTap={goBack}>
<Text className='back-arrow'></Text> <Text className='back-arrow'></Text>
</View> </View>