1.2.0版本

This commit is contained in:
2026-08-04 11:07:38 +08:00
parent a091664cee
commit bf8ce482e4
78 changed files with 1998 additions and 863 deletions
+2 -1
View File
@@ -17,7 +17,8 @@
"Bash(npx terser *)",
"Bash(npm ls *)",
"Bash(npm install *)",
"Bash(node compress-images.js)"
"Bash(node compress-images.js)",
"Bash(cd \"F:\\\\\\\\weixin_wordc\")"
]
}
}
+22
View File
@@ -218,6 +218,28 @@ npm run dev:h5
---
## 最近更新记录(2026-08-04
### Batch 12 — 主题跟随 + 贴纸编辑 + 安全区适配
| 序号 | 修改内容 |
|------|----------|
| 1 | **主题跟随系统**`app.config.ts` 补 `darkmode: true``ThemeContext` 增加 `useDidShow` + `wx.onAppShow` 双兜底,每次回到前台重新检测系统主题 |
| 2 | **贴纸编辑画板空白**Canvas 2D 初始化改为 `Taro.nextTick()` + 300ms 自动重试;增加 `Math.round(cw * dpr)` 防小数尺寸;`img.src` 增加空值保护 |
| 3 | **画板截断统一**:DIY 工作台、预览弹窗、确认页三处 `.canvas-area` / `.preview-canvas` 统一补充 `max-width: 100%` + `box-sizing: border-box` |
| 4 | **顶部安全区适配**`env(safe-area-inset-top, 20px)` 兜底 + 48px 按钮高度;各页面独立 `.page-header` 强制 `!important` 对齐全局 |
### 已知遗留问题(待修复)
| 序号 | 问题 | 状态 |
|------|------|------|
| 1 | **地址在设计中添加后无法及时同步** | ❌ 未修复 — checkout 页从 store 读取地址时,design 数据写入和读取生命周期不一致 |
| 2 | **贴纸编辑页无法运行** | ❌ 未修复 — Canvas 2D `createImage` 在某些基础库版本返回 `undefined`;线稿 AI 接口未接入 |
| 3 | **个人主页登录后页面失灵** | ❌ 未修复 — Profile 页 `useDidShow` 与 `LoginGuard` 状态竞争,导致登录后菜单点击事件丢失 |
| 4 | **跟随系统的主题切换按钮无法及时切换** | ⚠️ 部分修复 — `darkmode: true` + `useDidShow` 已加,但部分华为/小米机型 `wx.onThemeChange` 仍不触发,需进一步排查 |
---
## 最近更新记录(2026-08-02
### Batch 11 — 商品浏览页(Shop Page)沉浸式体验构建
+61 -2
View File
@@ -95,8 +95,67 @@
2.订单中每个条目的商品详情页中放置商品icon图片的链接似乎出了问题,只显示了文字(即图片地址映射的文字)没有显示真正的图片
3.订单中每个条目中的商品详情页缺少了物流进度,之前有的上次更新被删除了请补充回去。 -->
20260801 0:50 问题优化
<!-- 20260801 0:50 问题优化
1.关于订单详情中,夜间主题下订单信息和待付款的字样要切换为白色,而不是维持白天模式的黑丝,会看不见
2.所有点击更改收货地址后弹出的选择收货地址的方框的宽度尺寸超过了手机显示的尺寸,和其他框尺寸不统一,订单设计好的确认页面和订单详情页面针对这一功能都要做修改。
3.收货地址页面中,初始状态用户没有设置地址时的文字提示没有做主题切换的更改,昼夜两种模式下都为黑色导致夜间模式看不起字,和问题一一样要改为亮度较高的字体颜色。
3.收货地址页面中,初始状态用户没有设置地址时的文字提示没有做主题切换的更改,昼夜两种模式下都为黑色导致夜间模式看不起字,和问题一一样要改为亮度较高的字体颜色。 -->
<!-- 20260803 14:33 问题优化
1.主题切换按钮换到个人主页的设置中,加一个主题切换,然后把所有页面的切换按钮的都删去,这个主题切换有三种选择(浅色模式|深色模式|跟随系统)。
2.小程序每个页面的显示会和手机的顶部放时间的那个框冲突,实际上就是和不同机型的适配效果不匹配,看看有没有微信小程序提供什么函数或者方式可以让小程序匹配机型显示,顶部留出手机的系统空间,不冲突。
3.在设计DIY工作台中,选中贴纸,页面会出现(编辑按钮),可以一直有,只是选中了贴纸这个按钮才会亮起,不选中是灰色状态。点击后进入贴纸编辑页面,提供一些基础功能,比如裁切,调色相,亮度一些功能,还有转为线稿(接AI)的功能。
4.工作台拖动图片整个页面也在动,要固定住
5.设计清单页面中提供每个条目的删除按钮(或者搞个批量管理,可以选择条目然后直接删除),用户有不想继续设计的产品就直接删除就好了
6.商品设计中最后一个设计确认的成品展示显示画面不完全
右侧会有一部分画面无法展示。
7.商品页现在是单页模式,我想改成用户在商品页是一页,点击对应商品后跳到对应商品页,商品页效果不变,只是变成多页而已。 -->
<!-- 20260803 19:38 优化建议
1.主题切换默认更随系统,但是我修改系统暗色模式时小程序没有对应切换主题
2.贴纸编辑页没有出现我要修改的贴纸,画板那个部分是空白的,刊例来说我做出修改也能实时看到修改效果呀?
3.似乎没有做到贴合机型,比如订单页面编辑订单的按钮太上了,和小程序退出的按钮重合导致点击不到,可以改一下放置位置。 -->
20260803 2202 优化问题
1.贴纸编辑页依旧没有照片出现,实时展现贴纸修改效果
2.主题切换按钮更随系统的方式要做到系统实时改深浅,微信小程序提供这个api接口wx.onThemeChange 监听系统主题改变事件,可以接入用来监听实时切换小程序主题。API细节描述如下:
wx.onThemeChange(function listener)
基础库 2.11.0 开始支持,低版本需做兼容处理。
小程序插件:不支持
相关文档: DarkMode 适配指南
功能描述
监听系统主题改变事件。该事件与 App.onThemeChange 的回调时机一致。
参数
function listener
系统主题改变事件的监听函数
参数
Object res
属性 类型 说明
theme string 系统当前的主题,取值为light或dark
合法值 说明
dark 深色主题
light 浅色主题
注意
只有在全局配置"darkmode": true时才会触发此事件。
3.依旧设计时的画板展示部分和确认设计时的显示不完全相同,有截断的情况,这个问题需要跟进处理
4.画面依旧有些不符合手机型号,你查看F:\weixin_wordc\wechat_api.md文档中,主要关注一些有没有监控手机型号的api调用,调用后可清楚了解尺寸并做好适配当前机型的页面调整,现在还是有一些和手机时间栏那部分冲突。应当有一些顶部画面的处理
-1
View File
File diff suppressed because one or more lines are too long
Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
{"pages":["pages/index/index","pages/shop/index","pages/product/index","pages/diy/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":"light","navigationStyle":"custom","backgroundColor":"#ffffff"},"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":"我的"}]}}
{"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":"light","navigationStyle":"custom","backgroundColor":"#ffffff","darkmode":true},"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":"我的"}]}}
+1 -1
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -133,6 +133,14 @@
</image>
</template>
<template name="tmpl_0_15">
<canvas canvas-id="{{i.p0}}" disable-scroll="{{xs.b(i.p1,!1)}}" binderror="eh" bindtouchstart="eh" bindtouchmove="eh" bindtouchend="eh" bindtouchcancel="eh" bindlongtap="eh" type="{{i.p2}}" style="{{i.st}}" class="{{i.cl}}" bindtap="eh" id="{{i.uid||i.sid}}" data-sid="{{i.sid}}">
<block wx:for="{{i.cn}}" wx:key="sid">
<template is="{{xs.a(c, item.nn, l)}}" data="{{i:item,c:c+1,l:xs.f(l,item.nn)}}" />
</block>
</canvas>
</template>
<template name="tmpl_0_8">
<block>{{i.v}}</block>
</template>
+1 -1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.theme-toggle{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;backdrop-filter:blur(8rpx);-webkit-backdrop-filter:blur(8rpx);background:rgba(0,0,0,.35);border:1rpx solid hsla(0,0%,100%,.25);border-radius:50%;bottom:150rpx;-webkit-box-shadow:0 4rpx 16rpx rgba(0,0,0,.2);box-shadow:0 4rpx 16rpx rgba(0,0,0,.2);display:-webkit-flex;display:-ms-flexbox;display:flex;height:80rpx;-webkit-justify-content:center;justify-content:center;left:24rpx;position:fixed;-webkit-transition:all .3s ease;transition:all .3s ease;width:80rpx;z-index:900}.theme-toggle:active{background:rgba(0,0,0,.5);-webkit-transform:scale(.92);-ms-transform:scale(.92);transform:scale(.92)}.theme-icon{font-size:36rpx;line-height:1}
+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(1268),r=t(9703),i=t(4848);function l(){var e=(0,r.wR)(),n=e.theme,t=[{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,i.jsx)(s.Ss,{className:"theme-".concat(n),children:(0,i.jsxs)(s.Ss,{className:"agreement-page",children:[(0,i.jsx)(c.A,{}),(0,i.jsxs)(s.Ss,{className:"agreement-content",children:[(0,i.jsx)(s.EY,{className:"agreement-title",children:"\u5b9a\u5236\u534f\u8bae"}),t.map(function(e,n){return(0,i.jsxs)(s.Ss,{className:"agreement-section",children:[(0,i.jsx)(s.EY,{className:"agreement-h3",children:e.title}),(0,i.jsx)(s.EY,{className:"agreement-p",children:e.content})]},n)})]})]})})}var o={navigationBarTitleText:"\u5b9a\u5236\u534f\u8bae"};Page((0,a.createPageConfig)(l,"pages/agreement/index",{root:{cn:[]}},o||{}))}},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,n,t){var a=t(7842),s=t(118),c=t(9703),r=t(4848);function i(){var e=(0,c.wR)(),n=(e.theme,e.resolvedTheme),t=[{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,r.jsx)(s.Ss,{className:"theme-".concat(n),children:(0,r.jsx)(s.Ss,{className:"agreement-page",children:(0,r.jsxs)(s.Ss,{className:"agreement-content",children:[(0,r.jsx)(s.EY,{className:"agreement-title",children:"\u5b9a\u5236\u534f\u8bae"}),t.map(function(e,n){return(0,r.jsxs)(s.Ss,{className:"agreement-section",children:[(0,r.jsx)(s.EY,{className:"agreement-h3",children:e.title}),(0,r.jsx)(s.EY,{className:"agreement-p",children:e.content})]},n)})]})})})}var l={navigationBarTitleText:"\u5b9a\u5236\u534f\u8bae"};Page((0,a.createPageConfig)(i,"pages/agreement/index",{root:{cn:[]}},l||{}))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[907,96,76],function(){return n(6855)});e.O()}]);
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.checkout-page{padding:0 24rpx 24rpx}.back-btn{font-size:40rpx;padding:10rpx}.checkout-canvas{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;justify-content:center;padding:40rpx}.canvas-area{background:rgba(0,0,0,.03);border:2rpx dashed var(--line-star);overflow:hidden;position:relative}.checkout-image{height:100%;left:0;position:absolute;top:0;width:100%}.checkout-info{padding:24rpx}.info-row{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;border-bottom:1rpx dashed rgba(0,0,0,.06);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;padding:20rpx 0}.info-row:last-child{border-bottom:none}.info-label{color:var(--text-secondary);font-size:28rpx}.info-value{color:var(--text-primary);font-size:28rpx;font-weight:500}.total-row{margin-top:8rpx;padding-top:24rpx}.info-total{color:var(--accent-pink);font-size:40rpx;font-weight:800}.checkout-address-card{padding:24rpx}.checkout-address-header{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:16rpx}.address-change{background:rgba(91,140,255,.08);border-radius:8rpx;padding:6rpx 16rpx}.change-text{color:var(--accent-blue);font-size:24rpx}.checkout-address-body{padding-top:8rpx}.addr-row{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;margin-bottom:8rpx}.addr-name{color:var(--text-primary);font-size:28rpx;font-weight:700}.addr-phone{color:var(--text-secondary);font-size:26rpx}.addr-detail{color:var(--text-secondary);font-size:26rpx;line-height:1.5}.checkout-address-empty{padding:32rpx 0;text-align:center}.empty-text{color:var(--text-secondary);font-size:26rpx}.addr-picker-header{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:20rpx}.addr-picker-title{color:var(--text-primary);font-size:32rpx;font-weight:700}.addr-picker-close{color:var(--text-secondary);font-size:28rpx;padding:8rpx}.addr-picker-list{-webkit-flex:1;-ms-flex:1;flex:1;overflow-y:auto}.addr-picker-item{border-bottom:1rpx dashed rgba(0,0,0,.05);padding:20rpx 16rpx}.addr-picker-item.active{background:rgba(91,140,255,.06);border-radius:12rpx}.addr-picker-row{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;margin-bottom:8rpx}.addr-picker-name{color:var(--text-primary);font-size:28rpx;font-weight:700}.addr-picker-phone{color:var(--text-secondary);font-size:26rpx}.addr-picker-default{border:1rpx solid var(--accent-pink);border-radius:6rpx;color:var(--accent-pink);font-size:22rpx;padding:2rpx 8rpx}.addr-picker-detail{color:var(--text-secondary);font-size:26rpx}.addr-picker-empty{color:var(--text-secondary);font-size:28rpx;padding:40rpx;text-align:center}.addr-picker-add{border:2rpx dashed var(--accent-blue);border-radius:12rpx;margin-top:16rpx;padding:20rpx;text-align:center}.addr-picker-add-text{color:var(--accent-blue);font-size:28rpx}.checkout-actions{background:var(--bg-card);border-top:var(--line-card);bottom:0;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:24rpx;left:0;padding:20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));position:fixed;right:0;z-index:500}.checkout-actions .btn-gradient,.checkout-actions .btn-outline{-webkit-flex:1;-ms-flex:1;flex:1;text-align:center}
.checkout-page{padding:0 24rpx 24rpx}.back-btn{font-size:40rpx;padding:10rpx}.checkout-canvas{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:center;justify-content:center;padding:40rpx}.canvas-area{background:rgba(0,0,0,.03);border:2rpx dashed var(--line-star);-webkit-box-sizing:border-box;box-sizing:border-box;max-width:100%;overflow:hidden;position:relative}.checkout-image{left:0;max-height:100%;max-width:100%;position:absolute;top:0}.checkout-info{padding:24rpx}.info-row{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;border-bottom:1rpx dashed rgba(0,0,0,.06);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;padding:20rpx 0}.info-row:last-child{border-bottom:none}.info-label{color:var(--text-secondary);font-size:28rpx}.info-value{color:var(--text-primary);font-size:28rpx;font-weight:500}.total-row{margin-top:8rpx;padding-top:24rpx}.info-total{color:var(--accent-pink);font-size:40rpx;font-weight:800}.checkout-address-card{padding:24rpx}.checkout-address-header{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:16rpx}.address-change{background:rgba(91,140,255,.08);border-radius:8rpx;padding:6rpx 16rpx}.change-text{color:var(--accent-blue);font-size:24rpx}.checkout-address-body{padding-top:8rpx}.addr-row{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;margin-bottom:8rpx}.addr-name{color:var(--text-primary);font-size:28rpx;font-weight:700}.addr-phone{color:var(--text-secondary);font-size:26rpx}.addr-detail{color:var(--text-secondary);font-size:26rpx;line-height:1.5}.checkout-address-empty{padding:32rpx 0;text-align:center}.empty-text{color:var(--text-secondary);font-size:26rpx}.addr-picker-header{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:20rpx}.addr-picker-title{color:var(--text-primary);font-size:32rpx;font-weight:700}.addr-picker-close{color:var(--text-secondary);font-size:28rpx;padding:8rpx}.addr-picker-list{-webkit-flex:1;-ms-flex:1;flex:1;overflow-y:auto}.addr-picker-item{border-bottom:1rpx dashed rgba(0,0,0,.05);padding:20rpx 16rpx}.addr-picker-item.active{background:rgba(91,140,255,.06);border-radius:12rpx}.addr-picker-row{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;margin-bottom:8rpx}.addr-picker-name{color:var(--text-primary);font-size:28rpx;font-weight:700}.addr-picker-phone{color:var(--text-secondary);font-size:26rpx}.addr-picker-default{border:1rpx solid var(--accent-pink);border-radius:6rpx;color:var(--accent-pink);font-size:22rpx;padding:2rpx 8rpx}.addr-picker-detail{color:var(--text-secondary);font-size:26rpx}.addr-picker-empty{color:var(--text-secondary);font-size:28rpx;padding:40rpx;text-align:center}.addr-picker-add{border:2rpx dashed var(--accent-blue);border-radius:12rpx;margin-top:16rpx;padding:20rpx;text-align:center}.addr-picker-add-text{color:var(--accent-blue);font-size:28rpx}.checkout-actions{background:var(--bg-card);border-top:var(--line-card);bottom:0;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:24rpx;left:0;padding:20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));position:fixed;right:0;z-index:500}.checkout-actions .btn-gradient,.checkout-actions .btn-outline{-webkit-flex:1;-ms-flex:1;flex:1;text-align:center}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.design-page{padding:0 24rpx 24rpx}.status-tabs{margin:20rpx 0}.tabs-scroll{white-space:nowrap}.tab-item{display:inline-block;padding:20rpx 32rpx;position:relative}.tab-label{color:var(--text-secondary);font-size:28rpx;font-weight:500}.tab-item.active .tab-label{color:var(--accent-pink);font-weight:700}.tab-underline{background:var(--accent-pink);border-radius:2rpx;bottom:8rpx;height:4rpx;left:50%;position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:40rpx}.design-list{padding-bottom:24rpx}.design-card{margin-bottom:20rpx;padding:28rpx}.design-body{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:20rpx;margin-bottom:20rpx}.design-icon{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;font-size:56rpx}.design-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:56rpx;width:56rpx}.design-info{-webkit-flex:1;-ms-flex:1;flex:1}.design-title-row{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:12rpx}.design-name{color:var(--text-primary);font-size:32rpx;font-weight:700}.design-meta{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:8rpx}.design-total{color:var(--accent-pink);font-size:26rpx;font-weight:600}.design-actions{-ms-flex-pack:end;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;justify-content:flex-end}.design-btn{font-size:26rpx;padding:16rpx 40rpx}.empty-state{-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:100rpx 40rpx;text-align:center}.empty-icon-img{height:56rpx;margin-bottom:24rpx;width:56rpx}.empty-text{color:var(--text-primary);font-size:32rpx;font-weight:700;margin-bottom:12rpx}.empty-sub{color:var(--text-secondary);font-size:26rpx;margin-bottom:32rpx}
.design-page{min-height:100vh;padding:0 24rpx 24rpx}.manage-btn{color:var(--accent-blue);font-size:28rpx;font-weight:500}.status-tabs{background:var(--bg-card);border:var(--line-card);border-radius:16rpx;margin:20rpx 0;padding:8rpx 0}.tabs-scroll{white-space:nowrap}.tab-item{display:inline-block;padding:20rpx 32rpx;position:relative}.tab-label{color:var(--text-secondary);font-size:28rpx;font-weight:500}.tab-item.active .tab-label{color:var(--accent-pink);font-weight:700}.tab-underline{background:var(--accent-pink);border-radius:2rpx;bottom:8rpx;height:4rpx;left:50%;position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:40rpx}.batch-bar{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:16rpx;padding:20rpx 24rpx}.batch-select-all{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx}.batch-checkbox{-ms-flex-negative:0;border:3rpx dashed var(--text-secondary);border-radius:50%;-webkit-flex-shrink:0;flex-shrink:0;height:36rpx;-webkit-transition:all .2s;transition:all .2s;width:36rpx}.batch-checkbox.checked{background:var(--accent-pink);border-color:var(--accent-pink)}.batch-text{color:var(--text-primary);font-size:26rpx}.batch-delete{color:var(--text-secondary);font-size:26rpx;padding:10rpx 24rpx}.batch-delete.active{color:#ff4757;font-weight:600}.design-list{padding-bottom:24rpx}.design-card{margin-bottom:20rpx;padding:28rpx;-webkit-transition:all .2s ease;transition:all .2s ease}.design-card.selected{background:rgba(255,154,158,.06);border-color:var(--accent-pink)}.design-body{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:20rpx;margin-bottom:20rpx}.design-icon{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;font-size:56rpx}.design-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:56rpx;width:56rpx}.design-info{-webkit-flex:1;-ms-flex:1;flex:1}.design-title-row{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:12rpx}.design-name{color:var(--text-primary);font-size:32rpx;font-weight:700}.design-meta{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:8rpx}.design-total{color:var(--accent-pink);font-size:26rpx;font-weight:600}.design-actions{-ms-flex-pack:end;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;justify-content:flex-end}.design-btn{font-size:26rpx;padding:16rpx 40rpx}.empty-state{-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:100rpx 40rpx;text-align:center}.empty-icon-img{height:56rpx;margin-bottom:24rpx;width:56rpx}.empty-text{color:var(--text-primary);font-size:32rpx;font-weight:700;margin-bottom:12rpx}.empty-sub{color:var(--text-secondary);font-size:26rpx;margin-bottom:32rpx}
+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
View File
@@ -0,0 +1 @@
{"navigationBarTitleText":"编辑贴纸","usingComponents":{"comp":"../../../comp"}}
+2
View File
@@ -0,0 +1,2 @@
<import src="../../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />
+1
View File
@@ -0,0 +1 @@
.sticker-edit-page{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;min-height:100vh;padding:0 24rpx 24rpx}.edit-header{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;padding:calc(env(safe-area-inset-top, 0rpx) + 24rpx) 0 24rpx}.edit-back{color:var(--text-primary);font-size:36rpx;width:60rpx}.edit-title{color:var(--text-primary);-webkit-flex:1;-ms-flex:1;flex:1;font-size:34rpx;font-weight:700;text-align:center}.edit-save{color:var(--accent-pink);font-size:28rpx;font-weight:600;text-align:right;width:60rpx}.edit-canvas-wrap{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:1;-ms-flex:1;flex:1;-webkit-justify-content:center;justify-content:center;min-height:400rpx;padding:20rpx 0}.edit-canvas{background:var(--bg-input);border-radius:16rpx;height:500rpx;width:100%}.edit-tools{padding:32rpx}.edit-row{-ms-flex-align:center;-ms-flex-pack:justify;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:32rpx}.edit-row:last-child{margin-bottom:0}.edit-label{-ms-flex-negative:0;color:var(--text-primary);-webkit-flex-shrink:0;flex-shrink:0;font-size:28rpx;font-weight:500;width:120rpx}.slider-wrap{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:1;-ms-flex:1;flex:1;gap:16rpx}.slider-track{background:var(--bg-input);border-radius:4rpx;-webkit-flex:1;-ms-flex:1;flex:1;height:8rpx;position:relative}.slider-fill{background:-webkit-gradient(linear,left top,right top,from(#ff9a9e),to(#fecfef));background:-webkit-linear-gradient(left,#ff9a9e,#fecfef);background:linear-gradient(90deg,#ff9a9e,#fecfef);border-radius:4rpx;height:100%;left:0;position:absolute;top:0}.slider-thumb{background:#fff;border:4rpx solid var(--accent-pink);border-radius:50%;-webkit-box-shadow:0 2rpx 8rpx rgba(0,0,0,.15);box-shadow:0 2rpx 8rpx rgba(0,0,0,.15);height:32rpx;position:absolute;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:32rpx}.slider-value{color:var(--text-secondary);font-size:24rpx;min-width:60rpx;text-align:right}.edit-quick-actions{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:16rpx}.quick-btn{background:var(--bg-input);border:2rpx dashed var(--line-star);border-radius:40rpx;color:var(--text-primary);font-size:24rpx;font-weight:500;padding:16rpx 28rpx;-webkit-transition:all .2s;transition:all .2s}.quick-btn:active{opacity:.8;-webkit-transform:scale(.97);-ms-transform:scale(.97);transform:scale(.97)}.quick-btn.active{background:rgba(255,154,158,.12);border-color:var(--accent-pink);color:var(--accent-pink)}.edit-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;gap:20rpx;padding:20rpx 0 calc(20rpx + env(safe-area-inset-bottom))}.edit-footer .btn-gradient,.edit-footer .btn-outline{-webkit-flex:1;-ms-flex:1;flex:1;font-size:28rpx;padding:24rpx 0;text-align:center}.edit-loading{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;background:rgba(0,0,0,.4);bottom:0;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-justify-content:center;justify-content:center;left:0;position:fixed;right:0;top:0;z-index:1000}.edit-loading-text{color:#fff;font-size:28rpx;margin-top:20rpx}
+1 -1
View File
@@ -1 +1 @@
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[421],{7265:function(e,s,a){var c=a(7842),i=a(5544),r=a(118),t=a(758),l=a.n(t),n=a(6540),o=a(5234),m=a(1268),d=a(9703),h=a(4848),g=[{title:"\u6bd5\u4e1a\u7eaa\u5ff5\u7b14\u8bb0\u672c",desc:"\u5168\u73ed\u540d\u5b57\u7ec4\u6210\u6821\u5fbd",image:"/img/book_small/The1.jpg"},{title:"\u94dc\u8d28\u676f\u57ab",desc:"\u91d1\u5c5e\u8d28\u611f\u684c\u9762\u827a\u672f",image:"/img/cup/The1.jpg"},{title:"\u7af9\u5236\u7b14\u76d2",desc:"\u81ea\u7136\u7af9\u7eb9\u6587\u623f\u96c5\u5668",image:"/img/penbox/The1.jpg"},{title:"\u4e66\u672c\u578b\u706f",desc:"\u6e29\u6696\u5149\u5f71\u70b9\u4eae\u5fc3\u610f",image:"/img/booklight/The1.jpg"},{title:"\u60c5\u4fa3\u5b9a\u5236\u793c",desc:"\u4e24\u4e2a\u4eba\u7684\u540d\u5b57\u4ea4\u7ec7",image:"/img/book_big/The1.jpg"},{title:"\u4f01\u4e1a\u5e74\u4f1a\u793c",desc:"\u5458\u5de5\u540d\u5b57\u7ec4\u6210Logo",image:"/img/penbox/The2.jpg"}],p={"notebook-small":"/img/book_small/The1.jpg","notebook-large":"/img/book_big/The1.jpg",coaster:"/img/cup/The1.jpg",penbox:"/img/penbox/The1.jpg",booklamp:"/img/booklight/The1.jpg"};function x(){var e=(0,d.wR)(),s=e.theme,a=(0,n.useState)(""),c=(0,i.A)(a,2),t=c[0],x=c[1],j=function(e){l().navigateTo({url:"/pages/product/index?id=".concat(e)})},N=t.trim()?o.R6.filter(function(e){return e.name.includes(t)||e.desc.includes(t)}):o.R6,u=function(e){x(e.detail.value)};return(0,h.jsx)(r.Ss,{className:"theme-".concat(s),children:(0,h.jsxs)(r.Ss,{className:"index-page",children:[(0,h.jsx)(m.A,{}),(0,h.jsx)(r.Ss,{className:"header-bar",children:(0,h.jsx)(r.EY,{className:"header-title",children:"\u667a\u7ed8\u5fae\u523b"})}),(0,h.jsxs)(r.Ss,{className:"search-card dashed-card",children:[(0,h.jsx)(r.Ss,{className:"star-badge"}),(0,h.jsxs)(r.Ss,{className:"search-inner",children:[(0,h.jsx)(r._V,{className:"search-icon-img",src:"/icon/\u641c\u7d22.png",mode:"aspectFit"}),(0,h.jsx)(r.pd,{className:"search-input",type:"text",placeholder:"\u641c\u7d22\u5b9a\u5236\u54c1\u7c7b\uff1a\u7b14\u8bb0\u672c\u3001\u676f\u57ab\u3001\u4e66\u706f...",value:t,onInput:u,confirmType:"search"})]}),t.trim()&&(0,h.jsx)(r.Ss,{className:"search-result-hint",children:(0,h.jsx)(r.EY,{className:"hint-text",children:N.length>0?"\u627e\u5230 ".concat(N.length," \u4e2a\u76f8\u5173\u54c1\u7c7b"):"\u6ca1\u6709\u627e\u5230\u76f8\u5173\u54c1\u7c7b\uff0c\u8bd5\u8bd5\u770b\u5176\u4ed6\u5173\u952e\u8bcd"})})]}),(0,h.jsxs)(r.Ss,{className:"showcase-section mt-20",children:[(0,h.jsx)(r.EY,{className:"section-title",children:"\u5b9a\u5236\u6210\u54c1\u5c55\u793a"}),(0,h.jsx)(r.RC,{className:"showcase-swiper",indicatorColor:"#e0e0e0",indicatorActiveColor:"#ff9a9e",circular:!0,autoplay:!0,interval:3e3,duration:500,previousMargin:"40rpx",nextMargin:"40rpx",indicatorDots:!0,children:g.map(function(e,s){return(0,h.jsx)(r.wu,{className:"showcase-swiper-item",children:(0,h.jsxs)(r.Ss,{className:"showcase-swiper-card dashed-card",children:[(0,h.jsx)(r.Ss,{className:"star-badge"}),(0,h.jsx)(r.Ss,{className:"showcase-image-wrapper",children:(0,h.jsx)(r._V,{className:"showcase-real-img",src:e.image,mode:"aspectFill"})}),(0,h.jsxs)(r.Ss,{className:"showcase-text-area",children:[(0,h.jsxs)(r.Ss,{className:"showcase-info",children:[(0,h.jsx)(r.EY,{className:"showcase-title",children:e.title}),(0,h.jsx)(r.EY,{className:"showcase-desc",children:e.desc})]}),(0,h.jsx)(r.Ss,{className:"showcase-tag",children:"\u6210\u54c1\u6848\u4f8b"})]})]})},s)})})]}),(0,h.jsxs)(r.Ss,{className:"category-section mt-20",children:[(0,h.jsx)(r.EY,{className:"section-title",children:"\u70ed\u95e8\u54c1\u7c7b"}),(0,h.jsx)(r.Ss,{className:"category-grid",children:N.map(function(e){return(0,h.jsxs)(r.Ss,{className:"category-item dashed-card",onTap:function(){return j(e.id)},children:[(0,h.jsx)(r.Ss,{className:"star-badge"}),(0,h.jsx)(r.Ss,{className:"category-img-wrapper",children:(0,h.jsx)(r._V,{className:"category-img-real",src:p[e.id]||"/icon/\u56db\u89d2\u661f.png",mode:"aspectFill"})}),(0,h.jsx)(r.EY,{className:"category-name",children:e.name}),(0,h.jsx)(r.EY,{className:"category-desc",children:e.desc}),(0,h.jsxs)(r.EY,{className:"category-price",children:["\xa5",e.price," \u8d77"]})]},e.id)})}),0===N.length&&(0,h.jsxs)(r.Ss,{className:"empty-category dashed-card",children:[(0,h.jsx)(r._V,{className:"empty-icon-img",src:"/icon/\u641c\u7d22.png",mode:"aspectFit"}),(0,h.jsx)(r.EY,{className:"empty-text",children:"\u672a\u627e\u5230\u5339\u914d\u7684\u54c1\u7c7b"}),(0,h.jsx)(r.EY,{className:"empty-sub",children:"\u5c1d\u8bd5\u641c\u7d22\u201c\u7b14\u8bb0\u672c\u201d\u3001\u201c\u676f\u57ab\u201d\u7b49"})]})]}),(0,h.jsx)(r.Ss,{className:"safe-bottom-placeholder"})]})})}var j={};Page((0,c.createPageConfig)(x,"pages/index/index",{root:{cn:[]}},j||{}))}},function(e){var s=function(s){return e(e.s=s)};e.O(0,[907,96,76],function(){return s(7265)});e.O()}]);
"use strict";(wx["webpackJsonp"]=wx["webpackJsonp"]||[]).push([[421],{7265:function(e,s,a){var c=a(7842),i=a(5544),r=a(118),t=a(758),l=a.n(t),n=a(6540),o=a(5234),m=a(9703),d=a(4848),h=[{title:"\u6bd5\u4e1a\u7eaa\u5ff5\u7b14\u8bb0\u672c",desc:"\u5168\u73ed\u540d\u5b57\u7ec4\u6210\u6821\u5fbd",image:"/img/book_small/The1.jpg"},{title:"\u94dc\u8d28\u676f\u57ab",desc:"\u91d1\u5c5e\u8d28\u611f\u684c\u9762\u827a\u672f",image:"/img/cup/The1.jpg"},{title:"\u7af9\u5236\u7b14\u76d2",desc:"\u81ea\u7136\u7af9\u7eb9\u6587\u623f\u96c5\u5668",image:"/img/penbox/The1.jpg"},{title:"\u4e66\u672c\u578b\u706f",desc:"\u6e29\u6696\u5149\u5f71\u70b9\u4eae\u5fc3\u610f",image:"/img/booklight/The1.jpg"},{title:"\u60c5\u4fa3\u5b9a\u5236\u793c",desc:"\u4e24\u4e2a\u4eba\u7684\u540d\u5b57\u4ea4\u7ec7",image:"/img/book_big/The1.jpg"},{title:"\u4f01\u4e1a\u5e74\u4f1a\u793c",desc:"\u5458\u5de5\u540d\u5b57\u7ec4\u6210Logo",image:"/img/penbox/The2.jpg"}],g={"notebook-small":"/img/book_small/The1.jpg","notebook-large":"/img/book_big/The1.jpg",coaster:"/img/cup/The1.jpg",penbox:"/img/penbox/The1.jpg",booklamp:"/img/booklight/The1.jpg"};function p(){var e=(0,m.wR)(),s=(e.theme,e.resolvedTheme),a=(0,n.useState)(""),c=(0,i.A)(a,2),t=c[0],p=c[1],x=function(e){l().navigateTo({url:"/pages/product/index?id=".concat(e)})},j=t.trim()?o.R6.filter(function(e){return e.name.includes(t)||e.desc.includes(t)}):o.R6,N=function(e){p(e.detail.value)};return(0,d.jsx)(r.Ss,{className:"theme-".concat(s),children:(0,d.jsxs)(r.Ss,{className:"index-page",children:[(0,d.jsx)(r.Ss,{className:"header-bar",children:(0,d.jsx)(r.EY,{className:"header-title",children:"\u667a\u7ed8\u5fae\u523b"})}),(0,d.jsxs)(r.Ss,{className:"search-card dashed-card",children:[(0,d.jsx)(r.Ss,{className:"star-badge"}),(0,d.jsxs)(r.Ss,{className:"search-inner",children:[(0,d.jsx)(r._V,{className:"search-icon-img",src:"/icon/\u641c\u7d22.png",mode:"aspectFit"}),(0,d.jsx)(r.pd,{className:"search-input",type:"text",placeholder:"\u641c\u7d22\u5b9a\u5236\u54c1\u7c7b\uff1a\u7b14\u8bb0\u672c\u3001\u676f\u57ab\u3001\u4e66\u706f...",value:t,onInput:N,confirmType:"search"})]}),t.trim()&&(0,d.jsx)(r.Ss,{className:"search-result-hint",children:(0,d.jsx)(r.EY,{className:"hint-text",children:j.length>0?"\u627e\u5230 ".concat(j.length," \u4e2a\u76f8\u5173\u54c1\u7c7b"):"\u6ca1\u6709\u627e\u5230\u76f8\u5173\u54c1\u7c7b\uff0c\u8bd5\u8bd5\u770b\u5176\u4ed6\u5173\u952e\u8bcd"})})]}),(0,d.jsxs)(r.Ss,{className:"showcase-section mt-20",children:[(0,d.jsx)(r.EY,{className:"section-title",children:"\u5b9a\u5236\u6210\u54c1\u5c55\u793a"}),(0,d.jsx)(r.RC,{className:"showcase-swiper",indicatorColor:"#e0e0e0",indicatorActiveColor:"#ff9a9e",circular:!0,autoplay:!0,interval:3e3,duration:500,previousMargin:"40rpx",nextMargin:"40rpx",indicatorDots:!0,children:h.map(function(e,s){return(0,d.jsx)(r.wu,{className:"showcase-swiper-item",children:(0,d.jsxs)(r.Ss,{className:"showcase-swiper-card dashed-card",children:[(0,d.jsx)(r.Ss,{className:"star-badge"}),(0,d.jsx)(r.Ss,{className:"showcase-image-wrapper",children:(0,d.jsx)(r._V,{className:"showcase-real-img",src:e.image,mode:"aspectFill"})}),(0,d.jsxs)(r.Ss,{className:"showcase-text-area",children:[(0,d.jsxs)(r.Ss,{className:"showcase-info",children:[(0,d.jsx)(r.EY,{className:"showcase-title",children:e.title}),(0,d.jsx)(r.EY,{className:"showcase-desc",children:e.desc})]}),(0,d.jsx)(r.Ss,{className:"showcase-tag",children:"\u6210\u54c1\u6848\u4f8b"})]})]})},s)})})]}),(0,d.jsxs)(r.Ss,{className:"category-section mt-20",children:[(0,d.jsx)(r.EY,{className:"section-title",children:"\u70ed\u95e8\u54c1\u7c7b"}),(0,d.jsx)(r.Ss,{className:"category-grid",children:j.map(function(e){return(0,d.jsxs)(r.Ss,{className:"category-item dashed-card",onTap:function(){return x(e.id)},children:[(0,d.jsx)(r.Ss,{className:"star-badge"}),(0,d.jsx)(r.Ss,{className:"category-img-wrapper",children:(0,d.jsx)(r._V,{className:"category-img-real",src:g[e.id]||"/icon/\u56db\u89d2\u661f.png",mode:"aspectFill"})}),(0,d.jsx)(r.EY,{className:"category-name",children:e.name}),(0,d.jsx)(r.EY,{className:"category-desc",children:e.desc}),(0,d.jsxs)(r.EY,{className:"category-price",children:["\xa5",e.price," \u8d77"]})]},e.id)})}),0===j.length&&(0,d.jsxs)(r.Ss,{className:"empty-category dashed-card",children:[(0,d.jsx)(r._V,{className:"empty-icon-img",src:"/icon/\u641c\u7d22.png",mode:"aspectFit"}),(0,d.jsx)(r.EY,{className:"empty-text",children:"\u672a\u627e\u5230\u5339\u914d\u7684\u54c1\u7c7b"}),(0,d.jsx)(r.EY,{className:"empty-sub",children:"\u5c1d\u8bd5\u641c\u7d22\u201c\u7b14\u8bb0\u672c\u201d\u3001\u201c\u676f\u57ab\u201d\u7b49"})]})]}),(0,d.jsx)(r.Ss,{className:"safe-bottom-placeholder"})]})})}var x={};Page((0,c.createPageConfig)(p,"pages/index/index",{root:{cn:[]}},x||{}))}},function(e){var s=function(s){return e(e.s=s)};e.O(0,[907,96,76],function(){return s(7265)});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
@@ -1 +1 @@
.orders-page{min-height:100vh;padding:0 24rpx 24rpx}.page-header{margin:20rpx 0 0;padding:50rpx 32rpx 24rpx}.status-tabs{margin:20rpx 0}.tabs-scroll{white-space:nowrap}.tab-item{display:inline-block;padding:20rpx 32rpx;position:relative}.tab-label{color:#b08d8d;font-size:28rpx;font-weight:500}.tab-item.active .tab-label{color:#ff9a9e;font-weight:700}.tab-underline{background:#ff9a9e;border-radius:4rpx;bottom:8rpx;height:4rpx;left:50%;position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:40rpx}.orders-list{padding:0}.order-card{margin-bottom:24rpx;padding:28rpx}.order-header{-ms-flex-pack:justify;-ms-flex-align:center;-webkit-align-items:center;align-items:center;border-bottom:2rpx dashed #fce4ec;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:20rpx;padding-bottom:20rpx}.order-date{color:#b08d8d;font-size:24rpx}.order-body{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:20rpx}.order-icon-wrapper{-ms-flex-align:center;-ms-flex-pack:center;-ms-flex-negative:0;-webkit-align-items:center;align-items:center;background:-webkit-linear-gradient(315deg,#fff0f5,#e3f2fd);background:linear-gradient(135deg,#fff0f5,#e3f2fd);border-radius:16rpx;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;flex-shrink:0;height:120rpx;-webkit-justify-content:center;justify-content:center;margin-right:20rpx;width:120rpx}.order-icon{font-size:56rpx}.order-icon-img{height:56rpx;width:56rpx}.order-info{-webkit-flex:1;-ms-flex:1;flex:1}.order-product{color:#5c3a3a;display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.order-sku{color:#b08d8d;display:block;font-size:24rpx;margin-bottom:8rpx}.order-meta{color:#ccc;display:block;font-size:22rpx}.order-footer{border-top:2rpx dashed #fce4ec;padding-top:16rpx}.order-price-bar{-ms-flex-align:baseline;-ms-flex-pack:end;-webkit-align-items:baseline;align-items:baseline;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;justify-content:flex-end;margin-bottom:20rpx}.price-label{color:#b08d8d;font-size:24rpx;margin-right:8rpx}.price-value{color:#ff6b81;font-size:36rpx;font-weight:800}.order-actions{-ms-flex-pack:end;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;-webkit-justify-content:flex-end;justify-content:flex-end}.order-btn{border-radius:40rpx!important;font-size:26rpx!important;padding:14rpx 28rpx!important}.empty-state{-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:120rpx 40rpx}.empty-icon-img{height:56rpx;margin-bottom:24rpx;width:56rpx}.empty-text{color:var(--text-primary);font-size:36rpx;font-weight:700;margin-bottom:12rpx}.empty-sub{color:var(--text-secondary);font-size:28rpx;margin-bottom:40rpx}
.orders-page{min-height:100vh;padding:0 24rpx 24rpx}.page-header{margin:20rpx 0 0;padding:calc(env(safe-area-inset-top, 0rpx) + 44rpx) 32rpx 24rpx!important}.status-tabs{margin:20rpx 0}.tabs-scroll{white-space:nowrap}.tab-item{display:inline-block;padding:20rpx 32rpx;position:relative}.tab-label{color:#b08d8d;font-size:28rpx;font-weight:500}.tab-item.active .tab-label{color:#ff9a9e;font-weight:700}.tab-underline{background:#ff9a9e;border-radius:4rpx;bottom:8rpx;height:4rpx;left:50%;position:absolute;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:40rpx}.orders-list{padding:0}.order-card{margin-bottom:24rpx;padding:28rpx}.order-header{-ms-flex-pack:justify;-ms-flex-align:center;-webkit-align-items:center;align-items:center;border-bottom:2rpx dashed #fce4ec;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;justify-content:space-between;margin-bottom:20rpx;padding-bottom:20rpx}.order-date{color:#b08d8d;font-size:24rpx}.order-body{-ms-flex-align:start;-webkit-align-items:flex-start;align-items:flex-start;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:20rpx}.order-icon-wrapper{-ms-flex-align:center;-ms-flex-pack:center;-ms-flex-negative:0;-webkit-align-items:center;align-items:center;background:-webkit-linear-gradient(315deg,#fff0f5,#e3f2fd);background:linear-gradient(135deg,#fff0f5,#e3f2fd);border-radius:16rpx;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-shrink:0;flex-shrink:0;height:120rpx;-webkit-justify-content:center;justify-content:center;margin-right:20rpx;width:120rpx}.order-icon{font-size:56rpx}.order-icon-img{height:56rpx;width:56rpx}.order-info{-webkit-flex:1;-ms-flex:1;flex:1}.order-product{color:#5c3a3a;display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.order-sku{color:#b08d8d;display:block;font-size:24rpx;margin-bottom:8rpx}.order-meta{color:#ccc;display:block;font-size:22rpx}.order-footer{border-top:2rpx dashed #fce4ec;padding-top:16rpx}.order-price-bar{-ms-flex-align:baseline;-ms-flex-pack:end;-webkit-align-items:baseline;align-items:baseline;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:flex-end;justify-content:flex-end;margin-bottom:20rpx}.price-label{color:#b08d8d;font-size:24rpx;margin-right:8rpx}.price-value{color:#ff6b81;font-size:36rpx;font-weight:800}.order-actions{-ms-flex-pack:end;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:16rpx;-webkit-justify-content:flex-end;justify-content:flex-end}.order-btn{border-radius:40rpx!important;font-size:26rpx!important;padding:14rpx 28rpx!important}.empty-state{-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:120rpx 40rpx}.empty-icon-img{height:56rpx;margin-bottom:24rpx;width:56rpx}.empty-text{color:var(--text-primary);font-size:36rpx;font-weight:700;margin-bottom:12rpx}.empty-sub{color:var(--text-secondary);font-size:28rpx;margin-bottom:40rpx}
+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{min-height:100vh;padding:0 24rpx 24rpx}.page-header{margin:20rpx 0 0;padding:50rpx 32rpx 24rpx}.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:50rpx 40rpx;text-align:center}.service-hero-icon{height:56rpx;margin-bottom:20rpx;width:56rpx}.service-title{color:var(--text-primary);display:block;font-size:36rpx;font-weight:700;margin-bottom:12rpx}.service-desc{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:32rpx}.service-btn{border-radius:50rpx!important;font-size:30rpx!important;padding:24rpx 56rpx!important}.contact-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;gap:20rpx}.contact-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:28rpx 32rpx}.contact-icon{font-size:48rpx;margin-right:24rpx}.contact-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:32rpx;margin-right:24rpx;width:32rpx}.contact-info{-webkit-flex:1;-ms-flex:1;flex:1}.contact-label{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:4rpx}.contact-value{color:var(--text-primary);font-size:30rpx;font-weight:600}.faq-card{margin-bottom:20rpx;padding:28rpx 32rpx}.faq-q{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;margin-bottom:12rpx}.faq-a{color:var(--text-secondary);font-size:26rpx;line-height:1.6}.enterprise-service{padding:32rpx}.enterprise-icon{height:28rpx;margin-right:12rpx;width:28rpx}.enterprise-title{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.enterprise-desc{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:24rpx}
.service-page{min-height:100vh;padding:0 24rpx 24rpx}.page-header{margin:20rpx 0 0;padding:calc(env(safe-area-inset-top, 0rpx) + 44rpx) 32rpx 24rpx!important}.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:50rpx 40rpx;text-align:center}.service-hero-icon{height:56rpx;margin-bottom:20rpx;width:56rpx}.service-title{color:var(--text-primary);display:block;font-size:36rpx;font-weight:700;margin-bottom:12rpx}.service-desc{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:32rpx}.service-btn{border-radius:50rpx!important;font-size:30rpx!important;padding:24rpx 56rpx!important}.contact-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;gap:20rpx}.contact-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:28rpx 32rpx}.contact-icon{font-size:48rpx;margin-right:24rpx}.contact-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:32rpx;margin-right:24rpx;width:32rpx}.contact-info{-webkit-flex:1;-ms-flex:1;flex:1}.contact-label{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:4rpx}.contact-value{color:var(--text-primary);font-size:30rpx;font-weight:600}.faq-card{margin-bottom:20rpx;padding:28rpx 32rpx}.faq-q{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;margin-bottom:12rpx}.faq-a{color:var(--text-secondary);font-size:26rpx;line-height:1.6}.enterprise-service{padding:32rpx}.enterprise-icon{height:28rpx;margin-right:12rpx;width:28rpx}.enterprise-title{color:var(--text-primary);display:block;font-size:30rpx;font-weight:700;margin-bottom:8rpx}.enterprise-desc{color:var(--text-secondary);display:block;font-size:24rpx;margin-bottom:24rpx}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
.settings-page{padding:0 24rpx 24rpx}.settings-user{padding:24rpx}.settings-avatar{border:3rpx dashed var(--line-star);border-radius:50%;height:100rpx;width:100rpx}.settings-avatar-placeholder{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;background:var(--bg-input);border:3rpx dashed var(--line-star);border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:100rpx;-webkit-justify-content:center;justify-content:center;padding:30rpx;width:100rpx}.settings-name{color:var(--text-primary);display:block;font-size:32rpx;font-weight:700;margin-bottom:8rpx}.settings-id{color:var(--text-secondary);font-size:24rpx}.arrow{color:var(--text-secondary);font-size:36rpx}.settings-list{padding:0 24rpx}.settings-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:28rpx 0}.settings-item.bordered{border-bottom:1rpx dashed rgba(0,0,0,.06)}.settings-item-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:32rpx;margin-right:16rpx;width:32rpx}.settings-item-label{color:var(--text-primary);-webkit-flex:1;-ms-flex:1;flex:1;font-size:30rpx}.logout-btn{border:3rpx dashed #ff6b6b;color:#ff6b6b;padding:24rpx 0;text-align:center}
.settings-page{padding:0 24rpx 24rpx}.settings-area{padding-top:calc(env(safe-area-inset-top, 0rpx))}.settings-user{padding:24rpx}.settings-avatar{border:3rpx dashed var(--line-star);border-radius:50%;height:100rpx;width:100rpx}.settings-avatar-placeholder{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;background:var(--bg-input);border:3rpx dashed var(--line-star);border-radius:50%;-webkit-box-sizing:border-box;box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;height:100rpx;-webkit-justify-content:center;justify-content:center;padding:30rpx;width:100rpx}.settings-name{color:var(--text-primary);display:block;font-size:32rpx;font-weight:700;margin-bottom:8rpx}.settings-id{color:var(--text-secondary);font-size:24rpx}.arrow{color:var(--text-secondary);font-size:36rpx}.settings-list{padding:0 24rpx}.settings-item{-ms-flex-align:center;-webkit-align-items:center;align-items:center;display:-webkit-flex;display:-ms-flexbox;display:flex;padding:28rpx 0}.settings-item.bordered{border-bottom:1rpx dashed rgba(0,0,0,.06)}.settings-item-icon-img{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0;height:32rpx;margin-right:16rpx;width:32rpx}.settings-item-label{color:var(--text-primary);-webkit-flex:1;-ms-flex:1;flex:1;font-size:30rpx}.logout-btn{border:3rpx dashed #ff6b6b;color:#ff6b6b;padding:24rpx 0;text-align:center}.settings-section{padding:32rpx}.section-label{color:var(--text-primary);display:block;font-size:30rpx;font-weight:600;margin-bottom:24rpx}.theme-options{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;gap:16rpx}.theme-option{-ms-flex-align:center;-ms-flex-pack:center;-webkit-align-items:center;align-items:center;background:var(--bg-input);border:3rpx dashed transparent;border-radius:20rpx;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:1;-ms-flex:1;flex:1;gap:12rpx;-webkit-justify-content:center;justify-content:center;min-width:160rpx;padding:24rpx 16rpx;-webkit-transition:all .2s ease;transition:all .2s ease}.theme-option.active{background:rgba(255,154,158,.08);border-color:var(--accent-pink)}.theme-option-text{color:var(--text-primary);font-size:28rpx;font-weight:500}.theme-check{background:var(--accent-pink);border-radius:50%;height:16rpx;width:16rpx}.theme-hint{color:var(--text-secondary);display:block;font-size:24rpx;margin-top:20rpx;text-align:center}
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"navigationStyle":"custom","usingComponents":{"comp":"../../../comp"}}
+2
View File
@@ -0,0 +1,2 @@
<import src="../../../base.wxml"/>
<template is="taro_tmpl" data="{{root:root}}" />
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
+33
View File
@@ -450,6 +450,39 @@ emoji → icon 图片彻底收尾:productConfig 增加 `iconImg`、store 写
### Batch 10Completed
夜间模式字体 + 地址弹窗统一 + 空状态主题适配。
### Batch 122026-08-04)— 主题跟随 + 贴纸编辑 + 安全区适配 ✅
**背景**:真机测试发现 4 个顽固问题:主题跟随系统不切换、贴纸编辑页画板空白、DIY 画板与确认页截断不一致、顶部按钮与胶囊按钮重叠。
| 序号 | 任务 | 状态 |
|------|------|------|
| 1 | **主题跟随系统**`app.config.ts``darkmode: true``ThemeContext` 增加 `useDidShow` + `wx.onAppShow` 双兜底,每次回到前台重新检测系统主题 | ✅ 完成 |
| 2 | **贴纸编辑画板空白**Canvas 2D 初始化改为 `Taro.nextTick()` + 300ms 自动重试;增加 `Math.round(cw * dpr)` 防小数尺寸;`img.src` 增加空值保护 | ✅ 完成 |
| 3 | **画板截断统一**`.canvas-area` / `.preview-canvas` 统一补充 `max-width: 100%` + `box-sizing: border-box` | ✅ 完成 |
| 4 | **顶部安全区适配**`env(safe-area-inset-top, 20px)` 兜底 + 48px 按钮高度;各页面独立 `.page-header` 强制 `!important` 对齐全局 | ✅ 完成 |
**新增踩坑记录**
| 坑 | 现象 | 解法 |
|---|---|---|
| `wx.onThemeChange` 不触发 | 真机上切换系统主题小程序无反应 | 微信要求 `app.json` 必须有 `"darkmode": true`,否则监听永不被触发 |
| `createSelectorQuery` 查不到 Canvas 节点 | 真机偶尔 `res[0]` 为 undefined | 用 `Taro.nextTick()` 替代 `setTimeout`;查不到时 300ms 后自动重试 |
| `env(safe-area-inset-top)` 取不到 | 部分安卓机型返回 0,导航栏贴顶 | fallback 从 `0px` 提升到 `20px`CSS `!important` 强制覆盖各页面旧值 |
---
### Batch 13(待办)— 遗留问题跟踪
以下 4 项问题在 Batch 12 中尝试修复后,真机验证仍未完全解决,需后续继续跟进:
| 序号 | 问题 | 当前状态 | 根因分析 |
|------|------|----------|----------|
| 1 | **地址在设计中添加后无法及时同步** | ❌ 未修复 | checkout 页从 store 读取地址时,design 数据写入和读取可能不在同一页面生命周期触发 |
| 2 | **贴纸编辑页无法运行** | ❌ 未修复 | Canvas 2D `createImage` 在某些基础库版本返回 `undefined`;线稿 AI 接口未接入 |
| 3 | **个人主页登录后页面失灵** | ❌ 未修复 | Profile 页 `useDidShow``LoginGuard` 状态竞争,导致登录后菜单点击事件丢失 |
| 4 | **跟随系统的主题切换按钮无法及时切换** | ⚠️ 部分修复 | `darkmode: true` + `useDidShow` 已加,但真机 `wx.onThemeChange` 在部分华为/小米机型仍不触发,需进一步排查 |
---
### Batch 11Completed
商品浏览页(Shop Page)沉浸式体验:新增「商品」tab(5 tab 架构)、单页双容器浏览页、滚动驱动视差动画、产品主题色联动、电商级长文案升级。
+4 -1
View File
@@ -2,8 +2,10 @@ export default defineAppConfig({
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',
@@ -19,7 +21,8 @@ export default defineAppConfig({
window: {
backgroundTextStyle: 'light',
navigationStyle: 'custom',
backgroundColor: '#ffffff'
backgroundColor: '#ffffff',
darkmode: true
},
tabBar: {
custom: true,
+17
View File
@@ -7,11 +7,15 @@
.theme-light {
min-height: 100vh;
background-color: #ffffff;
padding-top: env(safe-area-inset-top, 0px);
box-sizing: border-box;
}
.theme-dark {
min-height: 100vh;
background-color: #1e1e2f;
padding-top: env(safe-area-inset-top, 0px);
box-sizing: border-box;
}
/* 变量定义 */
@@ -210,6 +214,19 @@ page {
height: 160px;
}
/* === 自定义导航栏安全区适配 === */
/* 所有页面的顶部虚线卡头部下移,避免与微信胶囊按钮重叠 */
.page-header {
padding-top: calc(env(safe-area-inset-top, 20px) + 48px) !important;
}
/* 特定无 header 但有返回箭头的页面兜底 */
.back-btn,
.edit-back {
display: inline-block;
padding-top: env(safe-area-inset-top, 20px) !important;
}
/* ==========================================================
底部弹窗遮罩(地址选择器等共用)
========================================================== */
+74 -10
View File
@@ -1,15 +1,17 @@
import { createContext, useContext, useState, useCallback, useEffect } from 'react'
import Taro from '@tarojs/taro'
import { getTheme, setTheme as saveTheme, type ThemeMode } from '../utils/store'
import Taro, { useDidShow } from '@tarojs/taro'
import { getTheme, setTheme as saveTheme, resolveTheme, type ThemeMode } from '../utils/store'
interface ThemeContextValue {
theme: ThemeMode
resolvedTheme: 'light' | 'dark'
toggleTheme: () => void
setTheme: (t: ThemeMode) => void
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
theme: 'auto',
resolvedTheme: 'light',
toggleTheme: () => {},
setTheme: () => {}
})
@@ -18,24 +20,86 @@ export { ThemeContext }
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, set] = useState<ThemeMode>(getTheme())
const [resolvedTheme, setResolved] = useState<'light' | 'dark'>(resolveTheme(getTheme()))
const updateResolved = useCallback((mode: ThemeMode) => {
setResolved(resolveTheme(mode))
}, [])
const setTheme = useCallback((t: ThemeMode) => {
saveTheme(t)
set(t)
}, [])
updateResolved(t)
}, [updateResolved])
const toggleTheme = useCallback(() => {
const next = theme === 'light' ? 'dark' : 'light'
const next = resolvedTheme === 'light' ? 'dark' : 'light'
setTheme(next)
}, [theme, setTheme])
}, [resolvedTheme, setTheme])
// eventCenter 监听已移除:ThemeContext 本身就在 App.tsx 全局挂载,
// 所有页面共用一个 state,无需再靠事件广播。
// 旧代码曾在此处监听 'theme:change' 并直接 set(参数),但 trigger 未传参数导致 theme 变成 undefined。
// 页面每次显示时(从小程序后台切回前台)重新检测系统主题
useDidShow(() => {
if (theme === 'auto') {
setResolved(resolveTheme('auto'))
}
})
// auto模式下监听系统主题变化
useEffect(() => {
// 兼容:Taro.onThemeChange 在部分版本不稳定,直接用 wx API 兜底
if (theme === 'auto') {
// 先同步一次当前系统主题
setResolved(resolveTheme('auto'))
const listener = (res: any) => {
setResolved(res.theme === 'dark' ? 'dark' : 'light')
}
// 每次小程序回到前台时重新检测系统主题(解决用户切出小程序去改系统主题后回来不刷新的问题)
const appShowListener = () => {
setResolved(resolveTheme('auto'))
}
try {
// @ts-ignore
if (typeof wx !== 'undefined' && wx.onThemeChange) {
// @ts-ignore
wx.onThemeChange(listener)
}
} catch {}
try {
// @ts-ignore
if (typeof wx !== 'undefined' && wx.onAppShow) {
// @ts-ignore
wx.onAppShow(appShowListener)
}
} catch {}
// Taro 兜底
try {
// @ts-ignore
if (Taro.onThemeChange) {
// @ts-ignore
Taro.onThemeChange(listener)
}
} catch {}
return () => {
try {
// @ts-ignore
if (wx.offThemeChange) wx.offThemeChange(listener)
} catch {}
try {
// @ts-ignore
if (wx.offAppShow) wx.offAppShow(appShowListener)
} catch {}
try {
// @ts-ignore
if (Taro.offThemeChange) Taro.offThemeChange(listener)
} catch {}
}
} else {
updateResolved(theme)
}
}, [theme, updateResolved])
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
<ThemeContext.Provider value={{ theme, resolvedTheme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
)
+69
View File
@@ -0,0 +1,69 @@
import { useState, useEffect } from 'react'
import Taro from '@tarojs/taro'
export interface SafeAreaInfo {
statusBarHeight: number
menuButtonTop: number
menuButtonHeight: number
capsuleRight: number
safeInsetTop: number
safeInsetBottom: number
/** 建议给 .page-header 的 paddingTop 像素值 */
headerPaddingTop: number
/** 建议给 .back-btn 的 paddingTop 像素值 */
backBtnPaddingTop: number
}
let cached: SafeAreaInfo | null = null
export function useSafeArea(): SafeAreaInfo {
const [info, setInfo] = useState<SafeAreaInfo>(cached ?? {
statusBarHeight: 20,
menuButtonTop: 26,
menuButtonHeight: 32,
capsuleRight: 368,
safeInsetTop: 20,
safeInsetBottom: 0,
headerPaddingTop: 66,
backBtnPaddingTop: 20
})
useEffect(() => {
if (cached) {
setInfo(cached)
return
}
try {
const sys = Taro.getSystemInfoSync()
const mb = Taro.getMenuButtonBoundingClientRect()
const statusBarHeight = sys.statusBarHeight || 20
const menuButtonTop = mb.top ?? statusBarHeight + 4
const menuButtonHeight = mb.height ?? 32
const capsuleRight = mb.right ?? (sys.windowWidth - 7)
const safeInsetTop = sys.safeArea?.top ?? statusBarHeight
const safeInsetBottom = sys.safeAreaInsetBottom ?? (sys.screenHeight - (sys.safeArea?.bottom ?? sys.screenHeight))
// header 需要排在胶囊按钮下面,稍微留点空隙
const headerPaddingTop = Math.max(menuButtonTop + menuButtonHeight + 6, statusBarHeight + 44)
// back-btn 需要和状态栏底部对齐再加一点
const backBtnPaddingTop = Math.max(statusBarHeight, menuButtonTop - 4)
const data: SafeAreaInfo = {
statusBarHeight,
menuButtonTop,
menuButtonHeight,
capsuleRight,
safeInsetTop,
safeInsetBottom,
headerPaddingTop,
backBtnPaddingTop
}
cached = data
setInfo(data)
} catch {
// fallback 不变
}
}, [])
return info
}
+2 -4
View File
@@ -4,11 +4,10 @@ import { useState, useEffect } from 'react'
import './index.scss'
import { getAddressList, addAddress, updateAddress, deleteAddress } from '../../utils/store'
import type { AddressItem } from '../../types'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function AddressPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [list, setList] = useState<AddressItem[]>([])
const [editing, setEditing] = useState<AddressItem | null>(null)
const [showForm, setShowForm] = useState(false)
@@ -89,9 +88,8 @@ export default function AddressPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='address-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+2 -4
View File
@@ -1,10 +1,9 @@
import { View, Text } from '@tarojs/components'
import './index.scss'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function AgreementPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const SECTIONS = [
{
@@ -30,9 +29,8 @@ export default function AgreementPage() {
]
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='agreement-page'>
<ThemeToggle />
<View className='agreement-content'>
<Text className='agreement-title'></Text>
{SECTIONS.map((sec, idx) => (
+4 -2
View File
@@ -20,14 +20,16 @@
overflow: hidden;
background: rgba(0,0,0,0.03);
border: 2px dashed var(--line-star);
max-width: 100%;
box-sizing: border-box;
}
.checkout-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
/* 商品信息 */
+3 -5
View File
@@ -3,11 +3,10 @@ import Taro from '@tarojs/taro'
import { useState, useEffect } from 'react'
import './index.scss'
import { getDesignList, designToOrder, getDefaultAddress, getAddressList, type AddressItem } from '../../utils/store'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function CheckoutPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [design, setDesign] = useState<any>(null)
const [address, setAddress] = useState<AddressItem | null>(null)
const [showAddrPicker, setShowAddrPicker] = useState(false)
@@ -55,7 +54,7 @@ export default function CheckoutPage() {
if (!design) {
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='checkout-page'>
<View className='page-header dashed-card mt-20'>
<Text className='page-title'></Text>
@@ -75,9 +74,8 @@ export default function CheckoutPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='checkout-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+61
View File
@@ -1,10 +1,21 @@
.design-page {
padding: 0 24px 24px;
min-height: 100vh;
}
.manage-btn {
font-size: 28px;
color: var(--accent-blue);
font-weight: 500;
}
/* 状态 Tab */
.status-tabs {
margin: 20px 0;
background: var(--bg-card);
border: var(--line-card);
border-radius: 16px;
padding: 8px 0;
}
.tabs-scroll {
@@ -39,6 +50,50 @@
border-radius: 2px;
}
/* 批量管理工具栏 */
.batch-bar {
padding: 20px 24px;
margin-bottom: 16px;
display: flex;
align-items: center;
}
.batch-select-all {
display: flex;
align-items: center;
gap: 16px;
}
.batch-checkbox {
width: 36px;
height: 36px;
border-radius: 50%;
border: 3px dashed var(--text-secondary);
flex-shrink: 0;
transition: all 0.2s;
}
.batch-checkbox.checked {
border-color: var(--accent-pink);
background: var(--accent-pink);
}
.batch-text {
font-size: 26px;
color: var(--text-primary);
}
.batch-delete {
font-size: 26px;
color: var(--text-secondary);
padding: 10px 24px;
}
.batch-delete.active {
color: #ff4757;
font-weight: 600;
}
/* 设计卡片列表 */
.design-list {
padding-bottom: 24px;
@@ -47,6 +102,12 @@
.design-card {
margin-bottom: 20px;
padding: 28px;
transition: all 0.2s ease;
}
.design-card.selected {
border-color: var(--accent-pink);
background: rgba(255, 154, 158, 0.06);
}
.design-body {
+90 -24
View File
@@ -2,9 +2,8 @@ import { View, Text, ScrollView, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState, useEffect, useCallback } from 'react'
import './index.scss'
import { getDesignList, getUserInfo, type DesignItem } from '../../utils/store'
import { getDesignList, removeDesigns, type DesignItem } from '../../utils/store'
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
import LoginGuard from '../../components/LoginGuard'
@@ -23,15 +22,16 @@ const STATUS_STYLE: Record<string, { label: string; cls: string }> = {
}
export default function DesignListPage() {
const { theme } = useThemeContext()
const { resolvedTheme } = useThemeContext()
const [activeTab, setActiveTab] = useState('all')
const [list, setList] = useState<DesignItem[]>([])
const [managing, setManaging] = useState(false)
const [selected, setSelected] = useState<Set<string>>(new Set())
const load = useCallback(() => {
setList(getDesignList())
}, [])
// 自动读取外部传入的筛选状态
const init = useCallback(() => {
const filter = Taro.getStorageSync('designList:filter')
if (filter && STATUS_TABS.some(t => t.code === filter)) {
@@ -41,7 +41,6 @@ export default function DesignListPage() {
load()
}, [load])
// 页面每次显示时刷新
useEffect(() => {
const page = Taro.getCurrentInstance().page
if (page) {
@@ -70,15 +69,58 @@ export default function DesignListPage() {
})
}
const toggleSelect = (id: string) => {
const next = new Set(selected)
if (next.has(id)) next.delete(id)
else next.add(id)
setSelected(next)
}
const toggleSelectAll = () => {
if (selected.size === filtered.length) {
setSelected(new Set())
} else {
setSelected(new Set(filtered.map(i => i.id)))
}
}
const handleDelete = () => {
if (selected.size === 0) return
Taro.showModal({
title: '确认删除',
content: `确定删除选中的 ${selected.size} 个条目吗?`,
success: (res) => {
if (res.confirm) {
removeDesigns(Array.from(selected))
setSelected(new Set())
setManaging(false)
load()
}
}
})
}
const handleManage = () => {
if (managing) {
setManaging(false)
setSelected(new Set())
} else {
setManaging(true)
}
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<LoginGuard>
<View className='design-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
<Text className='page-title'></Text>
<View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={() => Taro.navigateBack()}></Text>
<Text className='page-title'></Text>
<Text className='manage-btn' onTap={handleManage}>
{managing ? '完成' : '管理'}
</Text>
</View>
</View>
{/* 状态筛选 */}
@@ -97,6 +139,21 @@ export default function DesignListPage() {
</ScrollView>
</View>
{/* 批量管理工具栏 */}
{managing && (
<View className='batch-bar dashed-card'>
<View className='flex-between' style={{ width: '100%' }}>
<View className='batch-select-all' onTap={toggleSelectAll}>
<View className={`batch-checkbox ${selected.size === filtered.length && filtered.length > 0 ? 'checked' : ''}`} />
<Text className='batch-text'> ({selected.size}/{filtered.length})</Text>
</View>
<View className={`batch-delete ${selected.size > 0 ? 'active' : ''}`} onTap={handleDelete}>
<Text></Text>
</View>
</View>
</View>
)}
{/* 列表 */}
<ScrollView className='design-list' scrollY>
{filtered.map(item => {
@@ -104,10 +161,17 @@ export default function DesignListPage() {
const iconSrc = item.productIcon?.startsWith('/icon/')
? item.productIcon
: PRODUCT_ICON_MAP[item.productId] || '/icon/四角星.png'
const isSelected = selected.has(item.id)
return (
<View key={item.id} className='design-card dashed-card'>
<View className='star-badge' />
<View
key={item.id}
className={`design-card dashed-card ${isSelected ? 'selected' : ''}`}
onTap={() => managing ? toggleSelect(item.id) : undefined}
>
<View className='design-body'>
{managing && (
<View className={`batch-checkbox ${isSelected ? 'checked' : ''}`} />
)}
<Image className='design-icon-img' src={iconSrc} mode='aspectFit' />
<View className='design-info'>
<View className='design-title-row'>
@@ -118,18 +182,20 @@ export default function DesignListPage() {
<Text className='design-total'>: ¥{(item.unitPrice * item.count).toFixed(2)}</Text>
</View>
</View>
<View className='design-actions'>
{item.status !== 'ordered' && (
<View className='btn-gradient design-btn' onTap={() => goDesign(item)}>
<Text>{item.status === 'undesigned' ? '开始设计' : '继续设计'}</Text>
</View>
)}
{item.status === 'ordered' && (
<View className='btn-outline design-btn' onTap={() => Taro.switchTab({ url: '/pages/orders/index' })}>
<Text></Text>
</View>
)}
</View>
{!managing && (
<View className='design-actions'>
{item.status !== 'ordered' && (
<View className='btn-gradient design-btn' onTap={() => goDesign(item)}>
<Text>{item.status === 'undesigned' ? '开始设计' : '继续设计'}</Text>
</View>
)}
{item.status === 'ordered' && (
<View className='btn-outline design-btn' onTap={() => Taro.switchTab({ url: '/pages/orders/index' })}>
<Text></Text>
</View>
)}
</View>
)}
</View>
)
})}
+33
View File
@@ -17,6 +17,8 @@
border-radius: 16px;
min-width: 200px;
min-height: 200px;
max-width: 100%;
box-sizing: border-box;
}
.mask-border {
@@ -225,6 +227,8 @@
position: relative;
min-width: 200px;
min-height: 200px;
max-width: 100%;
box-sizing: border-box;
}
.preview-image {
@@ -272,6 +276,35 @@
pointer-events: none;
}
/* 常驻编辑按钮 */
.edit-btn-bar {
display: flex;
justify-content: flex-end;
}
.edit-btn {
padding: 16px 48px;
border-radius: 50px;
font-size: 28px;
font-weight: 600;
text-align: center;
background: var(--bg-input);
color: var(--text-secondary);
border: 2px dashed var(--text-muted);
transition: all 0.2s ease;
}
.edit-btn.active {
background: linear-gradient(135deg, rgba(255, 154, 158, 0.12) 0%, rgba(160, 196, 255, 0.12) 100%);
color: var(--accent-pink);
border-color: var(--accent-pink);
}
.edit-btn.disabled {
opacity: 0.5;
pointer-events: none;
}
/* 按钮 */
.action-btns {
display: flex;
+22 -7
View File
@@ -4,11 +4,10 @@ import { useState, useEffect } from 'react'
import './index.scss'
import { getProductById } from '../../utils/productConfig'
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../utils/store'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function DIYPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [category, setCategory] = useState<any>(null)
const [step, setStep] = useState(1)
const [designId, setDesignId] = useState('')
@@ -202,6 +201,13 @@ export default function DIYPage() {
const handleTouchEnd = () => setIsDragging(false)
const handleEditSticker = () => {
if (!activeStickerId || !designId) return
Taro.navigateTo({
url: `/pages/diy/stickerEdit/index?designId=${designId}&stickerId=${activeStickerId}`
})
}
const handleScale = (id: string, delta: number) => {
const next = stickers.map(s => {
if (s.id !== id) return s
@@ -246,7 +252,7 @@ export default function DIYPage() {
if (!category) {
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='diy-page'>
<Text className='page-title'>...</Text>
</View>
@@ -255,9 +261,8 @@ export default function DIYPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='diy-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
@@ -268,8 +273,8 @@ export default function DIYPage() {
</View>
</View>
{/* 画布区域 */}
<View className='canvas-wrapper dashed-card mt-20'>
{/* 画布区域 — catchtouchmove 阻止事件冒泡导致页面滚动 */}
<View className='canvas-wrapper dashed-card mt-20' catchMove>
<View className='star-badge' />
<View className='canvas-area' style={getMaskStyle()}>
{stickers.map(s => (
@@ -337,6 +342,16 @@ export default function DIYPage() {
</View>
)}
{/* 常驻编辑按钮 */}
<View className='edit-btn-bar mt-20'>
<View
className={`edit-btn ${activeStickerId ? 'active' : 'disabled'}`}
onTap={handleEditSticker}
>
<Text></Text>
</View>
</View>
{/* 底部操作按钮 */}
<View className='action-btns mt-20'>
<View className='btn-gradient' onTap={() => setPreviewMode(true)}>
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationBarTitleText: '编辑贴纸'
})
+184
View File
@@ -0,0 +1,184 @@
.sticker-edit-page {
padding: 0 24px 24px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.edit-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: calc(env(safe-area-inset-top, 0px) + 24px) 0 24px;
}
.edit-back {
font-size: 36px;
color: var(--text-primary);
width: 60px;
}
.edit-title {
font-size: 34px;
font-weight: 700;
color: var(--text-primary);
text-align: center;
flex: 1;
}
.edit-save {
font-size: 28px;
color: var(--accent-pink);
font-weight: 600;
width: 60px;
text-align: right;
}
/* 画布区域 */
.edit-canvas-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px 0;
min-height: 400px;
}
.edit-canvas {
width: 100%;
height: 500px;
background: var(--bg-input);
border-radius: 16px;
}
/* 工具面板 */
.edit-tools {
padding: 32px;
}
.edit-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32px;
}
.edit-row:last-child {
margin-bottom: 0;
}
.edit-label {
font-size: 28px;
font-weight: 500;
color: var(--text-primary);
width: 120px;
flex-shrink: 0;
}
.slider-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
}
.slider-track {
flex: 1;
height: 8px;
border-radius: 4px;
background: var(--bg-input);
position: relative;
}
.slider-fill {
height: 100%;
border-radius: 4px;
background: linear-gradient(90deg, #ff9a9e, #fecfef);
position: absolute;
left: 0;
top: 0;
}
.slider-thumb {
width: 32px;
height: 32px;
border-radius: 50%;
background: #fff;
border: 4px solid var(--accent-pink);
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.slider-value {
font-size: 24px;
color: var(--text-secondary);
min-width: 60px;
text-align: right;
}
/* 快速操作按钮 */
.edit-quick-actions {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.quick-btn {
padding: 16px 28px;
border-radius: 40px;
font-size: 24px;
font-weight: 500;
background: var(--bg-input);
color: var(--text-primary);
border: 2px dashed var(--line-star);
transition: all 0.2s;
}
.quick-btn:active {
opacity: 0.8;
transform: scale(0.97);
}
.quick-btn.active {
background: rgba(255, 154, 158, 0.12);
border-color: var(--accent-pink);
color: var(--accent-pink);
}
/* 底部操作 */
.edit-footer {
padding: 20px 0 calc(20px + env(safe-area-inset-bottom));
display: flex;
gap: 20px;
}
.edit-footer .btn-gradient,
.edit-footer .btn-outline {
flex: 1;
text-align: center;
padding: 24px 0;
font-size: 28px;
}
/* 加载遮罩 */
.edit-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.edit-loading-text {
font-size: 28px;
color: #fff;
margin-top: 20px;
}
+416
View File
@@ -0,0 +1,416 @@
import { View, Text, Canvas, Image } from '@tarojs/components'
import Taro, { createCanvasContext, canvasToTempFilePath } from '@tarojs/taro'
import { useState, useEffect, useRef, useCallback } from 'react'
import './index.scss'
import { getProductById } from '../../../utils/productConfig'
import { getDesignList, setDesignList, updateDesign, type DesignItem, type StickerItem } from '../../../utils/store'
import { useThemeContext } from '../../../context/ThemeContext'
/* ============================================================
使用 Canvas 实现贴纸编辑(亮度/色相/线稿)
============================================================ */
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
/** 拖动手势滑块组件 */
interface SliderProps {
value: number
min: number
max: number
step?: number
onChange: (val: number) => void
showValue?: boolean
format?: (v: number) => string
}
function TouchSlider({ value, min, max, step = 1, onChange, format }: SliderProps) {
const trackRef = useRef<any>(null)
const [dragging, setDragging] = useState(false)
const computedFromClientX = (clientX: number) => {
if (!trackRef.current) return value
const query = Taro.createSelectorQuery().in(trackRef.current)
// 注意:这里需要取 track 元素的位置信息
const rect = trackRef.current.getBoundingClientRect ? trackRef.current.getBoundingClientRect() : { left: 0, width: 300 }
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
let raw = min + (max - min) * ratio
if (step > 0) {
raw = Math.round(raw / step) * step
}
return clamp(raw, min, max)
}
const handleTouchStart = (e: any) => {
setDragging(true)
const x = e.touches?.[0]?.clientX ?? 0
onChange(computedFromClientX(x))
}
const handleTouchMove = (e: any) => {
if (!dragging) return
const x = e.touches?.[0]?.clientX ?? 0
onChange(computedFromClientX(x))
}
const handleTouchEnd = () => {
setDragging(false)
}
const pct = `${((value - min) / (max - min)) * 100}%`
return (
<View className='slider-wrap'>
<View
className='slider-track'
ref={trackRef}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<View className='slider-fill' style={{ width: pct }} />
<View className={`slider-thumb ${dragging ? 'dragging' : ''}`} style={{ left: pct }} />
</View>
<Text className='slider-value'>{format ? format(value) : value}</Text>
</View>
)
}
export default function StickerEditPage() {
const { resolvedTheme } = useThemeContext()
const [designId, setDesignId] = useState('')
const [stickerId, setStickerId] = useState('')
const [sticker, setSticker] = useState<StickerItem | null>(null)
// 编辑参数
const [brightness, setBrightness] = useState(0)
const [hue, setHue] = useState(0)
const [contrast, setContrast] = useState(0)
const [loading, setLoading] = useState(false)
const [canvasReady, setCanvasReady] = useState(false)
const canvasRef = useRef<any>(null)
const ctxRef = useRef<any>(null)
const canvasNodeRef = useRef<any>(null)
const canvasSizeRef = useRef<{width:number, height:number}>({width:0,height:0})
// 使用 nextTick + 重试初始化 Canvas(小程序 2D)
const initCanvas = useCallback((st: StickerItem) => {
if (canvasReady) return
Taro.nextTick(() => {
const query = Taro.createSelectorQuery()
query.select('#editCanvas')
.fields({ node: true, size: true })
.exec((res: any) => {
const canvas = res?.[0]?.node
if (!canvas) {
setTimeout(() => initCanvas(st), 300)
return
}
const ctx = canvas.getContext('2d')
let cw = res[0]?.width || 300
let ch = res[0]?.height || 400
if (cw <= 0) cw = 300
if (ch <= 0) ch = 400
const dpr = Taro.getSystemInfoSync().pixelRatio || 1
canvas.width = Math.round(cw * dpr)
canvas.height = Math.round(ch * dpr)
ctx.scale(dpr, dpr)
canvasNodeRef.current = canvas
ctxRef.current = ctx
canvasSizeRef.current = { width: cw, height: ch }
setCanvasReady(true)
drawSticker(ctx, cw, ch, st)
})
})
}, [canvasReady])
useEffect(() => {
const params = Taro.getCurrentInstance().router?.params
const dId = params?.designId as string
const sId = params?.stickerId as string
setDesignId(dId)
setStickerId(sId)
const list = getDesignList()
const design = list.find(d => d.id === dId)
const st = design?.designData?.stickers?.find(s => s.id === sId)
if (st) {
setSticker(st)
setBrightness(st.edits?.brightness || 0)
setHue(st.edits?.hue || 0)
setContrast(st.edits?.contrast || 0)
initCanvas(st)
}
}, [initCanvas])
/** canvas 上绘制带滤镜的贴纸 */
const drawSticker = (ctx: any, cw: number, ch: number, st: StickerItem, opts?: { brightness: number; hue: number; contrast: number }) => {
if (!ctx || !canvasNodeRef.current) return
const canvas = canvasNodeRef.current
// 防护:cw/ch 为 0 时容易绘制异常
if (!cw || !ch) return
const imgW = st.width || 200
const imgH = st.height || 200
const img = canvas.createImage()
img.onload = () => {
ctx.clearRect(0, 0, cw, ch)
ctx.save()
// 居中缩放显示(限制最大为画板 90%)
const scale = Math.min(cw / imgW, ch / imgH, 4) * 0.9
const w = imgW * scale
const h = imgH * scale
const x = (cw - w) / 2
const y = (ch - h) / 2
// 应用滤镜(小程序 Canvas 2D 支持 filter 属性 2.16.0+
const b = opts?.brightness ?? brightness
const hVal = opts?.hue ?? hue
const c = opts?.contrast ?? contrast
if (b !== 0 || hVal !== 0 || c !== 0) {
ctx.filter = `brightness(${100 + b}%) hue-rotate(${hVal}deg) contrast(${100 + c}%)`
} else {
ctx.filter = 'none'
}
ctx.drawImage(img, x, y, w, h)
ctx.restore()
}
img.onerror = (err: any) => {
console.error('Canvas load image error:', err, st.src)
Taro.showToast({ title: '图片加载失败', icon: 'none' })
}
if (st.src) {
img.src = st.src
}
}
/** 重新绘制(参数变化时) */
const redraw = () => {
if (!sticker || !ctxRef.current || !canvasNodeRef.current) return
const { width, height } = canvasSizeRef.current
drawSticker(ctxRef.current, width, height, sticker, { brightness, hue, contrast })
}
useEffect(() => {
redraw()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [brightness, hue, contrast])
const handleSave = () => {
if (!sticker || !designId) return
setLoading(true)
// 使用 Canvas 2D 导出
// @ts-ignore
Taro.canvasToTempFilePath({
canvas: canvasNodeRef.current,
quality: 0.92,
success: (res: any) => {
const newSrc = res.tempFilePath
const dList = getDesignList()
const dIdx = dList.findIndex(d => d.id === designId)
if (dIdx === -1) { setLoading(false); return }
const stickers = dList[dIdx].designData?.stickers || []
const sIdx = stickers.findIndex(s => s.id === stickerId)
if (sIdx === -1) { setLoading(false); return }
stickers[sIdx] = {
...stickers[sIdx],
src: newSrc,
edits: { brightness, hue, contrast }
}
updateDesign(designId, { designData: { ...dList[dIdx].designData, stickers } })
Taro.showToast({ title: '保存成功', icon: 'success' })
Taro.navigateBack()
},
fail: () => {
setLoading(false)
Taro.showToast({ title: '保存失败', icon: 'none' })
}
})
}
/** 裁剪:调用微信cropImage */
const handleCrop = () => {
if (!sticker) return
// @ts-ignore
if (!Taro.cropImage) {
Taro.showToast({ title: '当前微信版本不支持裁剪', icon: 'none' })
return
}
// @ts-ignore
Taro.cropImage({
src: sticker.src,
cropScale: '1:1',
success: (res) => {
const newSticker = { ...sticker, src: res.tempFilePath }
setSticker(newSticker)
// 重绘
setTimeout(() => redraw(), 200)
}
})
}
/** 线稿:预留后端AI接口 */
const API_BASE = 'https://your-api-domain.com' // ← 填入你的服务器地址
const handleSketch = () => {
if (!sticker) return
setLoading(true)
Taro.uploadFile({
url: `${API_BASE}/api/sketch`,
filePath: sticker.src,
name: 'image',
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.url) {
const newSticker = { ...sticker, src: data.url, edits: { ...sticker.edits, sketchSrc: data.url } }
setSticker(newSticker)
setTimeout(() => redraw(), 200)
Taro.showToast({ title: '线稿生成成功', icon: 'success' })
} else {
throw new Error('no url')
}
} catch {
// 如果接口不可用,降级为前端灰度+边缘检测
applyFrontendSketch()
}
},
fail: () => {
applyFrontendSketch()
}
})
}
/** 前端线稿降级方案:灰度+反相高对比 */
const applyFrontendSketch = () => {
if (!sticker || !ctxRef.current) { setLoading(false); return }
Taro.showToast({ title: '使用本地线稿', icon: 'none' })
setBrightness(20)
setContrast(80)
setHue(0)
setLoading(false)
}
const bPct = `${((brightness + 100) / 200) * 100}%`
const hPct = `${((hue + 180) / 360) * 100}%`
const cPct = `${((contrast + 100) / 200) * 100}%`
return (
<View className={`theme-${resolvedTheme}`}>
<View className='sticker-edit-page'>
{/* Header */}
<View className='edit-header'>
<Text className='edit-back' onTap={() => Taro.navigateBack()}></Text>
<Text className='edit-title'></Text>
<Text className='edit-save' onTap={handleSave}></Text>
</View>
{/* Canvas 预览 */}
<View className='edit-canvas-wrap'>
<Canvas
id='editCanvas'
type='2d'
className='edit-canvas'
style={{ width: '100%', height: '400px' }}
/>
</View>
{/* 快速操作 */}
<View className='edit-tools dashed-card mt-20'>
<View className='edit-row'>
<Text className='edit-label'></Text>
<View className='edit-quick-actions'>
<View className='quick-btn' onTap={handleCrop}>
<Text></Text>
</View>
<View className='quick-btn' onTap={handleSketch}>
<Text>线稿</Text>
</View>
</View>
</View>
{/* 亮度 */}
<View className='edit-row slider-row'
onTouchMove={(e) => {
const { clientX } = e.changedTouches[0]
// 取得滑轨位置
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
query.select('.slider-track').boundingClientRect((rect: any) => {
if (!rect) return
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
const v = Math.round((-100 + 200 * ratio) / 1) * 1
setBrightness(clamp(v, -100, 100))
}).exec()
}}
>
<Text className='edit-label'></Text>
<View className='slider-wrap'>
<View className='slider-track'>
<View className='slider-fill' style={{ width: bPct }} />
<View className='slider-thumb' style={{ left: bPct }} />
</View>
<Text className='slider-value'>{brightness > 0 ? `+${brightness}` : brightness}</Text>
</View>
</View>
{/* 色相 */}
<View className='edit-row slider-row'
onTouchMove={(e) => {
const { clientX } = e.changedTouches[0]
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
query.select('.slider-track').boundingClientRect((rect: any) => {
if (!rect) return
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
const v = Math.round((-180 + 360 * ratio) / 1) * 1
setHue(clamp(v, -180, 180))
}).exec()
}}
>
<Text className='edit-label'></Text>
<View className='slider-wrap'>
<View className='slider-track'>
<View className='slider-fill' style={{ width: hPct }} />
<View className='slider-thumb' style={{ left: hPct }} />
</View>
<Text className='slider-value'>{hue > 0 ? `+${hue}` : hue}</Text>
</View>
</View>
{/* 对比度 */}
<View className='edit-row slider-row'
onTouchMove={(e) => {
const { clientX } = e.changedTouches[0]
const query = Taro.createSelectorQuery().in(e.currentTarget as any)
query.select('.slider-track').boundingClientRect((rect: any) => {
if (!rect) return
const ratio = clamp((clientX - rect.left) / rect.width, 0, 1)
const v = Math.round((-100 + 200 * ratio) / 1) * 1
setContrast(clamp(v, -100, 100))
}).exec()
}}
>
<Text className='edit-label'></Text>
<View className='slider-wrap'>
<View className='slider-track'>
<View className='slider-fill' style={{ width: cPct }} />
<View className='slider-thumb' style={{ left: cPct }} />
</View>
<Text className='slider-value'>{contrast > 0 ? `+${contrast}` : contrast}</Text>
</View>
</View>
</View>
<View style={{ flex: 1 }} />
{loading && (
<View className='edit-loading'>
<Text className='edit-loading-text'></Text>
</View>
)}
</View>
</View>
)
}
+2 -4
View File
@@ -3,7 +3,6 @@ import Taro from '@tarojs/taro'
import { useState } from 'react'
import './index.scss'
import { CATEGORIES } from '../../utils/productConfig'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
// 成品展示配图(从 img 里取对应实物图)
@@ -26,7 +25,7 @@ const PRODUCT_IMG_MAP: Record<string, string> = {
}
export default function Index() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [searchKey, setSearchKey] = useState('')
const navigateToProduct = (categoryId: string) => {
@@ -44,9 +43,8 @@ export default function Index() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='index-page'>
<ThemeToggle />
{/* 顶部标题栏 */}
<View className='header-bar'>
+3 -5
View File
@@ -2,13 +2,12 @@ import { View, Text, Image, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState, useEffect } from 'react'
import './index.scss'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store'
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
export default function OrderDetailPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const params = Taro.getCurrentInstance().router?.params
const orderId = params?.orderId || ''
@@ -47,7 +46,7 @@ export default function OrderDetailPage() {
if (!order) {
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='order-detail-page'>
<View className='page-header dashed-card mt-20'>
<Text className='page-title'></Text>
@@ -58,9 +57,8 @@ export default function OrderDetailPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='order-detail-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+1 -1
View File
@@ -4,7 +4,7 @@
}
.page-header {
padding: 50px 32px 24px;
padding: calc(env(safe-area-inset-top, 0px) + 44px) 32px 24px !important;
margin: 20px 0 0;
}
+2 -4
View File
@@ -4,7 +4,6 @@ import { useState, useEffect } from 'react'
import './index.scss'
import { getOrderList, type OrderItem } from '../../utils/store'
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
import LoginGuard from '../../components/LoginGuard'
@@ -25,7 +24,7 @@ const STATUS_MAP: Record<string, { label: string; badge: string }> = {
}
export default function OrdersPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [activeTab, setActiveTab] = useState('all')
const [orders, setOrders] = useState<OrderItem[]>([])
@@ -71,10 +70,9 @@ export default function OrdersPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<LoginGuard>
<View className='orders-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+3 -5
View File
@@ -4,14 +4,13 @@ import { useState } from 'react'
import './index.scss'
import { getProductById } from '../../utils/productConfig'
import { addDesign } from '../../utils/store'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
import { getProductIconImg } from '../../utils/productConfig'
const HERO_COLORS = ['#FFE4EC', '#FFF0F5', '#FCE4EC']
export default function ProductPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [showModal, setShowModal] = useState(false)
const [quantity, setQuantity] = useState(1)
@@ -21,7 +20,7 @@ export default function ProductPage() {
if (!product) {
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='product-page'>
<View className='page-header dashed-card mt-20'>
<Text className='page-title'></Text>
@@ -54,9 +53,8 @@ export default function ProductPage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='product-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+2 -4
View File
@@ -3,7 +3,6 @@ import Taro, { useDidShow } from '@tarojs/taro'
import { useState } from 'react'
import './index.scss'
import { getOrderList, getUserInfo, setUserInfo, getDesignList } from '../../utils/store'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
const ICON_MAP: Record<string, string> = {
@@ -33,7 +32,7 @@ const MENU_ITEMS = [
]
export default function ProfilePage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [userInfo, setUserInfoState] = useState({ nickName: '', avatarUrl: '' })
const [showLogin, setShowLogin] = useState(false)
@@ -163,9 +162,8 @@ export default function ProfilePage() {
}
return (
<View className={`theme-${theme}`} style={{ minHeight: '100vh' }}>
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<View className='profile-page'>
<ThemeToggle />
{/* 用户信息头部 */}
<View className='profile-header dashed-card mt-20'>
+1 -1
View File
@@ -4,7 +4,7 @@
}
.page-header {
padding: 50px 32px 24px;
padding: calc(env(safe-area-inset-top, 0px) + 44px) 32px 24px !important;
margin: 20px 0 0;
}
+2 -4
View File
@@ -1,7 +1,6 @@
import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import './index.scss'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
const FAQ_ITEMS = [
@@ -12,12 +11,11 @@ const FAQ_ITEMS = [
]
export default function ServicePage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
return (
<View className={`theme-${theme}`} style={{ minHeight: '100vh' }}>
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<View className='service-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
<Text className='page-title'></Text>
+62
View File
@@ -1,6 +1,9 @@
.settings-page {
padding: 0 24px 24px;
}
.settings-area {
padding-top: calc(env(safe-area-inset-top, 0px));
}
.settings-user {
padding: 24px;
@@ -79,4 +82,63 @@
border: 3px dashed #ff6b6b;
}
/* --- 主题选择 --- */
.settings-section {
padding: 32px;
}
.section-label {
font-size: 30px;
font-weight: 600;
color: var(--text-primary);
display: block;
margin-bottom: 24px;
}
.theme-options {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.theme-option {
flex: 1;
min-width: 160px;
padding: 24px 16px;
border-radius: 20px;
background: var(--bg-input);
border: 3px dashed transparent;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
transition: all 0.2s ease;
}
.theme-option.active {
border-color: var(--accent-pink);
background: rgba(255, 154, 158, 0.08);
}
.theme-option-text {
font-size: 28px;
color: var(--text-primary);
font-weight: 500;
}
.theme-check {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-pink);
}
.theme-hint {
font-size: 24px;
color: var(--text-secondary);
margin-top: 20px;
display: block;
text-align: center;
}
/* 弹窗复用 address 弹窗和 profile 弹窗样式 */
+98 -74
View File
@@ -2,10 +2,15 @@ import { View, Text, Image, Input, Button } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState, useEffect, useCallback } from 'react'
import './index.scss'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
import { getUserInfo, setUserInfo, clearUserInfo, getAddressList, getDefaultAddress, type AddressItem } from '../../utils/store'
const THEME_OPTIONS: { label: string; value: 'light' | 'dark' | 'auto' }[] = [
{ label: '浅色模式', value: 'light' },
{ label: '深色模式', value: 'dark' },
{ label: '跟随系统', value: 'auto' }
]
const MENU = [
{ label: '个人信息', icon: '/icon/个人.png', action: (s: any) => s.setShowEdit(true) },
{ label: '账号管理', icon: '/icon/账号管理.png', action: () => Taro.navigateTo({ url: '/pages/userDatabase/index' }) },
@@ -14,7 +19,7 @@ const MENU = [
]
export default function SettingsPage() {
const { theme } = useThemeContext()
const { theme, setTheme, resolvedTheme } = useThemeContext()
const [user, setUser] = useState<any>({})
const [editName, setEditName] = useState('')
const [editAvatar, setEditAvatar] = useState('')
@@ -56,91 +61,110 @@ export default function SettingsPage() {
}
return (
<View className={`theme-${theme}`} style={{ minHeight: '100vh' }}>
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
<View className='settings-page'>
<ThemeToggle />
<View className='settings-area'>
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
<View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={() => Taro.navigateBack()}></Text>
<Text className='page-title'></Text>
<View style={{ width: '60px' }} />
</View>
</View>
{/* 用户卡片 */}
<View className='settings-user dashed-card mt-20'>
<View className='flex-between' onTap={() => setShowEdit(true)}>
<View className='flex-center'>
{user.avatarUrl ? (
<Image className='settings-avatar' src={user.avatarUrl} mode='aspectFill' />
) : (
<View className='settings-avatar-placeholder'>
<Image className='settings-avatar-icon' src='/icon/个人.png' mode='aspectFit' />
</View>
)}
<View style={{ marginLeft: '20px' }}>
<Text className='settings-name'>{user.nickName || '未登录'}</Text>
<Text className='settings-id'>ID: {user.openid ? user.openid.slice(-8) : '---'}</Text>
</View>
<View className='page-header dashed-card mt-20'>
<View className='flex-between' style={{ width: '100%' }}>
<Text className='back-btn' onTap={() => Taro.navigateBack()}></Text>
<Text className='page-title'></Text>
<View style={{ width: '60px' }} />
</View>
<Text className='arrow'></Text>
</View>
</View>
{/* 菜单列表 */}
<View className='settings-list dashed-card mt-20'>
{MENU.map((item, idx) => (
<View key={idx} className={`settings-item ${idx < MENU.length - 1 ? 'bordered' : ''}`} onTap={() => item.action({ setShowEdit })}>
<Image className='settings-item-icon-img' src={item.icon} mode='aspectFit' />
<Text className='settings-item-label'>{item.label}</Text>
{/* 用户卡片 */}
<View className='settings-user dashed-card mt-20'>
<View className='flex-between' onTap={() => setShowEdit(true)}>
<View className='flex-center'>
{user.avatarUrl ? (
<Image className='settings-avatar' src={user.avatarUrl} mode='aspectFill' />
) : (
<View className='settings-avatar-placeholder'>
<Image className='settings-avatar-icon' src='/icon/个人.png' mode='aspectFit' />
</View>
)}
<View style={{ marginLeft: '20px' }}>
<Text className='settings-name'>{user.nickName || '未登录'}</Text>
<Text className='settings-id'>ID: {user.openid ? user.openid.slice(-8) : '---'}</Text>
</View>
</View>
<Text className='arrow'></Text>
</View>
))}
</View>
{/* 退出登录 */}
{user?.openid && (
<View className='mt-20'>
<View className='btn-outline logout-btn' onTap={handleLogout}>
<Text>退</Text>
</View>
</View>
)}
<View style={{ height: '40px' }} />
{/* 编辑弹窗 */}
{showEdit && (
<View className='modal-overlay'>
<View className='modal-card dashed-card'>
<View className='star-badge' />
<Text className='modal-title'></Text>
<Button className='avatar-btn' openType='chooseAvatar' onChooseAvatar={onChooseAvatar}>
{editAvatar ? (
<Image className='avatar-img' src={editAvatar} mode='aspectFill' />
) : (
<Image className='avatar-placeholder-img' src='/icon/个人.png' mode='aspectFit' />
)}
</Button>
<Input
className='nickname-input'
placeholder='请输入昵称'
value={editName}
onInput={(e: any) => setEditName(e.detail.value)}
/>
<View className='form-actions'>
<View className='btn-outline' onTap={() => setShowEdit(false)}>
<Text></Text>
{/* 主题设置 */}
<View className='settings-section dashed-card mt-20'>
<Text className='section-label'></Text>
<View className='theme-options'>
{THEME_OPTIONS.map(opt => (
<View
key={opt.value}
className={`theme-option ${theme === opt.value ? 'active' : ''}`}
onTap={() => setTheme(opt.value)}
>
<Text className='theme-option-text'>{opt.label}</Text>
{theme === opt.value && <View className='theme-check' />}
</View>
<View className='btn-gradient' onTap={handleSaveInfo}>
<Text></Text>
))}
</View>
{theme === 'auto' && (
<Text className='theme-hint'>{resolvedTheme === 'dark' ? '深色模式' : '浅色模式'}</Text>
)}
</View>
{/* 菜单列表 */}
<View className='settings-list dashed-card mt-20'>
{MENU.map((item, idx) => (
<View key={idx} className={`settings-item ${idx < MENU.length - 1 ? 'bordered' : ''}`} onTap={() => item.action({ setShowEdit })}>
<Image className='settings-item-icon-img' src={item.icon} mode='aspectFit' />
<Text className='settings-item-label'>{item.label}</Text>
<Text className='arrow'></Text>
</View>
))}
</View>
{/* 退出登录 */}
{user?.openid && (
<View className='mt-20'>
<View className='btn-outline logout-btn' onTap={handleLogout}>
<Text>退</Text>
</View>
</View>
)}
<View style={{ height: '40px' }} />
{/* 编辑弹窗 */}
{showEdit && (
<View className='modal-overlay'>
<View className='modal-card dashed-card'>
<Text className='modal-title'></Text>
<Button className='avatar-btn' openType='chooseAvatar' onChooseAvatar={onChooseAvatar}>
{editAvatar ? (
<Image className='avatar-img' src={editAvatar} mode='aspectFill' />
) : (
<Image className='avatar-placeholder-img' src='/icon/个人.png' mode='aspectFit' />
)}
</Button>
<Input
className='nickname-input'
placeholder='请输入昵称'
value={editName}
onInput={(e: any) => setEditName(e.detail.value)}
/>
<View className='form-actions'>
<View className='btn-outline' onTap={() => setShowEdit(false)}>
<Text></Text>
</View>
<View className='btn-gradient' onTap={handleSaveInfo}>
<Text></Text>
</View>
</View>
</View>
</View>
</View>
)}
)}
</View>
</View>
</View>
)
+3
View File
@@ -0,0 +1,3 @@
export default definePageConfig({
navigationStyle: 'custom'
})
+345
View File
@@ -0,0 +1,345 @@
/* ============================================================
Shop Detail Page — 商品详情页
结构:沉浸式详情页(固定Hero + ScrollView内容 + CTA
============================================================ */
.shop-detail-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(28, 22, 18);
}
/* --- Fixed Hero Area --- */
.shop-hero-fixed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 65vh; /* hero takes 65% viewport height */
z-index: 0;
overflow: hidden;
}
.shop-hero-clip {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
will-change: height, transform;
}
.shop-hero-img {
width: 100%;
height: 100%;
object-fit: cover;
will-change: top;
}
/* Bottom gradient mask: pseudo-mask approach (better x-compatibility) */
.shop-hero-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 55%;
pointer-events: none;
}
/* Hero info (title, price) anchored at bottom */
.shop-hero-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
padding: 80rpx 40rpx 24rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.shop-hero-title {
font-size: 40rpx;
font-weight: 700;
line-height: 1.3;
color: #f0e6d8;
}
.shop-hero-subtitle {
font-size: 22rpx;
color: #b0a395;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-hero-price-row {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-top: 6rpx;
}
.shop-hero-price {
font-size: 44rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-hero-original {
font-size: 26rpx;
color: #8a7e72;
text-decoration: line-through;
}
/* --- ScrollView Content Layer --- */
.shop-scroll {
position: relative;
z-index: 3;
}
.shop-content {
min-height: 100vh; /* ensure scrollability */
will-change: transform;
}
/* --- Transition Band (sticky-like top info) --- */
.shop-band {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 4;
padding: 40rpx 40rpx 32rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
transition: opacity 0.15s ease;
will-change: opacity;
}
.shop-band-title {
font-size: 44rpx;
font-weight: 700;
line-height: 1.3;
color: #f0e6d8;
}
.shop-band-subtitle {
font-size: 24rpx;
color: #b0a395;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-band-price-row {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-top: 6rpx;
}
.shop-band-price {
font-size: 48rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-band-original {
font-size: 26rpx;
color: #8a7e72;
text-decoration: line-through;
}
/* --- Detail Body --- */
.shop-detail-body {
padding: 32rpx 40rpx 60rpx;
will-change: opacity, transform;
}
.shop-section {
margin-bottom: 32rpx;
}
.shop-section-title {
font-size: 34rpx;
font-weight: 700;
color: #f0e6d8;
margin: 28rpx 0 20rpx;
display: block;
position: relative;
padding-left: 20rpx;
}
.shop-section-title::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 6rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, #ff9a9e, #fecfef);
}
.shop-section-text {
font-size: 28rpx;
color: #c4b8a8;
line-height: 1.75;
display: block;
}
.shop-divider {
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
margin: 40rpx 0;
}
/* --- Tags --- */
.shop-tag-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin: 20rpx 0;
}
.shop-tag {
padding: 10rpx 28rpx;
border-radius: 40rpx;
font-size: 22rpx;
font-weight: 500;
background: rgba(255, 154, 158, 0.12);
border: 1rpx solid rgba(255, 154, 158, 0.2);
display: inline-flex;
align-items: center;
justify-content: center;
}
.shop-tag-text {
color: #ff9a9e;
}
/* --- Spec Table --- */
.shop-spec-table {
width: 100%;
border-collapse: collapse;
border-radius: 16rpx;
overflow: hidden;
font-size: 26rpx;
}
.shop-spec-row {
display: flex;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(255,255,255,0.06);
}
.shop-spec-row:last-child {
border-bottom: none;
}
.shop-spec-key {
width: 35%;
color: #8a7e72;
font-weight: 500;
font-size: 24rpx;
}
.shop-spec-val {
flex: 1;
color: #e8e0d6;
word-break: break-all;
}
.shop-bottom-spacer {
height: 200rpx;
}
/* --- Back Button --- */
.shop-back-btn {
position: fixed;
top: calc(env(safe-area-inset-top, 20px) + 20rpx);
left: 24rpx;
z-index: 50;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(16rpx) saturate(1.4);
-webkit-backdrop-filter: blur(16rpx) saturate(1.4);
border: 1rpx solid rgba(255,255,255,0.12);
box-shadow: 0 2rpx 16rpx rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s ease, transform 0.15s ease;
}
.shop-back-btn:active {
transform: scale(0.92);
}
.shop-back-arrow {
font-size: 36rpx;
color: rgba(255,255,255,0.85);
line-height: 1;
}
/* --- Bottom CTA Bar --- */
.shop-cta-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 60;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
backdrop-filter: blur(16rpx) saturate(1.2);
-webkit-backdrop-filter: blur(16rpx) saturate(1.2);
transition: background-color 0.4s ease;
}
.shop-cta-price-col {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.shop-cta-price {
font-size: 40rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-cta-original {
font-size: 24rpx;
color: #8a7e72;
text-decoration: line-through;
}
.shop-cta-btn {
flex: 1;
max-width: 260rpx;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%);
color: #5c3a3a;
border-radius: 50rpx;
padding: 24rpx 48rpx;
font-size: 30rpx;
font-weight: 600;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.shop-cta-btn-text {
color: inherit;
font-weight: 600;
}
+279
View File
@@ -0,0 +1,279 @@
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState, useEffect, useCallback, useMemo } from 'react'
import './index.scss'
import { PRODUCTS } from '../../../utils/productConfig'
import { ProductCategory } from '../../../types'
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
const easeOut = (t: number) => 1 - Math.pow(1 - t, 3)
export default function ShopDetailPage() {
const [winHeight, setWinHeight] = useState(667)
const [progress, setProgress] = useState(0)
const routerParams = Taro.getCurrentInstance().router?.params
const productId = routerParams?.id || ''
const product: ProductCategory | undefined = PRODUCTS.find(p => p.id === productId)
useEffect(() => {
try {
const info = Taro.getSystemInfoSync()
setWinHeight(info.windowHeight)
} catch (e) {
// fallback to safe default
}
}, [])
const handleScroll = useCallback((e: any) => {
const st = e.detail?.scrollTop ?? 0
const total = winHeight * 1.2
const p = clamp(st / total, 0, 1)
setProgress(p)
}, [winHeight])
/* Scroll-progress → multi-stage hero & content animations */
const heroStyles = useMemo(() => {
const p = progress
// 1. Image crop insets (vh %)
let topCrop = 0, bottomCrop = 0
if (p <= 0.4) {
const t = easeOut(p / 0.4)
topCrop = lerp(0, 32, t)
bottomCrop = lerp(0, 38, t)
} else if (p <= 0.55) {
const t = (p - 0.4) / 0.15
topCrop = lerp(32, 34, t)
bottomCrop = lerp(38, 41, t)
} else {
topCrop = 34; bottomCrop = 41
}
// 2. Hero translateY
let translateY = 0
if (p <= 0.4) translateY = 0
else if (p <= 0.7) translateY = lerp(0, -75, (p - 0.4) / 0.3)
else translateY = -75
// 3. Hero bottom info opacity (always visible on open, fades after scroll)
let heroInfoOpacity = 1
if (p <= 0.35) heroInfoOpacity = 1
else if (p <= 0.48) heroInfoOpacity = clamp(1 - (p - 0.35) / 0.13, 0, 1)
else heroInfoOpacity = 0
// 4. Transition band opacity
let bandOpacity = 0
if (p <= 0.55) bandOpacity = 0
else if (p <= 0.7) bandOpacity = (p - 0.55) / 0.15
else bandOpacity = 1
// 5. Content reveal
let contentOpacity = 1, contentOffset = 0
if (p <= 0.05) { contentOpacity = 0; contentOffset = 60 }
else if (p <= 0.22) {
const t = (p - 0.05) / 0.17
contentOpacity = easeOut(t)
contentOffset = lerp(60, 0, easeOut(t))
}
// 6. Back button (fade out as hero scrolls)
let backBtnOpacity = 1
if (p <= 0.15) backBtnOpacity = clamp(1 - p / 0.15, 0, 1)
return {
topCrop,
bottomCrop,
translateY,
heroInfoOpacity,
bandOpacity,
contentOpacity,
contentOffset,
backBtnOpacity,
visibleHeight: 100 - topCrop - bottomCrop
}
}, [progress])
const tone = useMemo(() => product?.tone || [28, 22, 18], [product])
if (!product) {
return (
<View className="shop-detail-page">
<Text style={{ color: '#fff', padding: '40rpx' }}></Text>
</View>
)
}
return (
<View className="shop-detail-page">
{/* Fixed Hero */}
<View className="shop-hero-fixed">
<View
className="shop-hero-clip"
style={{
height: `${winHeight * heroStyles.visibleHeight / 100}px`,
overflow: 'hidden',
transform: `translateY(${winHeight * heroStyles.translateY / 100}px)`
}}
>
<Image
className="shop-hero-img"
src={product.images?.[0] || product.iconImg || ''}
mode="aspectFill"
style={{
width: '100%',
height: `${winHeight}px`,
position: 'absolute',
top: `${-winHeight * heroStyles.topCrop / 100}px`,
left: 0
}}
/>
</View>
{/* Bottom gradient overlay driven by product tone */}
<View
className="shop-hero-overlay"
style={{
background: `linear-gradient(to bottom, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0) 0%, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.55) 40%, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.95) 100%)`
}}
/>
<View
className="shop-hero-info"
style={{ opacity: heroStyles.heroInfoOpacity }}
>
<Text className="shop-hero-title">{product.name}</Text>
<Text className="shop-hero-subtitle">{product.subtitle || product.desc}</Text>
<View className="shop-hero-price-row">
<Text className="shop-hero-price">¥{product.price}</Text>
{product.originalPrice && product.originalPrice > product.price && (
<Text className="shop-hero-original">¥{product.originalPrice}</Text>
)}
</View>
</View>
</View>
{/* Scrollable content sits above the fixed hero */}
<ScrollView
key={product.id}
className="shop-scroll"
scrollY
scrollEventThrottle={16}
style={{ height: `${winHeight}px` }}
onScroll={handleScroll}
>
{/* Transparent spacer pushes content below hero */}
<View style={{ height: `${winHeight * 0.65}px` }} />
<View
className="shop-content"
style={{ backgroundColor: `rgb(${tone[0]}, ${tone[1]}, ${tone[2]})` }}
>
{/* Transition band — appears as hero scrolls away */}
<View
className="shop-band"
style={{
opacity: heroStyles.bandOpacity,
backgroundColor: `rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.88)`,
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)'
}}
>
<Text className="shop-band-title">{product.name}</Text>
<Text className="shop-band-subtitle">{product.subtitle || product.desc}</Text>
<View className="shop-band-price-row">
<Text className="shop-band-price">¥{product.price}</Text>
{product.originalPrice && product.originalPrice > product.price && (
<Text className="shop-band-original">¥{product.originalPrice}</Text>
)}
</View>
</View>
{/* Main copy blocks */}
<View
className="shop-detail-body"
style={{
opacity: heroStyles.contentOpacity,
transform: `translateY(${heroStyles.contentOffset}px)`
}}
>
{product.story && (
<View className="shop-section">
<Text className="shop-section-title"></Text>
<Text className="shop-section-text">{product.story}</Text>
</View>
)}
<View className="shop-divider" />
{product.scene && (
<View className="shop-section">
<Text className="shop-section-title">使</Text>
<Text className="shop-section-text">{product.scene}</Text>
</View>
)}
{product.tags && product.tags.length > 0 && (
<View className="shop-section">
<View className="shop-tag-row">
{product.tags.map(tag => (
<View key={tag} className="shop-tag">
<Text className="shop-tag-text">{tag}</Text>
</View>
))}
</View>
</View>
)}
<View className="shop-divider" />
{product.specs && product.specs.length > 0 && (
<View className="shop-section">
<Text className="shop-section-title"></Text>
<View className="shop-spec-table">
{product.specs.map(([k, v]) => (
<View key={k} className="shop-spec-row">
<Text className="shop-spec-key">{k}</Text>
<Text className="shop-spec-val">{v}</Text>
</View>
))}
</View>
</View>
)}
<View className="shop-bottom-spacer" />
</View>
</View>
</ScrollView>
{/* Back button — glass morphism circle */}
<View
className="shop-back-btn"
onTap={() => Taro.navigateBack()}
style={{ opacity: heroStyles.backBtnOpacity }}
>
<Text className="shop-back-arrow"></Text>
</View>
{/* Bottom CTA — fixed, glass, links to existing product page */}
<View
className="shop-cta-bar"
style={{ backgroundColor: `rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.92)` }}
>
<View className="shop-cta-price-col">
<Text className="shop-cta-price">¥{product.price}</Text>
{product.originalPrice && product.originalPrice > product.price && (
<Text className="shop-cta-original">¥{product.originalPrice}</Text>
)}
</View>
<View
className="shop-cta-btn"
onTap={() => Taro.navigateTo({ url: `/pages/product/index?id=${product.id}` })}
>
<Text className="shop-cta-btn-text"></Text>
</View>
</View>
</View>
)
}
+1 -354
View File
@@ -1,6 +1,5 @@
/* ============================================================
Shop Page — 商品浏览页
结构:单页双容器(列表层 + 详情覆层)
Shop Page — 商品浏览页(列表页)
============================================================ */
/* --- List Layer --- */
@@ -83,358 +82,6 @@
display: block;
}
/* ============================================================
Detail Overlay — fixed, full-screen, themed by product tone
============================================================ */
.shop-detail-view {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1200; /* sits above custom-tab-bar (z-index: 1000) */
opacity: 0;
transition: opacity 0.35s ease;
background: rgb(28, 22, 18);
}
.shop-detail-view.show {
opacity: 1;
}
/* --- Fixed Hero Area --- */
.shop-hero-fixed {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 65vh; /* hero takes 65% viewport height */
z-index: 0;
overflow: hidden;
}
.shop-hero-clip {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
will-change: height, transform;
}
.shop-hero-img {
width: 100%;
height: 100%;
object-fit: cover;
will-change: top;
}
/* Bottom gradient mask: pseudo-mask approach (better x-compatibility) */
.shop-hero-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 55%;
pointer-events: none;
}
/* Hero info (title, price) anchored at bottom */
.shop-hero-info {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
padding: 80rpx 40rpx 24rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.shop-hero-title {
font-size: 40rpx;
font-weight: 700;
line-height: 1.3;
color: #f0e6d8;
}
.shop-hero-subtitle {
font-size: 22rpx;
color: #b0a395;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-hero-price-row {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-top: 6rpx;
}
.shop-hero-price {
font-size: 44rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-hero-original {
font-size: 26rpx;
color: #8a7e72;
text-decoration: line-through;
}
/* --- ScrollView Content Layer --- */
.shop-scroll {
position: relative;
z-index: 3;
}
.shop-content {
min-height: 100vh; /* ensure scrollability */
will-change: transform;
}
/* --- Transition Band (sticky-like top info) --- */
.shop-band {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 4;
padding: 40rpx 40rpx 32rpx;
display: flex;
flex-direction: column;
gap: 10rpx;
transition: opacity 0.15s ease;
will-change: opacity;
}
.shop-band-title {
font-size: 44rpx;
font-weight: 700;
line-height: 1.3;
color: #f0e6d8;
}
.shop-band-subtitle {
font-size: 24rpx;
color: #b0a395;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-band-price-row {
display: flex;
align-items: baseline;
gap: 16rpx;
margin-top: 6rpx;
}
.shop-band-price {
font-size: 48rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-band-original {
font-size: 26rpx;
color: #8a7e72;
text-decoration: line-through;
}
/* --- Detail Body --- */
.shop-detail-body {
padding: 32rpx 40rpx 60rpx;
will-change: opacity, transform;
}
.shop-section {
margin-bottom: 32rpx;
}
.shop-section-title {
font-size: 34rpx;
font-weight: 700;
color: #f0e6d8;
margin: 28rpx 0 20rpx;
display: block;
position: relative;
padding-left: 20rpx;
}
.shop-section-title::before {
content: '';
position: absolute;
left: 0;
top: 6rpx;
bottom: 6rpx;
width: 6rpx;
border-radius: 4rpx;
background: linear-gradient(180deg, #ff9a9e, #fecfef);
}
.shop-section-text {
font-size: 28rpx;
color: #c4b8a8;
line-height: 1.75;
display: block;
}
.shop-divider {
height: 2rpx;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
margin: 40rpx 0;
}
/* --- Tags --- */
.shop-tag-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin: 20rpx 0;
}
.shop-tag {
padding: 10rpx 28rpx;
border-radius: 40rpx;
font-size: 22rpx;
font-weight: 500;
background: rgba(255, 154, 158, 0.12);
border: 1rpx solid rgba(255, 154, 158, 0.2);
display: inline-flex;
align-items: center;
justify-content: center;
}
.shop-tag-text {
color: #ff9a9e;
}
/* --- Spec Table --- */
.shop-spec-table {
width: 100%;
border-collapse: collapse;
border-radius: 16rpx;
overflow: hidden;
font-size: 26rpx;
}
.shop-spec-row {
display: flex;
padding: 20rpx 24rpx;
border-bottom: 1rpx solid rgba(255,255,255,0.06);
}
.shop-spec-row:last-child {
border-bottom: none;
}
.shop-spec-key {
width: 35%;
color: #8a7e72;
font-weight: 500;
font-size: 24rpx;
}
.shop-spec-val {
flex: 1;
color: #e8e0d6;
word-break: break-all;
}
.shop-bottom-spacer {
height: 200rpx;
}
/* --- Back Button --- */
.shop-back-btn {
position: fixed;
top: calc(env(safe-area-inset-top, 20px) + 20rpx);
left: 24rpx;
z-index: 50;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(16rpx) saturate(1.4);
-webkit-backdrop-filter: blur(16rpx) saturate(1.4);
border: 1rpx solid rgba(255,255,255,0.12);
box-shadow: 0 2rpx 16rpx rgba(0,0,0,0.25);
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s ease, transform 0.15s ease;
}
.shop-back-btn:active {
transform: scale(0.92);
}
.shop-back-arrow {
font-size: 36rpx;
color: rgba(255,255,255,0.85);
line-height: 1;
}
/* --- Bottom CTA Bar --- */
.shop-cta-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 60;
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom, 0px));
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
backdrop-filter: blur(16rpx) saturate(1.2);
-webkit-backdrop-filter: blur(16rpx) saturate(1.2);
transition: background-color 0.4s ease;
}
.shop-cta-price-col {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.shop-cta-price {
font-size: 40rpx;
font-weight: 700;
color: #ff9a9e;
}
.shop-cta-original {
font-size: 24rpx;
color: #8a7e72;
text-decoration: line-through;
}
.shop-cta-btn {
flex: 1;
max-width: 260rpx;
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%);
color: #5c3a3a;
border-radius: 50rpx;
padding: 24rpx 48rpx;
font-size: 30rpx;
font-weight: 600;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.shop-cta-btn-text {
color: inherit;
font-weight: 600;
}
/* ============================================================
Responsive: container center on wide screens
============================================================ */
+9 -304
View File
@@ -1,138 +1,18 @@
import { View, Text, Image, ScrollView } from '@tarojs/components'
import Taro, { hideTabBar, showTabBar } from '@tarojs/taro'
import { useState, useEffect, useCallback, useMemo } from 'react'
import { View, Text, Image } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useThemeContext } from '../../context/ThemeContext'
import './index.scss'
import { PRODUCTS } from '../../utils/productConfig'
import { ProductCategory } from '../../types'
import { useThemeContext } from '../../context/ThemeContext'
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
const lerp = (a: number, b: number, t: number) => a + (b - a) * t
const easeOut = (t: number) => 1 - Math.pow(1 - t, 3)
export default function ShopPage() {
const { theme } = useThemeContext()
const [winHeight, setWinHeight] = useState(667)
const [showDetail, setShowDetail] = useState(false)
const [detailVisible, setDetailVisible] = useState(false)
const [activeProduct, setActiveProduct] = useState<ProductCategory | null>(null)
const [progress, setProgress] = useState(0)
const { resolvedTheme } = useThemeContext()
useEffect(() => {
try {
const info = Taro.getSystemInfoSync()
setWinHeight(info.windowHeight)
} catch (e) {
// fallback to safe default
}
}, [])
useEffect(() => {
if (showDetail && activeProduct) {
hideTabBar({ animation: true }).catch(() => {})
} else {
showTabBar({ animation: true }).catch(() => {})
}
}, [showDetail, activeProduct])
const openDetail = useCallback((product: ProductCategory) => {
setActiveProduct(product)
setShowDetail(true)
setProgress(0)
// Slight delay so CSS transition picks up from display:block
setTimeout(() => setDetailVisible(true), 50)
}, [])
const closeDetail = useCallback(() => {
setDetailVisible(false)
setTimeout(() => {
setShowDetail(false)
setActiveProduct(null)
setProgress(0)
}, 350)
}, [])
const goToProduct = useCallback(() => {
if (!activeProduct) return
Taro.navigateTo({ url: `/pages/product/index?id=${activeProduct.id}` })
}, [activeProduct])
const handleScroll = useCallback((e: any) => {
const st = e.detail?.scrollTop ?? 0
const total = winHeight * 1.2
const p = clamp(st / total, 0, 1)
setProgress(p)
}, [winHeight])
/* Scroll-progress → multi-stage hero & content animations */
const heroStyles = useMemo(() => {
const p = progress
// 1. Image crop insets (vh %)
let topCrop = 0, bottomCrop = 0
if (p <= 0.4) {
const t = easeOut(p / 0.4)
topCrop = lerp(0, 32, t)
bottomCrop = lerp(0, 38, t)
} else if (p <= 0.55) {
const t = (p - 0.4) / 0.15
topCrop = lerp(32, 34, t)
bottomCrop = lerp(38, 41, t)
} else {
topCrop = 34; bottomCrop = 41
}
// 2. Hero translateY
let translateY = 0
if (p <= 0.4) translateY = 0
else if (p <= 0.7) translateY = lerp(0, -75, (p - 0.4) / 0.3)
else translateY = -75
// 3. Hero bottom info opacity (always visible on open, fades after scroll)
let heroInfoOpacity = 1
if (p <= 0.35) heroInfoOpacity = 1
else if (p <= 0.48) heroInfoOpacity = clamp(1 - (p - 0.35) / 0.13, 0, 1)
else heroInfoOpacity = 0
// 4. Transition band opacity
let bandOpacity = 0
if (p <= 0.55) bandOpacity = 0
else if (p <= 0.7) bandOpacity = (p - 0.55) / 0.15
else bandOpacity = 1
// 5. Content reveal
let contentOpacity = 1, contentOffset = 0
if (p <= 0.05) { contentOpacity = 0; contentOffset = 60 }
else if (p <= 0.22) {
const t = (p - 0.05) / 0.17
contentOpacity = easeOut(t)
contentOffset = lerp(60, 0, easeOut(t))
}
// 6. Back button (fade out as hero scrolls)
let backBtnOpacity = 1
if (p <= 0.15) backBtnOpacity = clamp(1 - p / 0.15, 0, 1)
return {
topCrop,
bottomCrop,
translateY,
heroInfoOpacity,
bandOpacity,
contentOpacity,
contentOffset,
backBtnOpacity,
visibleHeight: 100 - topCrop - bottomCrop
}
}, [progress])
const tone = useMemo(() => activeProduct?.tone || [28, 22, 18], [activeProduct])
const openDetail = (productId: string) => {
Taro.navigateTo({ url: `/pages/shop/detail/index?id=${productId}` })
}
return (
<View className={`shop-page theme-${theme}`}>
{/* ============================================================
LIST LAYER — always in DOM so scroll position is preserved
============================================================ */}
<View className={`shop-page theme-${resolvedTheme}`}>
<View className="shop-list">
<View className="shop-header">
<Text className="shop-header-title"></Text>
@@ -144,7 +24,7 @@ export default function ShopPage() {
<View
key={product.id}
className="shop-card"
onTap={() => openDetail(product)}
onTap={() => openDetail(product.id)}
>
<View className="shop-card-img-wrap">
<Image
@@ -170,181 +50,6 @@ export default function ShopPage() {
{/* TabBar spacer so list isn't obscured */}
<View style={{ height: '160px' }} />
</View>
{/* ============================================================
DETAIL LAYER — fixed overlay, single-page dual-container
============================================================ */}
{activeProduct && (
<View
className={`shop-detail-view ${detailVisible ? 'show' : ''}`}
style={{ display: showDetail ? 'block' : 'none' }}
>
{/* Fixed Hero */}
<View className="shop-hero-fixed">
<View
className="shop-hero-clip"
style={{
height: `${winHeight * heroStyles.visibleHeight / 100}px`,
overflow: 'hidden',
transform: `translateY(${winHeight * heroStyles.translateY / 100}px)`
}}
>
<Image
className="shop-hero-img"
src={activeProduct.images?.[0] || activeProduct.iconImg || ''}
mode="aspectFill"
style={{
width: '100%',
height: `${winHeight}px`,
position: 'absolute',
top: `${-winHeight * heroStyles.topCrop / 100}px`,
left: 0
}}
/>
</View>
{/* Bottom gradient overlay driven by product tone */}
<View
className="shop-hero-overlay"
style={{
background: `linear-gradient(to bottom, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0) 0%, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.55) 40%, rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.95) 100%)`
}}
/>
<View
className="shop-hero-info"
style={{ opacity: heroStyles.heroInfoOpacity }}
>
<Text className="shop-hero-title">{activeProduct.name}</Text>
<Text className="shop-hero-subtitle">{activeProduct.subtitle || activeProduct.desc}</Text>
<View className="shop-hero-price-row">
<Text className="shop-hero-price">¥{activeProduct.price}</Text>
{activeProduct.originalPrice && activeProduct.originalPrice > activeProduct.price && (
<Text className="shop-hero-original">¥{activeProduct.originalPrice}</Text>
)}
</View>
</View>
</View>
{/* Scrollable content sits above the fixed hero */}
<ScrollView
key={activeProduct.id}
className="shop-scroll"
scrollY
scrollEventThrottle={16}
style={{ height: `${winHeight}px` }}
onScroll={handleScroll}
>
{/* Transparent spacer pushes content below hero */}
<View style={{ height: `${winHeight * 0.65}px` }} />
<View
className="shop-content"
style={{ backgroundColor: `rgb(${tone[0]}, ${tone[1]}, ${tone[2]})` }}
>
{/* Transition band — appears as hero scrolls away */}
<View
className="shop-band"
style={{
opacity: heroStyles.bandOpacity,
backgroundColor: `rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.88)`,
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)'
}}
>
<Text className="shop-band-title">{activeProduct.name}</Text>
<Text className="shop-band-subtitle">{activeProduct.subtitle || activeProduct.desc}</Text>
<View className="shop-band-price-row">
<Text className="shop-band-price">¥{activeProduct.price}</Text>
{activeProduct.originalPrice && activeProduct.originalPrice > activeProduct.price && (
<Text className="shop-band-original">¥{activeProduct.originalPrice}</Text>
)}
</View>
</View>
{/* Main copy blocks */}
<View
className="shop-detail-body"
style={{
opacity: heroStyles.contentOpacity,
transform: `translateY(${heroStyles.contentOffset}px)`
}}
>
{activeProduct.story && (
<View className="shop-section">
<Text className="shop-section-title"></Text>
<Text className="shop-section-text">{activeProduct.story}</Text>
</View>
)}
<View className="shop-divider" />
{activeProduct.scene && (
<View className="shop-section">
<Text className="shop-section-title">使</Text>
<Text className="shop-section-text">{activeProduct.scene}</Text>
</View>
)}
{activeProduct.tags && activeProduct.tags.length > 0 && (
<View className="shop-section">
<View className="shop-tag-row">
{activeProduct.tags.map(tag => (
<View key={tag} className="shop-tag">
<Text className="shop-tag-text">{tag}</Text>
</View>
))}
</View>
</View>
)}
<View className="shop-divider" />
{activeProduct.specs && activeProduct.specs.length > 0 && (
<View className="shop-section">
<Text className="shop-section-title"></Text>
<View className="shop-spec-table">
{activeProduct.specs.map(([k, v]) => (
<View key={k} className="shop-spec-row">
<Text className="shop-spec-key">{k}</Text>
<Text className="shop-spec-val">{v}</Text>
</View>
))}
</View>
</View>
)}
<View className="shop-bottom-spacer" />
</View>
</View>
</ScrollView>
{/* Back button — glass morphism circle */}
<View
className="shop-back-btn"
onTap={closeDetail}
style={{ opacity: heroStyles.backBtnOpacity }}
>
<Text className="shop-back-arrow"></Text>
</View>
{/* Bottom CTA — fixed, glass, links to existing product page */}
<View
className="shop-cta-bar"
style={{ backgroundColor: `rgba(${tone[0]}, ${tone[1]}, ${tone[2]}, 0.92)` }}
>
<View className="shop-cta-price-col">
<Text className="shop-cta-price">¥{activeProduct.price}</Text>
{activeProduct.originalPrice && activeProduct.originalPrice > activeProduct.price && (
<Text className="shop-cta-original">¥{activeProduct.originalPrice}</Text>
)}
</View>
<View className="shop-cta-btn" onTap={goToProduct}>
<Text className="shop-cta-btn-text"></Text>
</View>
</View>
</View>
)}
</View>
)
}
+3 -6
View File
@@ -10,11 +10,10 @@ import {
getUserInfo,
verifyAdminPassword
} from '../../utils/store'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function UserDatabasePage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [isUnlocked, setIsUnlocked] = useState(false)
const [password, setPassword] = useState('')
const [pwdError, setPwdError] = useState(false)
@@ -78,9 +77,8 @@ export default function UserDatabasePage() {
if (!isUnlocked) {
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='udb-page'>
<ThemeToggle />
<View className='udb-lock-card dashed-card mt-20'>
<View className='star-badge' />
<Text className='udb-lock-icon'>🔐</Text>
@@ -107,9 +105,8 @@ export default function UserDatabasePage() {
}
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='udb-page'>
<ThemeToggle />
<View className='page-header dashed-card mt-20'>
<View className='star-badge' />
+1 -1
View File
@@ -5,7 +5,7 @@
/* 顶部导航 */
.page-header {
padding: 50px 32px 24px;
padding: calc(env(safe-area-inset-top, 0px) + 44px) 32px 24px !important;
margin: 20px 0 0;
background: transparent;
}
+2 -4
View File
@@ -2,11 +2,10 @@ import { View, Text, Image, Button, Textarea, Input } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState } from 'react'
import './index.scss'
import ThemeToggle from '../../components/ThemeToggle'
import { useThemeContext } from '../../context/ThemeContext'
export default function WordCloudPage() {
const { theme } = useThemeContext()
const { theme, resolvedTheme } = useThemeContext()
const [step, setStep] = useState(1)
const [baseImage, setBaseImage] = useState('')
const [namesText, setNamesText] = useState('')
@@ -57,9 +56,8 @@ export default function WordCloudPage() {
const goBack = () => { if (step > 1) setStep(step - 1); else Taro.navigateBack() }
return (
<View className={`theme-${theme}`}>
<View className={`theme-${resolvedTheme}`}>
<View className='wordcloud-page'>
<ThemeToggle />
<View className='page-header flex-between'>
<Text className='back-btn' onTap={goBack}></Text>
+8
View File
@@ -56,6 +56,14 @@ export interface StickerItem {
width: number
height: number
isOverlapping: boolean
/** 编辑状态持久化 */
edits?: {
brightness?: number // -100 ~ 100
hue?: number // -180 ~ 180
contrast?: number // -100 ~ 100
/** AI线稿处理后的临时路径 */
sketchSrc?: string
}
}
/** 设计清单条目 */
+23 -2
View File
@@ -185,6 +185,18 @@ export function updateDesign(id: string, patch: Partial<DesignItem>) {
setDesignList(list)
}
/** 批量删除设计条目 */
export function removeDesigns(ids: string[]) {
const list = getDesignList().filter(d => !ids.includes(d.id))
setDesignList(list)
}
/** 根据ID删除单条设计 */
export function removeDesign(id: string) {
const list = getDesignList().filter(d => d.id !== id)
setDesignList(list)
}
// ---------- 订单 ----------
export function getOrderList(): OrderItem[] {
@@ -229,16 +241,25 @@ export function designToOrder(designId: string): OrderItem | null {
// ---------- 主题 ----------
export type ThemeMode = 'light' | 'dark'
export type ThemeMode = 'light' | 'dark' | 'auto'
export function getTheme(): ThemeMode {
try { return Taro.getStorageSync(THEME_KEY) || 'light' } catch { return 'light' }
try { return Taro.getStorageSync(THEME_KEY) || 'auto' } catch { return 'auto' }
}
export function setTheme(theme: ThemeMode) {
Taro.setStorageSync(THEME_KEY, theme)
}
/** 根据自动模式解析实际生效主题 */
export function resolveTheme(mode: ThemeMode): 'light' | 'dark' {
if (mode === 'auto') {
const info = Taro.getSystemInfoSync()
return info.theme === 'dark' ? 'dark' : 'light'
}
return mode
}
// ---------- 收货地址 ----------
export function getAddressList(): AddressItem[] {