feat: ui update
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
---
|
||||
name: 小程序UI全量改版V2灰阶杂志风
|
||||
overview: 把 miniprogram/ 现有 18 个页面的 UI 从旧 iOS 蓝白风格全量重写为 stitch_ai_ui_v2 的「零彩色灰阶杂志风 + 毛玻璃悬浮层」,保持全部业务逻辑与状态矩阵不变,并补齐上传页处理中三步进度等 v2 稿未覆盖的细节,交付真机可运行的小程序代码。
|
||||
design:
|
||||
styleKeywords:
|
||||
- 零彩色灰阶
|
||||
- 毛玻璃浮层
|
||||
- 杂志大留白
|
||||
- 衬线大标题
|
||||
- 发丝线分隔
|
||||
- 悬浮胶囊标签栏
|
||||
- 买手店线稿质感
|
||||
- 苹果级缓动
|
||||
fontSystem:
|
||||
fontFamily: Songti SC / PingFang SC
|
||||
heading:
|
||||
size: 68rpx
|
||||
weight: 500
|
||||
subheading:
|
||||
size: 34rpx
|
||||
weight: 600
|
||||
body:
|
||||
size: 30rpx
|
||||
weight: 400
|
||||
colorSystem:
|
||||
primary:
|
||||
- "#0A0A0A"
|
||||
- "#1C1C1E"
|
||||
- "#2C2C2E"
|
||||
background:
|
||||
- "#FFFFFF"
|
||||
- "#F7F7F5"
|
||||
- "#EFEFED"
|
||||
text:
|
||||
- "#0A0A0A"
|
||||
- "#6E6E73"
|
||||
- "#AEAEB2"
|
||||
functional:
|
||||
- "#C9C9CE"
|
||||
- "#B9B9BE"
|
||||
- "#E5E5E7"
|
||||
- "#D2D2D7"
|
||||
todos:
|
||||
- id: baseline-tokens
|
||||
content: 用 [subagent:code-explorer] 盘点旧彩色类与原生弹窗引用,重写 app.wxss 令牌与 icons.wxss 图标
|
||||
status: completed
|
||||
- id: shell
|
||||
content: 重写悬浮玻璃胶囊 tabBar 与「大标题+滚动收缩」导航,新增 utils/nav.ts 阈值 handler
|
||||
status: completed
|
||||
dependencies:
|
||||
- baseline-tokens
|
||||
- id: overlay
|
||||
content: 新增 v2-overlay 玻璃弹层组件(Modal/ActionSheet/Toast)并替换原生确认与选择弹窗
|
||||
status: completed
|
||||
dependencies:
|
||||
- baseline-tokens
|
||||
- id: tab-pages
|
||||
content: 重写 5 个 tab 页(衣橱/穿搭/运势/分享/我的)的视觉与信息层级
|
||||
status: completed
|
||||
dependencies:
|
||||
- shell
|
||||
- overlay
|
||||
- id: detail-pages
|
||||
content: 重写详情与表单页(衣物详情/穿搭详情/穿搭编辑/AI 推荐/通知两页)
|
||||
status: completed
|
||||
dependencies:
|
||||
- shell
|
||||
- overlay
|
||||
- id: upload-flow
|
||||
content: 补全上传处理过程三态,重写登录/分享三页/商城/订单
|
||||
status: completed
|
||||
dependencies:
|
||||
- shell
|
||||
- overlay
|
||||
- id: qa
|
||||
content: 用 [skill:playwright-cli] 逐屏对照 v2 基准走查,清理零彩色与类名残留并输出真机验收清单
|
||||
status: completed
|
||||
dependencies:
|
||||
- tab-pages
|
||||
- detail-pages
|
||||
- upload-flow
|
||||
---
|
||||
|
||||
## 产品概述
|
||||
|
||||
把正在运行的「纸条 AI 衣橱」微信小程序(`miniprogram/`,当前仍是 iOS 蓝白彩色风)整体升级为 v2 设计语言:零彩色灰阶、毛玻璃浮层、杂志式大留白、衬线大标题。18 个页面全部覆盖,真机可运行,业务逻辑与在途改动保持不变。同时补全 v2 相对现有前端需要强化的部分,重点是衣物上传后的处理过程界面。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 全局设计系统替换:旧 iOS 彩色令牌(#007AFF 蓝等)→ v2 灰阶令牌、衬线/无衬线双字阶、1rpx 发丝线、玻璃层与统一缓动
|
||||
- 导航体系重构:进入页面为左对齐衬线大标题 + 大写英文 eyebrow,向下滚动收缩为居中玻璃小标题
|
||||
- 底部标签栏改为悬浮玻璃胶囊,未读徽标零彩色化(黑底白字数字胶囊)
|
||||
- 18 个页面按 v2 重排信息层级与视觉:登录、衣橱、穿搭、运势、分享、我的、上传、衣物详情、穿搭详情、穿搭编辑、AI 推荐、通知列表与详情、分享创建与两个分享详情、额度与会员、订单
|
||||
- 衣物处理过程界面补全:选图后显示用户实际图片 + 三段发丝线进度轨(排队中 / AI 抠图 / 智能分类,由 ProcessStatus 轮询驱动 step 0/1/2)+ 失败态与重新上传 / 手动填写 / 额度不足弹窗,并保持衣橱「AI 处理中」角标与衣物详情处理中态一致
|
||||
- 弹层视觉统一:居中玻璃卡、底部动作表、深色玻璃胶囊 Toast
|
||||
- 全状态覆盖:骨架屏 / 空态 / 列表 / 加载更多 / 没有更多了 / 失败 / 失效 / 只读
|
||||
|
||||
## 视觉要求
|
||||
|
||||
- 零彩色纪律:所有颜色取自灰阶令牌,唯一彩色例外是运势页幸运色的 8px 色点;旧状态色语义在零彩色下重建(进行中=实心黑胶囊,中性=浅灰胶囊,失效=降透明度或删除线,AI 来源=黑底白字描边胶囊,删除/取消=黑字下划线文字按钮)
|
||||
- 内容区像杂志内页:纯白纸面、超大留白、发丝线分组、大块内容保持直角,圆角只出现在胶囊/抽屉/小控件
|
||||
- 衣物图片是唯一主角;导航、底部标签、弹窗与抽屉毛玻璃悬浮在内容之上
|
||||
- 交互保持顺滑:统一缓动、按压缩放反馈、列表递进入场,滚动收缩不做逐帧重绘
|
||||
|
||||
## 边界与约束
|
||||
|
||||
- 只改 UI 层(WXML/WXSS + 必要的 TS 触点),不重构请求、轮询、支付、分享、日志等业务逻辑
|
||||
- 工作区存在未提交改动(穿搭详情、穿搭页、分享页、衣物详情、AI 推荐、config.ts、models.ts),必须以当前工作区版本为基线保留
|
||||
- 不做设计稿 HTML 的新增或修改,`stitch_ai_ui_v2/index.html` 仅作只读视觉基准
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 微信小程序原生开发:WXML + WXSS + TypeScript(`strict` 严格模式),`renderer: webview`,无框架、无构建链、无组件库
|
||||
- 视觉事实来源:`stitch_ai_ui_v2/index.html`(单文件 375×812 交互原型,只读)
|
||||
- 单位换算规则:v2 的 px × 2 = rpx(`--pad:20px`→`40rpx`,`.display:34px`→`68rpx`,`.eyebrow:10px`→`20rpx`),0.5px 发丝线 → `1rpx`
|
||||
- 图标:沿用现有 `styles/icons.wxss` 的 CSS mask + `currentColor` 方案(天然单色,符合零彩色),把内联 SVG 的 `stroke-width` 由 2 调整为 1.5,并补齐 v2 用到的图标(返回 chevron-left、时钟、图片、锁、向上箭头等)
|
||||
- 字体:系统字体栈(衬线 `"Songti SC","STSong","Noto Serif SC",Georgia,serif`;无衬线 `-apple-system,BlinkMacSystemFont,"PingFang SC"`),禁用 `wx.loadFontFace`(需 https 外链)
|
||||
|
||||
## 实现方案
|
||||
|
||||
核心策略:**令牌层 → 组件层 → 页面层** 三段式。先把设计系统落到 `app.wxss` 与共享组件,再逐页重排 WXML/WXSS,TS 只做最小触点修改,避免 18 页各写一套视觉。
|
||||
|
||||
1. **令牌层先行**:重写 `app.wxss`,用 `page` 选择器固化灰阶、双字阶、发丝线、玻璃、圆角、缓动与排版工具类;全量删除旧彩色类(`.tag-blue/.tag-green/.tag-red/.tag-orange/.tag-indigo/.btn-primary` 蓝底/`.fab` 蓝底蓝阴影等,页面侧仅约 35 处引用),**不留兼容别名**,防止零彩色纪律被绕过。
|
||||
2. **组件层收敛重复**:重写 `custom-tab-bar`(通栏 → 悬浮玻璃胶囊,选中态实心黑 + 顶部短横线)、`components/navigation-bar`(同标签名原地重写,页面 JSON 不用改,保留胶囊安全区计算与页面栈不足 switchTab 兜底)、`components/clothes-card`、`components/empty-state`;新增 `components/v2-overlay` 承载 Modal / ActionSheet / Toast 三类玻璃弹层。
|
||||
3. **页面层只改结构与样式**:18 页 WXML/WXSS 按 v2 稿逐屏重排(下划线式分类条与分段、规格表、数字条、无缝拼图、悬浮玻璃操作条等),TS 仅在必要处改动:状态角标语义字段(如 `_statusType: 'blue'` → 灰阶语义)、弹层调用替换、滚动收缩 handler 接入。
|
||||
4. **滚动收缩驱动方式**:15 个页面用 `<scroll-view>`(wardrobe 还带 refresher 与上拉分页),3 个页面(login / notification-detail / upload)用页面级滚动。统一新增 `utils/nav.ts` 导出共享 handler,页面侧仅一行接入;scroll-view 页面加 `bindscroll="onNavScroll"`,页面级滚动页面用 `onPageScroll`。
|
||||
5. **上传处理过程补全**:processing 态改为 `Processing` eyebrow + 衬线大标题「AI 正在识别你的衣物…」+ 灰字「通常需要 10 秒左右,请稍候」+ 用户实际选中图片(`imagePath`,极小圆角、居中留白)+ 三段发丝线进度轨(实心/空心圆点 + 1px 连线,`step` 由既有 `pollStatus` 驱动);failed 态为「!」+ 衬线标题 + 双按钮;保留「衣物已上传」弹窗、额度不足弹窗与手动填写跳转。
|
||||
|
||||
关键取舍:
|
||||
|
||||
- **不引入组件库(tdesign 等)**:v2 需要精确控制字距、行高、发丝线与玻璃透明度,组件库反而增加覆盖成本与体积;原生 WXSS 即可满足。
|
||||
- **弹层不做全量替换**:`utils/request` 层统一的 `wx.showToast` 错误提示保留原生(非阻塞、改动风险高),仅把用户可见的确认/选择类弹窗(上传来源选择、上传完成、额度不足、删除确认、公开确认、取消分享、AI 生成确认、支付确认)迁移到 `v2-overlay`,回调分支与原逻辑一一对应。
|
||||
- **不做逐页独立样式**:所有页面只消费令牌与通用类,避免出现 18 份近似样式,降低后续维护成本。
|
||||
|
||||
性能与可靠性:
|
||||
|
||||
- 滚动监听只在跨越阈值(约 56px)时翻转布尔值 `setData`,绝不逐帧 setData;避免 `scroll-view` 高频事件造成卡顿
|
||||
- `backdrop-filter` 每屏限制 2 层(导航 + tabBar 或浮层),并给出 `rgba` 兜底底色,规避 Android 低版本不支持导致的透明穿帮
|
||||
- 保留 `lazy-load`、`wx:key`、`mergePaged` 分页合并、未读轮询 30s 节流与 tabBar 首帧安全 setData 机制
|
||||
- 页面切换动画用 CSS `animation` 实现,不重建 DOM,不增加运行时开销
|
||||
|
||||
## 执行注意
|
||||
|
||||
- **未提交改动保护**:`outfit-detail.wxml/.ts`、`outfit.wxml/.ts/.wxss`、`closet-share.wxml/.ts`、`closet-share-create.ts`、`clothes-detail.ts`、`ai-recommend.ts`、`outfit-share-detail.ts`、`types/models.ts`、`utils/config.ts` 的在途逻辑必须原样保留,以工作区当前版本为基线改写 WXML,禁止回退或覆盖。
|
||||
- **零彩色自查**:改完后用正则扫描 `miniprogram/**/*.wxss`,除幸运色色点与衣物线稿灰阶外,不允许出现任何有彩色 hex / rgba 彩色通道。
|
||||
- **类名一致性**:WXML 引用的每个类都要在 WXSS(页面或全局)中有定义,避免残留 `.card/.tag-*` 等已删除类造成样式丢失。
|
||||
- **悬浮层避让**:悬浮 tabBar 与悬浮操作条要求内容区 `padding-bottom` 预留(≥200rpx),避免最后一行内容被遮挡;滚动收缩的导航下内容顶部要预留状态栏 + 导航高度。
|
||||
- **组件样式隔离**:`clothes-card`/`empty-state` 已是 `apply-shared`,可继续复用全局令牌与通用类;新增组件若需复用全局类,同样在 `.json` 声明 `styleIsolation: apply-shared`。
|
||||
- **日志规范**:沿用 `utils/logger`,本次不新增噪声日志;弹层与滚动逻辑异常只走 `logger.warn`。
|
||||
- **验收方式**:Linux 环境无法启动微信开发者工具,视觉验收依赖「v2 原型基准截图 + 静态自查 + 用户真机/开发者工具预览」,需在收尾时输出逐页验收清单。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["令牌层 app.wxss:灰阶 / 双字阶 / 发丝线 / 玻璃 / 圆角 / 缓动"] --> B["图标层 styles/icons.wxss:1.5px 描边 mask 图标"]
|
||||
A --> C["壳层:custom-tab-bar 悬浮胶囊 + navigation-bar 大标题收缩 + utils/nav.ts 滚动阈值"]
|
||||
A --> D["通用组件层:clothes-card / empty-state / v2-overlay(Modal·ActionSheet·Toast)"]
|
||||
C --> E["页面层:5 个 tab 页 / 详情页 / 表单流程页 / 分享页 / 交易页 / 登录页"]
|
||||
D --> E
|
||||
E --> F["业务逻辑层:pages/*/*.ts 保持不变(请求 / 轮询 / 支付 / 分享 / 日志)"]
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
/root/zt-closet-wxmini-frontend/miniprogram/
|
||||
├── app.wxss # [MODIFY] 全量替换为 v2 令牌与通用类:灰阶墨阶/纸面/发丝线/玻璃/深色块、
|
||||
│ # 衬线+无衬线双字阶、.eyebrow/.display/.t-*/.num/.hair/.pad 工具类、
|
||||
│ # .btn(ink/ghost/text)/.tag(零彩色变体)/.chip/.skeleton/.seg/.spec/.statbar/.tile/.empty、
|
||||
│ # 悬浮层与安全区;删除全部旧彩色类与旧卡片投影
|
||||
├── app.json # [MODIFY] window.backgroundColor → #FFFFFF;tabBar color #AEAEB2 /
|
||||
│ # selectedColor #0A0A0A / backgroundColor #FFFFFF(custom tabBar 的兜底色)
|
||||
├── styles/icons.wxss # [MODIFY] 26 个 mask 图标描边 2 → 1.5;补齐 chevron-left / clock / image /
|
||||
│ # lock / arrow-up-right / star-half 等 v2 用到的图标;尺寸档位对齐 v2(21px→42rpx 等)
|
||||
├── utils/nav.ts # [NEW] 导出 onNavScroll / onPageScrollNav 共享 handler:按阈值(56px)翻转
|
||||
│ # navScrolled 布尔值,跨阈值才 setData;供 18 页一行接入
|
||||
├── custom-tab-bar/index.wxml # [MODIFY] 通栏 → 悬浮玻璃胶囊结构;选中态实心黑 + 顶部短横线;徽标零彩色
|
||||
├── custom-tab-bar/index.wxss # [MODIFY] 左右各 32rpx、圆角 56rpx、距底 24rpx + env(safe-area-inset-bottom)、
|
||||
│ # rgba(255,255,255,.72) + backdrop-filter + 1rpx 发丝边;.tab-badge 黑底白字数字胶囊
|
||||
├── custom-tab-bar/index.ts # [KEEP] 未读轮询节流、首帧安全 setData、matchRoute 选中态逻辑不动
|
||||
├── components/navigation-bar/ # [MODIFY] 原地重写(标签名不变,页面 JSON 无需改)
|
||||
│ ├── navigation-bar.wxml # 结构:状态栏占位 + 返回按钮 + 右侧操作槽;大标题态在页面内容顶部渲染
|
||||
│ ├── navigation-bar.wxss # 非滚动:透明无边框;滚动后:玻璃底 + 居中 30rpx 小标题 + 1rpx 发丝下边
|
||||
│ ├── navigation-bar.ts # 保留 getMenuButtonBoundingClientRect 安全区计算与页面栈不足 switchTab 兜底;
|
||||
│ │ # 新增 large / eyebrow / scrolled / actionText 属性
|
||||
│ └── navigation-bar.json # [MODIFY] 如需复用全局 .icon/.eyebrow 则声明 styleIsolation: apply-shared
|
||||
├── components/clothes-card/ # [MODIFY] 去卡片容器与描边,图下两行(衬线品名 + 灰色属性行);
|
||||
│ └── (wxml/wxss) # 状态角标改零彩色(进行中=实心黑胶囊,其他=浅灰胶囊);勾选圈改细描边黑圈
|
||||
├── components/empty-state/ # [MODIFY] 细描边线稿图标 + 衬线标题 + 灰字描述 + 黑胶囊/描边双按钮
|
||||
│ └── (wxml/wxss)
|
||||
├── components/v2-overlay/ # [NEW] 统一玻璃弹层组件
|
||||
│ ├── index.wxml / index.wxss # modal 居中玻璃卡 / actionsheet 底部动作表(含抓手)/ toast 深色玻璃胶囊
|
||||
│ ├── index.ts # properties: type/title/content/buttons/visible;methods: open/close;
|
||||
│ │ # triggerEvent('confirm'|'cancel'|'select'),与原生回调分支一一对应
|
||||
│ └── index.json # styleIsolation: apply-shared
|
||||
└── pages/ # 18 页:WXML/WXSS 重排 + 必要 TS 触点
|
||||
├── login/ # [MODIFY] 全屏纯白 + 居中线稿 Logo + 衬线品牌名 + 灰字 slogan + 底部实心黑胶囊;
|
||||
│ # 新手礼包改居中玻璃卡 + 发丝线权益列表
|
||||
├── wardrobe/ # [MODIFY] 衬线大标题 + 右侧统计行;分类由横滑胶囊改下划线式分类条(保留 categories
|
||||
│ # 数据与选中绑定);网格去卡片容器;FAB 改黑底白玻璃圆钮;
|
||||
│ # ts 仅改 _statusType 语义('blue' → 零彩色变体)
|
||||
├── outfit/ # [MODIFY] 分段改下划线式文字分段;「创建穿搭」「AI 推荐」合并为 tabBar 上方
|
||||
│ # 悬浮玻璃双按钮条;社区卡改瀑布流编辑排版
|
||||
├── horoscope/ # [MODIFY] 巨大衬线星座名 + 超大衬线数字 + 5 个细描边星;三项评分改横向发丝线刻度尺;
|
||||
│ # 长文本改衬线引文排版;幸运色/数字两栏发丝线小格(幸运色色点保留)
|
||||
├── closet-share/ # [MODIFY] 下划线分段;分享卡改横向条目(左 72×72 灰度拼图 + 右衬线标题/灰字有效期);
|
||||
│ # 失效项降至 40% 透明度 + 描边角标
|
||||
├── profile/ # [MODIFY] 72px 头像 + 衬线昵称 + 灰字副信息;额度概览改四栏发丝线数字条;
|
||||
│ # 功能列表改纯发丝线分组列表(行高 104rpx + 灰色值 + 极细箭头)
|
||||
├── upload/ # [MODIFY] 三态重写:idle 巨大虚线发丝线方框 + 细描边相机 + 衬线引导语;
|
||||
│ # processing 显示实际图片 + 三段发丝线进度轨(排队中/AI 抠图/智能分类);
|
||||
│ # failed 极简「!」+ 衬线标题 + 双按钮;ts 接入 v2-overlay 替换弹窗
|
||||
├── clothes-detail/ # [MODIFY] 属性改规格表(左 13px 灰标签 / 右 15px 黑值 + 发丝线行,分组用中英文 eyebrow);
|
||||
│ # 量化评分改细描边方点;适用场合改描边胶囊;保留处理中/失败/失效/只读四态
|
||||
├── outfit-detail/ # [MODIFY] 无缝白缝拼图封面 + 衬线大标题 + eyebrow 来源标签 + 发丝线标签行 +
|
||||
│ # 横滑单品缩略图;底部改悬浮玻璃胶囊操作条;保留属主/访客双角色与在途逻辑
|
||||
├── outfit-edit/ # [MODIFY] 表单视图与选择衣物视图双视图重排;选择态改细描边勾选 + 黑胶囊计数条
|
||||
├── ai-recommend/ # [MODIFY] 生成态改呼吸感细描边圆环 + 衬线文案;结果态改拼图卡 + 横滑单品;ts 替换确认弹窗
|
||||
├── notification/ # [MODIFY] 未读用左侧 2.5px 黑竖条 + 黑字标题,已读降为 ink-3;3 态覆盖
|
||||
├── notification-detail/ # [MODIFY] 衬线标题 + eyebrow 时间 + 发丝线正文排版
|
||||
├── closet-share-create/ # [MODIFY] 配置 / 选择衣物 / 成功预览三视图重排;底部实心黑胶囊按钮
|
||||
├── closet-share-detail/ # [MODIFY] 分享者行 + 无缝拼图 + 只读网格 + 悬浮玻璃「分享给好友」黑胶囊;保留需登录/失效态
|
||||
├── outfit-share-detail/ # [MODIFY] 同上结构,保留双角色与失效态
|
||||
├── mall/ # [MODIFY] 会员卡用全稿唯一深色块(--block);额度改四栏数字条;商品行左线稿图标 /
|
||||
│ # 中名称描述 / 右 tabular 价格 + 黑胶囊「购买」;ts 替换支付确认弹窗
|
||||
└── order/ # [MODIFY] 订单状态改描边胶囊(文案不依赖颜色);待支付倒计时改 tabular 衬线数字;3 态覆盖
|
||||
```
|
||||
|
||||
## 关键代码结构
|
||||
|
||||
`app.wxss` 的令牌块是全部 18 页与所有组件的共享契约(rpx 已按 px×2 换算,`page` 选择器替代 `:root`):
|
||||
|
||||
```css
|
||||
page {
|
||||
/* 墨阶 */
|
||||
--ink: #0A0A0A; --ink-2: #6E6E73; --ink-3: #AEAEB2; --ink-4: #C9C9CE; --art: #B9B9BE;
|
||||
/* 纸面 */
|
||||
--paper: #FFFFFF; --paper-2: #F7F7F5; --paper-3: #EFEFED;
|
||||
/* 发丝线与填充 */
|
||||
--hairline: rgba(0,0,0,.08); --hairline-2: rgba(0,0,0,.14);
|
||||
--fill: rgba(120,120,128,.07); --fill-2: rgba(120,120,128,.13);
|
||||
/* 深色块(全稿唯一视觉锚点:商城会员卡) */
|
||||
--block: #1C1C1E; --block-2: #2C2C2E;
|
||||
/* 玻璃 */
|
||||
--glass: rgba(255,255,255,.72); --glass-2: rgba(255,255,255,.86); --glass-dark: rgba(28,28,30,.82);
|
||||
/* 字族与缓动 */
|
||||
--serif: "Songti SC","STSong","Noto Serif SC",Georgia,serif;
|
||||
--sans: -apple-system,BlinkMacSystemFont,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||||
--ease: cubic-bezier(.32,.72,0,1);
|
||||
/* 圆角与度量 */
|
||||
--r-img: 8rpx; --r-ctrl: 20rpx; --r-sheet: 40rpx; --r-pill: 999rpx;
|
||||
--pad: 40rpx; --nav-h: 88rpx; --tab-h: 120rpx;
|
||||
}
|
||||
```
|
||||
|
||||
## 设计风格
|
||||
|
||||
沿用 v2 稿的「安静的展厅」:把衣橱当成一间高级买手店,内容区是一张纯白纸张,衣物图片是唯一主角,所有 UI 元素退到发丝线级别;导航、底部标签、弹窗做成悬浮在纸面上的毛玻璃层。零彩色,靠字重对比、留白尺度、发丝线与玻璃透明度建立秩序。
|
||||
|
||||
## 布局与信息架构
|
||||
|
||||
- 页面左右边距 40rpx;区块垂直间距 64rpx,块内 32rpx;正文行高 1.6~1.75;图文之间永远留白
|
||||
- 分组用 1rpx 发丝线而非卡片描边/投影;大块内容保持直角,圆角只出现在胶囊、抽屉与小控件
|
||||
- 导航:进入页面为左对齐衬线大标题 + 大写英文 eyebrow,向下滚动收缩为居中玻璃小标题
|
||||
- 底部标签:悬浮玻璃胶囊(左右各 32rpx、圆角 56rpx、距底 24rpx + 安全区),选中态实心黑 + 顶部短横线
|
||||
- 分类与分段:由横滑胶囊改为下划线式极简条,选中项黑字加粗 + 2rpx 黑色下划线
|
||||
|
||||
## 页面要点
|
||||
|
||||
- 五个 tab 页:衣橱为衬线大标题 + 统计行 + 下划线分类条 + 无容器网格;穿搭为下划线分段 + tabBar 上方悬浮玻璃双按钮条;运势为杂志内页(巨大衬线星座名、超大数字、发丝线刻度尺、衬线引文、两栏小格);分享为横向条目卡 + 失效降透明;我的为四栏数字条 + 纯发丝线分组列表
|
||||
- 详情页:规格表(左灰标签 / 右黑值)+ 细描边方点评分 + 描边胶囊标签 + 无缝拼图封面 + 悬浮玻璃底部操作条
|
||||
- 表单流程页:上传 idle 为巨大虚线方框 + 细描边相机;processing 为实际图片 + 三段发丝线进度轨(实心/空心圆点 + 1px 连线);failed 为极简「!」+ 衬线标题 + 双按钮;穿搭编辑为表单与选择双视图
|
||||
- 分享与交易:分享创建三视图 + 两个分享详情只读网格;额度与会员用全稿唯一深色块 + 四栏数字条 + 黑胶囊购买;订单用描边状态胶囊 + tabular 衬线倒计时
|
||||
- 登录:全屏纯白 + 居中线稿 Logo + 衬线品牌名 + 灰字 slogan + 底部实心黑胶囊按钮,新手礼包为居中玻璃卡
|
||||
|
||||
## 交互与动效
|
||||
|
||||
统一缓动 cubic-bezier(.32,.72,0,1),时长 320~420ms;按压 scale(.97) + 透明度 0.6;列表入场 40ms 递增;页面切换右滑入 + 轻微缩放淡入;弹层自底部滑入带轻微回弹;四类弹层(居中玻璃卡 / 底部抽屉 / 动作表 / 深色玻璃 Toast)统一由 v2-overlay 管理,保证可开关且不遮挡导航与标签栏。
|
||||
|
||||
## 响应式与适配
|
||||
|
||||
以 375pt 为基准,全量使用 rpx 自适应;顶部状态栏与微信胶囊由 navigation-bar 组件按 `wx.getMenuButtonBoundingClientRect()` 动态计算;底部安全区用 `env(safe-area-inset-bottom)`;悬浮层下内容预留 padding,避免最后一行被遮挡。
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- Purpose: 改造前一次性盘点旧彩色类与原生弹窗的全部引用点、18 个页面的区块与状态矩阵基线,形成逐页改造清单,避免漏改导致样式丢失或弹窗语义错位
|
||||
- Expected outcome: 一份可逐条勾选的清单:旧彩色类引用位置(约 35 处)、`wx.showModal/showActionSheet` 调用点、每页 WXML 区块与状态分支、各页滚动容器类型
|
||||
|
||||
### Skill
|
||||
|
||||
- **playwright-cli**
|
||||
- Purpose: 用无头浏览器打开 `stitch_ai_ui_v2/index.html`,逐屏截图存档为视觉基准,并在收尾阶段对关键屏做尺寸、颜色、字阶的像素级对照走查
|
||||
- Expected outcome: 18 屏基准截图 + 与实现值的对照结论(间距、字号、发丝线、玻璃透明度、悬浮层避让),据此修正偏差
|
||||
@@ -0,0 +1,206 @@
|
||||
---
|
||||
name: 穿搭页补全社区分段
|
||||
overview: 在穿搭 tab 新增「社区」分段,浏览所有用户公开的穿搭(作者信息、拼图封面、点赞/收藏数、场景与发布时间),并新增后端社区列表接口支撑。
|
||||
design:
|
||||
styleKeywords:
|
||||
- iOS HIG
|
||||
- 清爽克制
|
||||
- 发丝分隔线
|
||||
- 系统蓝
|
||||
- 线性图标
|
||||
fontSystem:
|
||||
fontFamily: PingFang SC
|
||||
heading:
|
||||
size: 32rpx
|
||||
weight: 600
|
||||
subheading:
|
||||
size: 28rpx
|
||||
weight: 500
|
||||
body:
|
||||
size: 26rpx
|
||||
weight: 400
|
||||
colorSystem:
|
||||
primary:
|
||||
- "#007AFF"
|
||||
- "#5856D6"
|
||||
background:
|
||||
- "#F2F2F7"
|
||||
- "#FFFFFF"
|
||||
text:
|
||||
- "#000000"
|
||||
- "#3C3C43"
|
||||
- "#8E8E93"
|
||||
- "#C7C7CC"
|
||||
functional:
|
||||
- "#FF2D55"
|
||||
- "#34C759"
|
||||
- "#FF9500"
|
||||
- "#FF3B30"
|
||||
todos:
|
||||
- id: backend-community-api
|
||||
content: 用 [subagent:code-explorer] 确认后端批量查询与计数模式;实现 GET /outfit/community 接口(DTO/DAO/Service/路由)并更新接口文档
|
||||
status: completed
|
||||
- id: frontend-community-data
|
||||
content: models.ts 新增社区穿搭类型;outfit.ts 实现三态分段、社区取数、相对时间与详情跳转
|
||||
status: completed
|
||||
dependencies:
|
||||
- backend-community-api
|
||||
- id: frontend-community-ui
|
||||
content: outfit.wxml/wxss 实现社区分段、作者行卡片、互动数据与空态样式
|
||||
status: completed
|
||||
dependencies:
|
||||
- frontend-community-data
|
||||
- id: verify-community
|
||||
content: 运行 go build 与 tsc 类型检查,核对前后端字段契约与路由注册
|
||||
status: completed
|
||||
dependencies:
|
||||
- backend-community-api
|
||||
- frontend-community-ui
|
||||
---
|
||||
|
||||
## 产品概述
|
||||
|
||||
为「纸条AI衣橱」小程序补全社区功能:在「穿搭」页现有「我的穿搭 / 已收藏」两个分段基础上,新增「社区」分段,让所有用户都能浏览其他用户公开的穿搭,并进入详情点赞、收藏,形成「公开到社区 → 他人浏览互动」的完整闭环。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 穿搭页分段控件扩展为三态:「我的穿搭 / 已收藏 / 社区」,切换即加载对应列表并重置分页
|
||||
- 社区分段以卡片流展示其他用户公开的穿搭(仅展示已公开且审核通过、状态正常的穿搭)
|
||||
- 社区卡片内容:作者头像与昵称(卡片顶部)、拼图式衣物封面与件数、穿搭名称、场景标签、点赞数与收藏数、相对发布时间
|
||||
- 点击社区卡片进入他人视角的穿搭详情页,可点赞、收藏(复用现有详情页,不新增页面)
|
||||
- 社区列表支持下拉刷新、上拉加载更多、骨架屏加载态与空态提示
|
||||
- 社区分段隐藏「+ 创建穿搭 / AI 推荐」入口行(浏览场景不展示管理入口)
|
||||
|
||||
## 视觉与交互效果
|
||||
|
||||
延续现有 iOS HIG 风格:分组灰底、纯白卡片、发丝分隔线、系统蓝强调色。社区卡片在现有穿搭卡片结构上顶部新增作者行(圆形头像 + 昵称 + 右侧相对时间),底部 meta 行新增点赞、收藏两组互动数据,整体与「我的穿搭」卡片流视觉统一、切换分段有平滑过渡。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 前端:微信小程序原生 + TypeScript(沿用现有 webview 渲染与纯 WXSS 手写方案,不引入组件库)
|
||||
- 后端:Go + Gin + GORM/MySQL(zt-closet-backend,新增接口沿用现有分层:router → handler → service → dao)
|
||||
- 复用:前端 `request` 封装、`paged` 分页工具、`format.formatRelativeTime`、`logger`、`empty-state` 组件与图标库;后端 `PageRequest/PageResponse`、`normalizePage`、批量查询与批量组装模式
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 总体策略
|
||||
|
||||
后端新增鉴权接口 `GET /outfit/community`:分页返回「其他用户已公开(is_public=2 且 audit_status=2 且 status=1)」的穿搭列表,聚合作者信息与点赞/收藏计数后一次性下发;前端将 `pages/outfit` 页扩展为三态分段,第三段对接新接口渲染社区卡片流,点击进入现有他人视角详情页(owner=0)。全程复用既有模式,不引入新架构、不新增页面、不改动 tabBar。
|
||||
|
||||
### 关键决策与理由
|
||||
|
||||
1. **接口路径 `GET /outfit/community`**:与既有 `GET /outfit/:uuid` 在同一 Gin 路由树共存是安全的——项目内已有完全相同形态的先例(`/notification/unread` 与 `/notification/:uuid` 共存且线上运行正常),Gin 1.12 静态路由优先匹配。
|
||||
2. **排除本人的公开穿搭**(SQL 加 `user_id != 当前用户`):贴合用户「查看其他用户公开的穿搭」的表述,且避免与「我的穿搭」分段内容重复;后续如需包含自己,仅需去掉一个 WHERE 条件。
|
||||
3. **互动数据由后端批量聚合下发**(like_count / collect_count):现有穿搭列表在前端逐条请求 `/like/:uuid`(一页 20 条即 20 次请求),社区列表直接使用后端聚合值,杜绝该 N+1 问题。
|
||||
4. **后端批量组装杜绝 N+1**:复用 `buildOutfitListResponses` 的批量模式(一次性批量查明细 + 衣物),作者信息用已有 `UserGetByIDs`,点赞/收藏计数新增 `GROUP BY target_id` 批量方法;单页固定约 7 条 SQL,与页大小无关。
|
||||
5. **社区卡片复用现有拼图封面结构**(cover-item 循环)与卡片骨架,仅新增作者行与互动数据块,模板通过 `activeTab === 2` 条件渲染差异部分,避免大段重复。
|
||||
6. **仅社区分段隐藏入口按钮行**,「我的穿搭 / 已收藏」保持现状,控制改动面。
|
||||
|
||||
### 性能与可靠性
|
||||
|
||||
- 后端:分页 `page/page_size`(上限 50,`normalizePage` 规整),`ORDER BY id DESC` 走主键;批量计数对空列表短路,避免 `IN ()` 查询;错误统一返回现有错误码(19001 等),沿用 slog 日志风格。
|
||||
- 前端:`page_size=20`、切换分段重置并丢弃过期响应(沿用现有 reqSeq 序号防乱序机制);辅助字段(相对时间)本地计算零请求;失败不打断主流程,仅记录 code 不打印敏感信息。
|
||||
- 契约一致性:`created_at` 秒级时间戳(前端 `formatRelativeTime` 直接消费);`owner_avatar` 由后端 `upload.AvatarURL` 转为可访问 URL;`processed_image` 为完整 URL。
|
||||
|
||||
### 实施注意事项
|
||||
|
||||
- 后端仓库位于 `/root/zt-closet-backend`(不在前端工作区),当前存在未提交改动(收藏/通知修复),新代码只做追加、不动无关文件。
|
||||
- 后端改动需用户自行部署后,前端指向线上域名(BASE_URL)才能联调。
|
||||
- 前端不改 `app.json`、不改 `outfit-detail`、不改 tabBar;`outfit.json` 已引入所需组件,无需调整。
|
||||
- 复用 `isOutfitPublicVisible` 的可见性口径(公开 2 + 审核通过 2 + 状态正常 1),保证社区列表与详情页可见性判定一致。
|
||||
|
||||
## 目录结构
|
||||
|
||||
后端 `/root/zt-closet-backend/`:
|
||||
|
||||
```
|
||||
├── internal/dto/outfit.go # [MODIFY] 新增社区列表 DTO:OutfitCommunityListRequest(内嵌 PageRequest)/ OutfitCommunityListResponse(内嵌 PageResponse)/ OutfitCommunityItem(含 outfit_uuid、outfit_name、outfit_desc、scene、source、created_at、items、owner_name、owner_avatar、like_count、collect_count)
|
||||
├── internal/mysqldao/outfit.go # [MODIFY] 新增 OutfitListCommunity(ctx, excludeUserID, page, pageSize):查询 is_public=2 AND audit_status=2 AND status=1 AND user_id != 本人,Count + Order("id DESC") 分页,参照 OutfitListByUserID 写法
|
||||
├── internal/mysqldao/like.go # [MODIFY] 新增 LikeCountByTargets(ctx, likeType, targetIDs):SELECT target_id, COUNT(*) ... GROUP BY target_id(仅 status=1),空入参短路
|
||||
├── internal/mysqldao/collect.go # [MODIFY] 新增 CollectCountByTargets 批量计数(同上模式)
|
||||
├── internal/service/outfit.go # [MODIFY] 新增 CommunityList:normalizePage → 查列表 → 批量组装(复用 OutfitItemListByOutfitIDs + ClothesGetByIDs + UserGetByIDs + upload.AvatarURL + 批量计数),空列表直接返回空数组
|
||||
├── internal/routers/api/v1/outfit.go # [MODIFY] 新增 Community handler(GetUserID → ShouldBindQuery → svc → ToResponse,与现有 List 写法一致)
|
||||
├── internal/routers/router.go # [MODIFY] 鉴权组注册 auth.GET("/outfit/community", outfitHandler.Community)
|
||||
└── 接口文档.md # [MODIFY] 新增社区列表接口章节;修正 FAQ「没有社区广场接口」的过时说明
|
||||
```
|
||||
|
||||
前端 `/root/zt-closet-wxmini-frontend/`:
|
||||
|
||||
```
|
||||
└── miniprogram/
|
||||
├── types/models.ts # [MODIFY] 新增 CommunityOutfitItem 类型(与后端响应字段一一对应)
|
||||
└── pages/outfit/
|
||||
├── outfit.ts # [MODIFY] activeTab 扩展 0/1/2;fetchList 按分段选 /outfit、/collect、/outfit/community;社区项装饰(直接取 like_count/collect_count,计算相对时间 _time);onOutfitTap 社区项 owner=0;空态文案分支;日志沿用 logger
|
||||
├── outfit.wxml # [MODIFY] 分段控件增加「社区」;entry-row 在社区分段隐藏;卡片新增作者行(社区分段)与收藏数、相对时间;状态标签仅非社区分段展示;空态文案分支
|
||||
└── outfit.wxss # [MODIFY] 新增作者行、圆形头像(含 icon-user 占位)、昵称、时间、互动数据样式(延续现有 rpx 与色板)
|
||||
```
|
||||
|
||||
## 关键代码结构
|
||||
|
||||
后端响应契约(新增到 `internal/dto/outfit.go`):
|
||||
|
||||
```
|
||||
type OutfitCommunityListResponse struct {
|
||||
PageResponse
|
||||
List []OutfitCommunityItem `json:"list"`
|
||||
}
|
||||
|
||||
type OutfitCommunityItem struct {
|
||||
OutfitUUID string `json:"outfit_uuid"`
|
||||
OutfitName string `json:"outfit_name"`
|
||||
OutfitDesc string `json:"outfit_desc"`
|
||||
Scene string `json:"scene"`
|
||||
Source int8 `json:"source"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Items []OutfitItemResponse `json:"items"`
|
||||
OwnerName string `json:"owner_name"`
|
||||
OwnerAvatar string `json:"owner_avatar"`
|
||||
LikeCount int64 `json:"like_count"`
|
||||
CollectCount int64 `json:"collect_count"`
|
||||
}
|
||||
```
|
||||
|
||||
前端类型(新增到 `types/models.ts`):
|
||||
|
||||
```ts
|
||||
export interface CommunityOutfitItem {
|
||||
outfit_uuid: string
|
||||
outfit_name: string
|
||||
outfit_desc: string
|
||||
scene: string
|
||||
source: number
|
||||
created_at: number
|
||||
items: { clothes_uuid: string; processed_image: string }[]
|
||||
owner_name: string
|
||||
owner_avatar: string
|
||||
like_count: number
|
||||
collect_count: number
|
||||
}
|
||||
```
|
||||
|
||||
## 设计风格
|
||||
|
||||
延续项目既有 iOS HIG 视觉语言:分组灰底 #F2F2F7、纯白卡片(圆角 32rpx)、0.5px 发丝分隔线、系统蓝 #007AFF 强调色、SF Symbols 风格线性图标,清爽克制、以内容为中心,社区分段与现有分段无缝衔接。
|
||||
|
||||
## 分段控件
|
||||
|
||||
胶囊分段由两项扩展为三项「我的穿搭 / 已收藏 / 社区」,三项等宽;选中项为白色滑块 + 系统蓝加粗文字,未选中为 #3C3C43;切换带轻量过渡,样式沿用现有 .segment 规格(高 64rpx、圆角 16rpx)。
|
||||
|
||||
## 社区卡片
|
||||
|
||||
- 作者行(新增):左侧 56rpx 圆形头像(无头像回退浅灰底 icon-user 占位),昵称 26rpx 深色单行省略,右侧相对时间 22rpx 灰字
|
||||
- 封面区:复用现有拼图 cover-item 错落摆放(最多 5 件)+ 右上角件数胶囊
|
||||
- 信息区:穿搭名 32rpx 加粗单行省略,描述 26rpx 灰字最多两行
|
||||
- 互动区(meta 行):左侧场景标签(tag-gray)与件数,右侧点赞(icon-heart)与收藏(icon-star)计数,图标 36rpx 灰字 22rpx,间距与现有卡片一致
|
||||
|
||||
## 状态设计
|
||||
|
||||
加载态复用骨架屏(sk-card 呼吸动画);社区空态复用 empty-state(title「还没有公开的穿搭」,desc「把喜欢的穿搭公开到社区,让大家看到你的搭配」,无按钮);列表底部「没有更多了」提示沿用现有样式;卡片按压态用现有 active 透明度反馈。
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- Purpose: 后端仓库(117 个 Go 文件)实现社区接口前,跨文件梳理 like/collect 数据模型、批量计数与批量组装模式、DTO/Handler 既有写法,确保新代码完全复用现有模式
|
||||
- Expected outcome: 产出可复用的批量计数 SQL 模式与接口实现要点清单(表名、字段、既有工具函数位置),降低返工与不一致风险
|
||||
@@ -0,0 +1,270 @@
|
||||
---
|
||||
name: 纸条AI衣橱小程序UI全量重构设计稿
|
||||
overview: 为「纸条AI衣橱」微信小程序(18 个页面)重新设计一套「杂志风纯白留白 + 毛玻璃悬浮层、纯黑白灰零彩色」的苹果式简约高级 UI,并以单个自包含 HTML 文件交付可点击跳转、tab 可切换、弹窗可开关的高保真交互原型,完整覆盖现有全部功能与全部状态。
|
||||
design:
|
||||
architecture:
|
||||
framework: html
|
||||
styleKeywords:
|
||||
- 杂志编辑部风
|
||||
- 毛玻璃浮层
|
||||
- 零彩色灰阶
|
||||
- 大留白
|
||||
- 发丝线分隔
|
||||
- 衬线大标题
|
||||
- 悬浮胶囊标签栏
|
||||
- 苹果级缓动
|
||||
- 买手店线稿质感
|
||||
fontSystem:
|
||||
fontFamily: Songti SC / PingFang SC
|
||||
heading:
|
||||
size: 34px
|
||||
weight: 500
|
||||
subheading:
|
||||
size: 17px
|
||||
weight: 500
|
||||
body:
|
||||
size: 15px
|
||||
weight: 400
|
||||
colorSystem:
|
||||
primary:
|
||||
- "#0A0A0A"
|
||||
- "#1C1C1E"
|
||||
- "#2C2C2E"
|
||||
background:
|
||||
- "#FFFFFF"
|
||||
- "#F7F7F5"
|
||||
- "#EFEFED"
|
||||
text:
|
||||
- "#0A0A0A"
|
||||
- "#6E6E73"
|
||||
- "#AEAEB2"
|
||||
functional:
|
||||
- "#E5E5E7"
|
||||
- "#D2D2D7"
|
||||
- "#0A0A0A"
|
||||
- "#AEAEB2"
|
||||
todos:
|
||||
- id: baseline
|
||||
content: 用 [subagent:code-explorer] 核对 18 页区块、状态矩阵与微文案,产出页面清单基线
|
||||
status: completed
|
||||
- id: design-system
|
||||
content: 建立零彩色设计令牌、双字阶、设备壳、毛玻璃浮层与 SVG 图标衣物线稿组件库
|
||||
status: completed
|
||||
dependencies:
|
||||
- baseline
|
||||
- id: interaction-core
|
||||
content: 实现栈式路由、标签与分段切换、状态切换器、四类弹层控制器与左侧索引
|
||||
status: completed
|
||||
dependencies:
|
||||
- design-system
|
||||
- id: flow-closet
|
||||
content: 完成登录、衣橱网格、上传三态与衣物详情四态页面
|
||||
status: completed
|
||||
dependencies:
|
||||
- interaction-core
|
||||
- id: flow-outfit
|
||||
content: 完成穿搭三段式、穿搭详情、穿搭编辑双视图与 AI 推荐页
|
||||
status: completed
|
||||
dependencies:
|
||||
- interaction-core
|
||||
- id: flow-share
|
||||
content: 完成运势内页、分享列表、衣柜分享创建三视图与两个分享详情
|
||||
status: completed
|
||||
dependencies:
|
||||
- interaction-core
|
||||
- id: flow-profile
|
||||
content: 完成我的页、通知列表与通知详情、编辑资料底部抽屉
|
||||
status: completed
|
||||
dependencies:
|
||||
- interaction-core
|
||||
- id: flow-commerce
|
||||
content: 完成商城双视图与订单页,含支付确认弹窗与待支付倒计时
|
||||
status: completed
|
||||
dependencies:
|
||||
- interaction-core
|
||||
- id: spec-and-qa
|
||||
content: 补设计规范页,用 [skill:playwright-cli] 逐屏截图走查全链路与状态矩阵
|
||||
status: completed
|
||||
dependencies:
|
||||
- flow-closet
|
||||
- flow-outfit
|
||||
- flow-share
|
||||
- flow-profile
|
||||
- flow-commerce
|
||||
- id: deploy-preview
|
||||
content: 用 [integration:cloudStudio] 部署原型,产出手机可预览链接
|
||||
status: completed
|
||||
dependencies:
|
||||
- spec-and-qa
|
||||
---
|
||||
|
||||
## 产品概述
|
||||
|
||||
「纸条 AI 衣橱」是一款智能衣橱与穿搭小程序,覆盖衣物管理、AI 穿搭推荐、每日运势、好友分享、会员额度与订单等完整业务。本次对全部 18 个页面做整体 UI 重构,产出可点击走通全流程的单文件交互原型,整体基调为「苹果简约高级」。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- 登录与新手礼包引导
|
||||
- 衣橱:分类浏览、拍照上传衣物(含 AI 识别进度)、衣物详情与 15 项属性编辑
|
||||
- 穿搭:我的穿搭 / 已收藏 / 社区三段浏览,创建与编辑穿搭,AI 推荐生成与保存
|
||||
- 运势:星座今日运势、四项评分、幸运色与幸运数字
|
||||
- 分享:衣柜分享与穿搭分享的创建、查看、只读浏览与失效态
|
||||
- 我的:资料与额度概览、通知、商城会员与额度包、订单管理
|
||||
- 全局:5 个底部标签、骨架屏 / 空态 / 异常态、弹窗与底部抽屉、未读红点
|
||||
|
||||
## 视觉要求
|
||||
|
||||
- 采用「浅色为底 + 玻璃浮层」:内容区像杂志内页,纯白纸面、衬线大标题、超大留白、发丝分隔线,衣物图片是唯一主角;导航栏、底部标签、弹窗与抽屉用毛玻璃悬浮在内容之上
|
||||
- 零彩色:完全用黑白灰层次表达信息与状态,唯一色彩来自衣物本身与幸运色
|
||||
- 页面间可真实点击跳转、标签可切换、弹窗可开关,能像真机一样走完整流程
|
||||
- 重构不只是换皮,需按产品设计逻辑重排信息架构、层级与交互反馈
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 交付物:单个自包含 HTML 文件,路径 `/root/zt-closet-wxmini-frontend/stitch_ai_ui_v2/index.html`(新建目录,不覆盖旧 `stitch_ai_ui/`)
|
||||
- 纯原生 HTML5 + CSS3(CSS 自定义属性设计令牌、Grid/Flex、backdrop-filter、clip-path)+ 原生 ES6 JavaScript,无框架、无构建、无包管理
|
||||
- 零外部依赖:不使用 Tailwind CDN / FontAwesome / Google Fonts(旧稿的致命问题:离线打开即掉样式),图标全部用内联 `<svg><symbol>` sprite,字体走系统字体栈
|
||||
- 图片占位:内联 SVG 灰度线稿(衬衫 / 裤装 / 连衣裙 / 外套 / 鞋 / 包 / 配饰剪影),既离线可用又天然契合零彩色调性
|
||||
- 验证:用 playwright-cli 以 `file://` 打开原型,逐屏截图并点击走查
|
||||
|
||||
## 实现方案
|
||||
|
||||
核心策略:**一套设计令牌 + 一个页面容器 + 一个路由栈 + 一个状态机**,让 18 屏共享同一套视觉与交互内核,避免逐页重造。
|
||||
|
||||
1. **设计系统先行**:用 CSS 变量固化零彩色灰阶、衬线/无衬线双字阶、间距刻度、发丝线、玻璃材质、软阴影与统一缓动曲线。所有页面只消费令牌,禁止硬编码颜色。
|
||||
2. **页面容器**:18 屏写成 `<section class="screen" data-screen="wardrobe">`,默认 `display:none`,由路由控制显隐;5 个 tab 屏共享同一个底部玻璃 tabBar(提到 screen 之外,随 tab 路由显隐),与真实小程序 `custom-tab-bar` 的共享方式一致。
|
||||
3. **栈式路由**:`navigate(screen, params)` / `back()`,维护页面栈以支持真实返回;`params` 驱动同一屏的变体(`owner=1|0` 决定穿搭详情是属主态还是访客态、`readonly=true` 决定衣物详情是否隐藏编辑删除、`tab=flow` 决定商城进入额度明细视图)。
|
||||
4. **状态机**:每屏内用 `data-state`(`skeleton | empty | list | loading | error`)切换,右侧状态切换器直接改这个属性,一套标记覆盖完整状态矩阵,无需为每个状态写重复结构。
|
||||
5. **统一弹层**:四类 —— Modal(居中玻璃卡)、Sheet(底部抽屉带抓手)、ActionSheet(iOS 底部动作表)、Toast(深色玻璃胶囊),全部由同一 overlay 控制器管理,保证「可开关」。
|
||||
6. **真实跳转**:所有可点元素声明 `data-nav="clothes-detail"` + `data-params='{"uuid":"c1"}'`,在 `#app` 上做事件委托统一处理,保证能像真机一样点着走。
|
||||
|
||||
关键取舍:
|
||||
|
||||
- **不引 Tailwind CDN**:离线可用 + 杂志风排版需要精确控制字距、行高、发丝线、玻璃透明度,原子类反而增加噪音。代价是 CSS 体积更大 → 用「令牌 + 语义类」收敛,禁止逐页堆裸样式。
|
||||
- **不引字体 CDN**:中文衬线用 `"Songti SC", "STSong", "Noto Serif SC", serif` 回退栈,拉丁回退 `Georgia`,保证任何环境打开都不掉字。
|
||||
- **占位图用 SVG 线稿而非图床外链**:后端 `processed_image` 在原型中取不到,外链图会离线失效且引入杂色;灰度线稿的代价是不如实拍图真实,通过大量留白与统一构图补足质感。
|
||||
- **单文件而非多文件**:用户明确要「单文件可点击交互原型」。代价是文件较大 → 用分节注释 + 页面名前缀类名组织。
|
||||
|
||||
性能与可靠性:
|
||||
|
||||
- 单文件无网络请求,首屏即渲染,无加载抖动。`backdrop-filter` 是唯一较重属性 → 限制每屏玻璃层数量(导航栏 + 底部栏/浮层,最多 2 层),避免大面积模糊导致滚动掉帧。
|
||||
- 事件委托统一在 `#app` 上,避免 18 屏 × N 元素逐个绑定监听。
|
||||
- 页面切换只切 `display`,不重建 DOM,保证跳转瞬时响应。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["原型外壳:左索引 + iPhone 设备框 + 右状态切换器"] --> B["路由层:navigate / back / params 页面栈"]
|
||||
B --> C["页面层:18 个 section.screen + data-state 状态机"]
|
||||
B --> D["浮层层:tabBar / Modal / Sheet / ActionSheet / Toast"]
|
||||
C --> E["组件层:按钮 / 分段 / 胶囊 / 列表行 / 规格行 / 卡片 / 骨架 / 空态"]
|
||||
D --> E
|
||||
E --> F["令牌层:零彩色灰阶 / 双字阶 / 间距 / 发丝线 / 玻璃 / 阴影 / 缓动"]
|
||||
C --> G["素材层:内联 SVG symbol 图标 + 衣物线稿占位插画"]
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
/root/zt-closet-wxmini-frontend/
|
||||
├── stitch_ai_ui_v2/ # [NEW] 新版设计稿目录(旧 stitch_ai_ui/ 原样保留)
|
||||
│ └── index.html # [NEW] 单文件可点击交互原型,内部按以下分节组织:
|
||||
│ # <style>
|
||||
│ # §1 设计令牌:零彩色灰阶 / 双字阶 / 间距刻度 / 圆角 / 发丝线 / 玻璃 / 阴影 / 缓动
|
||||
│ # §2 基础层:reset、排版类(.display-serif / .eyebrow / .num)、发丝线工具类
|
||||
│ # §3 外壳层:工作台背景、左索引面板、iPhone 375×812 设备框、状态栏、微信胶囊、安全区
|
||||
│ # §4 玻璃层:.glass-nav / .glass-tabbar / .glass-sheet / .glass-modal / .toast
|
||||
│ # §5 组件层:按钮(实心黑/描边/文字)、分段控件、胶囊标签、列表行、规格行、统计数字条、评分点阵、进度轨、衣物卡、穿搭拼图卡、分享卡、订单卡、骨架屏、空态、徽标
|
||||
│ # §6 页面层:18 屏专属样式,类名带页面前缀(.wd- / .of- / .cd- / .hs- / .cs- / .pf- / .up- / .od- / .ai- / .nt- / .ml- / .lg-)
|
||||
│ # <body>
|
||||
│ # 左索引面板:分组(登录 / 核心 Tab / 详情 / 表单流程 / 分享 / 交易 / 规范)
|
||||
│ # 设备框 → <main id="app"> 内含 18 个 <section class="screen">
|
||||
│ # 共享浮层容器:tabBar + overlay(Modal / Sheet / ActionSheet / Toast)
|
||||
│ # 右状态切换器:skeleton / empty / list / error / loading
|
||||
│ # 底部:色板与字阶规范展示
|
||||
│ # <script>
|
||||
│ # §A 数据:18 页 mock 数据(衣物、穿搭、运势、分享、商品、订单、通知)
|
||||
│ # §B 路由:navigate / back / 页面栈 / params 分发
|
||||
│ # §C 状态机:setState(screen, state)
|
||||
│ # §D 弹层控制器:openModal / openSheet / openActionSheet / toast
|
||||
│ # §E 交互:事件委托、tab 切换、分段控件、多选计数、点赞收藏乐观更新、倒计时
|
||||
│ # §F 引导:默认进入衣橱屏
|
||||
└── miniprogram/ # 只读参考,本次不修改任何文件
|
||||
├── app.json # 参考:18 页注册 + 5 tab 配置
|
||||
├── app.wxss # 参考:旧设计令牌(本次全量替换的依据)
|
||||
├── styles/icons.wxss # 参考:24 个图标的语义与命名
|
||||
├── types/models.ts # 参考:全部实体字段名
|
||||
├── custom-tab-bar/ # 参考:tab 结构 + 未读红点规则
|
||||
├── components/ # 参考:navigation-bar / clothes-card / empty-state
|
||||
└── pages/*/*.wxml # 参考:每页区块结构与微文案(唯一事实来源)
|
||||
```
|
||||
|
||||
## 执行注意
|
||||
|
||||
- **字段与文案以 `.wxml` / `.ts` 为唯一事实来源**(如「共 N 件 · 上传额度剩余 N 次」「AI 正在识别你的衣物…通常需要 10 秒左右」),不要凭印象造文案。
|
||||
- **零彩色纪律**:所有颜色只能取自令牌(`--paper` / `--paper-2` / `--ink` / `--ink-2` / `--ink-3` / `--hairline` / `--fill` / `--glass`),禁止出现任何有彩色 hex。唯一例外是幸运色的 8px 色点(业务数据)与衣物线稿本身的灰阶。
|
||||
- **旧状态色语义需在零彩色下重建**:进行中/强调 = 实心黑胶囊;中性 = 浅灰胶囊;失效/已读 = 降透明度或删除线;AI 来源 = 黑底白字描边胶囊;删除/取消 = 黑字下划线文字按钮,不依赖红色。
|
||||
- **小程序真实感三件套**:微信右上角胶囊(87×32pt)、状态栏 44px、底部安全区 `env(safe-area-inset-bottom)`,每一屏都必须体现。
|
||||
- **不修改 `miniprogram/` 下任何文件,不覆盖 `stitch_ai_ui/`**;本次只新增 `stitch_ai_ui_v2/index.html`。
|
||||
- 玻璃层下方滚动内容需预留 padding,避免内容被悬浮导航/底栏遮挡。
|
||||
|
||||
## 设计主题
|
||||
|
||||
「安静的展厅」—— 把衣橱当成一间高级买手店:内容区是一张纯白纸张,衣物图片是唯一主角,所有 UI 元素退到发丝线级别;导航、底部标签、弹窗做成一层悬浮在纸面上的毛玻璃。零彩色,靠字重对比、留白尺度、发丝线、灰阶层次与玻璃透明度建立秩序与高级感。
|
||||
|
||||
## 视觉语言
|
||||
|
||||
- **布局**:页面左右边距 20px;区块垂直间距 32px,块内 16px;正文行高 1.6~1.75;图片与文字之间永远留白,绝不贴边。卡片取消描边与投影,改用留白 + 发丝线分组,仅极少数关键卡保留极软阴影。
|
||||
- **双字阶**:衬线(Songti SC / STSong / Noto Serif SC / Georgia)用于页面主标题、数字大数与穿搭名,字重 400~500、负字距 -0.01em;无衬线(-apple-system / PingFang SC)用于正文、按钮、标签与导航。10~11px 大写英文 eyebrow 标签(`WARDROBE` / `AI STYLIST`),字距 0.16em、`--ink-3` 灰,是最关键的「高级感」装置。
|
||||
- **发丝线替代边框**:所有分隔用 0.5px `rgba(0,0,0,.08)`,圆角只在小尺寸控件上出现(胶囊、抽屉),大块内容保持直角,强化杂志感。
|
||||
- **玻璃材质**:`rgba(255,255,255,.72)` + `backdrop-filter: saturate(180%) blur(20px)` + 0.5px 发丝边。底部标签用悬浮胶囊(左右各留 16px、圆角 28px、距底 12px + 安全区),而非通栏。
|
||||
- **动效**:统一缓动 `cubic-bezier(.32,.72,0,1)`,时长 320~420ms。页面切换从右滑入 + 轻微缩放淡入;列表入场 40ms 递增 stagger;按压 `scale(.97)` + 透明度 0.6;弹层自底部滑入并带轻微回弹。
|
||||
- **图片处理**:衣物图统一用内联 SVG 灰度线稿,1:1 或 3:4 画幅,居中留白 12%,无边框、仅 4px 极小圆角,像买手店目录里的商品线稿;穿搭拼图封面用 1px 白缝的无缝网格。
|
||||
|
||||
## 色彩系统(零彩色灰阶语义)
|
||||
|
||||
`--ink` #0A0A0A(主文字 / 主按钮 / 选中态)→ `--ink-2` #6E6E73(次要文字 / 描述)→ `--ink-3` #AEAEB2(弱化 / 占位 / 已读)→ `--hairline` rgba(0,0,0,.08)(分隔)→ `--paper-2` #F7F7F5(页面底)→ `--paper` #FFFFFF(内容纸面)。唯一深色块用于商城会员卡(#1C1C1E),作为全稿视觉锚点。
|
||||
|
||||
## 导航与信息架构重构
|
||||
|
||||
全局自定义导航改为 Apple 式行为:进入页面时是 34px 左对齐衬线大标题 + eyebrow 小标签,向下滚动时收缩为居中玻璃小标题 —— 这是最大的高级感来源,也让每个 tab 页有杂志封面感。tabBar 从通栏改为悬浮玻璃胶囊,图标改 1.5px 细线性描边,选中态用实心黑 + 顶部 3px 短横线,不再使用彩色。
|
||||
|
||||
## 逐页重构要点
|
||||
|
||||
- **登录**:全屏纯白 + 居中线稿 Logo + 衬线品牌名「纸条 AI 衣橱」+ 灰字 slogan + 底部实心黑胶囊按钮;新手礼包改为居中玻璃卡 + 发丝线权益列表。
|
||||
- **衣橱**:大标题 + 右侧统计;分类由横滑胶囊改为下划线式极简分类条;网格去卡片容器,图下两行(衬线品名 + 灰色属性行);FAB 改黑底白玻璃圆钮。
|
||||
- **穿搭**:分段改下划线式文字分段;取消独立入口按钮行,把「创建穿搭」「AI 推荐」合并为 tabBar 上方的悬浮玻璃双按钮条;社区卡改瀑布流编辑排版,meta 用细描边图标 + tabular 数字。
|
||||
- **运势**:做成杂志内页 —— 巨大衬线星座名;综合运势用超大衬线数字 + 5 个细描边星;三项评分改横向发丝线刻度尺;长文本用衬线引文式排版;幸运色/数字改两栏发丝线小格。
|
||||
- **分享**:下划线分段;分享卡改横向条目(左 72×72 灰度拼图 + 右衬线标题/灰字有效期);失效项降至 40% 透明度 + 描边小角标;新建分享移到导航栏右侧「+」。
|
||||
- **我的**:大号头像 72px + 衬线昵称 + 一行灰字副信息;额度概览改四栏发丝线分隔数字条(大号 tabular 数字 + 10px eyebrow);功能列表改纯发丝线分组列表(行高 52px、右侧灰色值 + 极细箭头);会员权益用描边胶囊。
|
||||
- **上传**:idle 为巨大虚线发丝线方框 + 中央细描边相机 + 衬线引导语;processing 用三段发丝线进度轨(实心/空心圆点 + 1px 连线);failed 用极简「!」+ 衬线标题 + 双按钮。
|
||||
- **衣物详情**:属性展示改为规格表 —— 左标签(13px 灰)/ 右值(15px 黑)的发丝线行,分组标题用「中文 / 大写英文」eyebrow;量化评分改 5 个细描边方点;适用场合用无底色描边胶囊。
|
||||
- **穿搭详情 / AI 推荐 / 穿搭编辑**:封面用无缝白缝网格;信息区为衬线大标题 + eyebrow 来源标签 + 发丝线标签行 + 描述 + 横滑单品(80×80 无边框缩略图);底部操作改悬浮玻璃胶囊条。AI 生成态用呼吸感细描边圆环 + 衬线文案替代 spinner。
|
||||
- **商城 / 订单 / 通知 / 分享详情**:商城会员卡用唯一深色块,额度改四栏数字条,商品行左线稿图标 / 中名称描述 / 右 tabular 价格 + 黑胶囊「购买」;订单状态用描边胶囊 + 文案不依赖颜色,倒计时用 tabular 衬线数字;通知未读用左侧 4px 黑竖条 + 黑字标题;分享详情为分享者行 + 无缝拼图 + 只读网格 + 悬浮玻璃「分享给好友」黑胶囊。
|
||||
|
||||
## 交互与适配
|
||||
|
||||
- 所有列表覆盖「骨架屏 → 空态 → 正常列表 → 加载更多 → 没有更多了」;表单覆盖「默认 / 校验置灰 / 提交中 / 成功」;详情覆盖「正常 / 处理中 / 失败 / 失效 / 只读」。
|
||||
- 右侧状态切换器可即时切换任一屏的状态,左侧索引按「登录 / 核心 Tab / 详情 / 表单流程 / 分享 / 交易 / 规范」分组,点击直达。
|
||||
- 适配:以 375×812pt 为基准,设备框固定宽高、内容区独立滚动;底部安全区用 `env(safe-area-inset-bottom)` 模拟;工作台外层在窄屏下自动纵向堆叠,保证在桌面浏览器与手机浏览器都能正常预览。
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- Purpose: 在执行前核对 18 个页面的 UI 区块、状态矩阵、微文案与跳转关系,并盘点旧稿 `stitch_ai_ui/` 已覆盖的状态,形成页面清单基线。
|
||||
- Expected outcome: 一份可驱动左侧索引面板与右侧状态切换器的页面/状态清单,确保重构不漏功能、不漏状态。
|
||||
|
||||
### Skill
|
||||
|
||||
- **playwright-cli**
|
||||
- Purpose: 用无头浏览器以 `file://` 打开本地原型,逐屏截图、点击走查跳转链路、开关弹层与状态切换器。
|
||||
- Expected outcome: 18 屏截图 + 跳转链路验证结果,据此发现并修复渲染错位、玻璃层遮挡、路由断链等缺陷。
|
||||
|
||||
### Integration
|
||||
|
||||
- **cloudStudio**
|
||||
- Purpose: 将原型目录部署为可访问链接,便于在手机上真机预览毛玻璃与滚动效果。
|
||||
- Expected outcome: 一个可在手机浏览器/微信中打开预览的线上地址;若部署失败则以本地文件交付为准,不阻塞主交付物。
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": 1,
|
||||
"records": []
|
||||
}
|
||||
@@ -21,8 +21,8 @@
|
||||
],
|
||||
"tabBar": {
|
||||
"custom": true,
|
||||
"color": "#8E8E93",
|
||||
"selectedColor": "#007AFF",
|
||||
"color": "#AEAEB2",
|
||||
"selectedColor": "#0A0A0A",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"list": [
|
||||
{ "pagePath": "pages/wardrobe/wardrobe", "text": "衣橱" },
|
||||
@@ -35,7 +35,7 @@
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom",
|
||||
"backgroundColor": "#F2F2F7"
|
||||
"backgroundColor": "#FFFFFF"
|
||||
},
|
||||
"renderer": "webview",
|
||||
"style": "v2",
|
||||
|
||||
+792
-151
@@ -1,203 +1,844 @@
|
||||
/**app.wxss —— 全局设计令牌与通用样式**/
|
||||
/** app.wxss —— v2 设计令牌与通用样式:零彩色灰阶 / 毛玻璃浮层 / 杂志大留白 **/
|
||||
@import "styles/icons.wxss";
|
||||
|
||||
/* ============================================================================
|
||||
§1 设计令牌(唯一颜色与度量来源,页面禁止硬编码彩色)
|
||||
========================================================================== */
|
||||
page {
|
||||
--color-primary: #007AFF;
|
||||
--color-primary-aux: #5856D6;
|
||||
--color-pink: #FF2D55;
|
||||
--color-bg: #F2F2F7;
|
||||
--color-card: #FFFFFF;
|
||||
--color-hairline: #E5E5EA;
|
||||
--color-text-main: #000000;
|
||||
--color-text-secondary: #8E8E93;
|
||||
--color-text-tertiary: #C7C7CC;
|
||||
--color-green: #34C759;
|
||||
--color-orange: #FF9500;
|
||||
--color-red: #FF3B30;
|
||||
--color-yellow: #FFCC00;
|
||||
/* 墨阶 */
|
||||
--ink: #0A0A0A;
|
||||
--ink-2: #6E6E73;
|
||||
--ink-3: #AEAEB2;
|
||||
--ink-4: #C9C9CE;
|
||||
--art: #B9B9BE; /* 衣物线稿色 */
|
||||
/* 纸面 */
|
||||
--paper: #FFFFFF;
|
||||
--paper-2: #F7F7F5;
|
||||
--paper-3: #EFEFED;
|
||||
/* 发丝线与填充 */
|
||||
--hairline: rgba(0, 0, 0, .08);
|
||||
--hairline-2: rgba(0, 0, 0, .14);
|
||||
--fill: rgba(120, 120, 128, .07);
|
||||
--fill-2: rgba(120, 120, 128, .13);
|
||||
/* 深色块(全稿唯一视觉锚点:商城会员卡) */
|
||||
--block: #1C1C1E;
|
||||
--block-2: #2C2C2E;
|
||||
/* 玻璃 */
|
||||
--glass: rgba(255, 255, 255, .72);
|
||||
--glass-2: rgba(255, 255, 255, .86);
|
||||
--glass-dark: rgba(28, 28, 30, .82);
|
||||
/* 字族 */
|
||||
--serif: "Songti SC", "STSong", "Noto Serif SC", "Source Han Serif SC", Georgia, "Times New Roman", serif;
|
||||
--sans: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
/* 缓动 */
|
||||
--ease: cubic-bezier(.32, .72, 0, 1);
|
||||
--ease-out: cubic-bezier(.22, 1, .36, 1);
|
||||
/* 圆角:仅小控件与抽屉使用圆角,大块内容保持直角 */
|
||||
--r-img: 8rpx;
|
||||
--r-ctrl: 20rpx;
|
||||
--r-sheet: 40rpx;
|
||||
--r-pill: 999rpx;
|
||||
/* 度量 */
|
||||
--pad: 40rpx;
|
||||
--nav-h: 88rpx;
|
||||
--tab-h: 120rpx;
|
||||
--safe-b: 28rpx;
|
||||
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-main);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
font-size: 30rpx;
|
||||
line-height: 1.5;
|
||||
line-height: 1.62;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
/* ===== 布局 ===== */
|
||||
view,
|
||||
text,
|
||||
image,
|
||||
input,
|
||||
textarea,
|
||||
scroll-view,
|
||||
swiper,
|
||||
swiper-item {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
§2 排版 · 双字阶(衬线大标题 / 无衬线正文)
|
||||
========================================================================== */
|
||||
.eyebrow {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: .16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
line-height: 1;
|
||||
}
|
||||
.display {
|
||||
font-family: var(--serif);
|
||||
font-size: 68rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.012em;
|
||||
line-height: 1.14;
|
||||
}
|
||||
.t-xl { font-family: var(--serif); font-size: 52rpx; font-weight: 500; letter-spacing: -.01em; line-height: 1.22; }
|
||||
.t-lg { font-family: var(--serif); font-size: 40rpx; font-weight: 500; letter-spacing: -.005em; line-height: 1.32; }
|
||||
.t-md { font-size: 34rpx; font-weight: 600; line-height: 1.38; }
|
||||
.t-bd { font-size: 30rpx; line-height: 1.62; }
|
||||
.t-sm { font-size: 26rpx; line-height: 1.55; color: var(--ink-2); }
|
||||
.t-xs { font-size: 22rpx; line-height: 1.5; color: var(--ink-3); }
|
||||
.t-quote { font-family: var(--serif); font-size: 32rpx; line-height: 1.85; letter-spacing: .005em; color: var(--ink); }
|
||||
.num { font-variant-numeric: tabular-nums; }
|
||||
.c-ink { color: var(--ink); }
|
||||
.c-2 { color: var(--ink-2); }
|
||||
.c-3 { color: var(--ink-3); }
|
||||
.c-4 { color: var(--ink-4); }
|
||||
.ell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ============================================================================
|
||||
§3 布局 · 页面容器 / 页头 / 间距 / 发丝线
|
||||
========================================================================== */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg);
|
||||
box-sizing: border-box;
|
||||
background: var(--paper);
|
||||
}
|
||||
/* 底部预留:悬浮标签栏 / 悬浮双按钮条 / 悬浮操作条遮挡避让 */
|
||||
.pb-tab { padding-bottom: calc(var(--tab-h) + 60rpx + env(safe-area-inset-bottom)); }
|
||||
.pb-float { padding-bottom: calc(var(--tab-h) + 220rpx + env(safe-area-inset-bottom)); }
|
||||
.pb-dock { padding-bottom: calc(190rpx + env(safe-area-inset-bottom)); }
|
||||
.pb-safe { padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); }
|
||||
|
||||
/* ===== 卡片 ===== */
|
||||
.card {
|
||||
background: var(--color-card);
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
.head { padding: 24rpx var(--pad) 0; }
|
||||
.head .eyebrow { margin-bottom: 20rpx; }
|
||||
.head-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 28rpx; }
|
||||
.head-meta {
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .02em;
|
||||
padding-bottom: 12rpx;
|
||||
text-align: right;
|
||||
line-height: 1.6;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.pad { padding-left: var(--pad); padding-right: var(--pad); }
|
||||
.spacer { flex: 1; }
|
||||
.center { display: flex; align-items: center; justify-content: center; }
|
||||
.stack { display: flex; flex-direction: column; }
|
||||
.sec { margin-top: 64rpx; }
|
||||
.hair { border-bottom: 1rpx solid var(--hairline); }
|
||||
.hair-t { border-top: 1rpx solid var(--hairline); }
|
||||
.gap-8 { height: 8rpx; }
|
||||
.gap-16 { height: 16rpx; }
|
||||
.gap-24 { height: 24rpx; }
|
||||
.gap-32 { height: 32rpx; }
|
||||
.gap-48 { height: 48rpx; }
|
||||
.gap-64 { height: 64rpx; }
|
||||
.safe-bottom { padding-bottom: env(safe-area-inset-bottom); }
|
||||
|
||||
/* ===== 发丝分隔线 ===== */
|
||||
.hairline {
|
||||
border-bottom: 1rpx solid var(--color-hairline);
|
||||
}
|
||||
.hairline-t {
|
||||
border-top: 1rpx solid var(--color-hairline);
|
||||
}
|
||||
|
||||
/* ===== 文本 ===== */
|
||||
.t-main { color: var(--color-text-main); }
|
||||
.t-sub { color: var(--color-text-secondary); }
|
||||
.t-weak { color: var(--color-text-tertiary); }
|
||||
.t-primary { color: var(--color-primary); }
|
||||
.t-danger { color: var(--color-red); }
|
||||
.t-green { color: var(--color-green); }
|
||||
.t-orange { color: var(--color-orange); }
|
||||
|
||||
.fs-title { font-size: 68rpx; font-weight: 700; }
|
||||
.fs-h1 { font-size: 40rpx; font-weight: 700; }
|
||||
.fs-h2 { font-size: 34rpx; font-weight: 600; }
|
||||
.fs-body { font-size: 30rpx; }
|
||||
.fs-caption { font-size: 26rpx; }
|
||||
.fs-note { font-size: 22rpx; }
|
||||
|
||||
.num { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ===== 按钮 ===== */
|
||||
/* ============================================================================
|
||||
§4 按钮 · 实心黑 / 描边 / 浅填充 / 玻璃 / 下划线文字
|
||||
========================================================================== */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
height: 100rpx;
|
||||
padding: 0 44rpx;
|
||||
border-radius: var(--r-pill);
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: .01em;
|
||||
white-space: nowrap;
|
||||
transition: transform .18s var(--ease), opacity .18s var(--ease), background .2s var(--ease);
|
||||
}
|
||||
.btn:active { transform: scale(.972); opacity: .74; }
|
||||
.btn--block { display: flex; width: 100%; }
|
||||
.btn--sm { height: 72rpx; padding: 0 30rpx; font-size: 27rpx; }
|
||||
.btn--ink { background: var(--ink); color: #fff; }
|
||||
.btn--ink.is-off { background: var(--fill-2); color: var(--ink-3); pointer-events: none; }
|
||||
.btn--ghost { box-shadow: inset 0 0 0 1rpx var(--hairline-2); color: var(--ink); }
|
||||
.btn--plain { background: var(--fill); color: var(--ink); }
|
||||
.btn--glass {
|
||||
background: var(--glass-2);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(22px);
|
||||
backdrop-filter: saturate(180%) blur(22px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 16rpx 48rpx -20rpx rgba(0, 0, 0, .18);
|
||||
color: var(--ink);
|
||||
}
|
||||
.btn--text {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: var(--ink);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 7rpx;
|
||||
text-decoration-thickness: 1rpx;
|
||||
text-decoration-color: var(--ink-4);
|
||||
}
|
||||
.btn--text:active { opacity: .5; transform: none; }
|
||||
.btn .icon { width: 34rpx; height: 34rpx; }
|
||||
|
||||
/* ============================================================================
|
||||
§5 胶囊标签 · 零彩色状态语义
|
||||
进行中/强调 = pill--ink;中性 = pill;失效/只读 = pill--line + 降透明
|
||||
========================================================================== */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
height: 46rpx;
|
||||
padding: 0 18rpx;
|
||||
border-radius: var(--r-pill);
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: .015em;
|
||||
background: var(--fill);
|
||||
color: var(--ink-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.pill--ink { background: var(--ink); color: #fff; }
|
||||
.pill--line { background: transparent; box-shadow: inset 0 0 0 1rpx var(--hairline-2); color: var(--ink-2); }
|
||||
.pill--plain { background: transparent; color: var(--ink-3); padding: 0 4rpx; }
|
||||
.pill--dim { opacity: .45; }
|
||||
.pill--dot::before {
|
||||
content: '';
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: .55;
|
||||
}
|
||||
.pill .icon { width: 24rpx; height: 24rpx; }
|
||||
|
||||
/* ============================================================================
|
||||
§6 列表 · 分组标题 / 行 / 规格表 / 数字条
|
||||
========================================================================== */
|
||||
.rows { background: var(--paper); }
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
min-height: 104rpx;
|
||||
width: 100%;
|
||||
padding: 0 var(--pad);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
transition: background .16s var(--ease);
|
||||
}
|
||||
.row:active { background: var(--fill); }
|
||||
.row-lb { flex: 1; font-size: 30rpx; line-height: 1.4; }
|
||||
.row-vl { font-size: 27rpx; color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
||||
.row-ch { width: 26rpx; height: 26rpx; color: var(--ink-4); flex-shrink: 0; }
|
||||
.row-ic { width: 38rpx; height: 38rpx; color: var(--ink-2); flex-shrink: 0; }
|
||||
.row-badge {
|
||||
min-width: 34rpx;
|
||||
height: 34rpx;
|
||||
padding: 0 10rpx;
|
||||
border-radius: 18rpx;
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
line-height: 34rpx;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.grp-hd { display: flex; align-items: baseline; gap: 18rpx; padding: 0 var(--pad) 22rpx; }
|
||||
.grp-hd .gh-cn { font-size: 26rpx; font-weight: 600; letter-spacing: .01em; color: var(--ink); }
|
||||
.grp-hd .gh-en { font-size: 19rpx; font-weight: 600; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-3); }
|
||||
.grp-line { height: 1rpx; background: var(--hairline); margin: 0 var(--pad) 8rpx; }
|
||||
|
||||
.spec {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 28rpx;
|
||||
padding: 22rpx var(--pad);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
.spec-k { width: 160rpx; flex-shrink: 0; font-size: 26rpx; color: var(--ink-2); line-height: 1.5; }
|
||||
.spec-v { flex: 1; font-size: 29rpx; line-height: 1.5; text-align: right; color: var(--ink); }
|
||||
.spec-v .dot {
|
||||
display: inline-block;
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 12rpx;
|
||||
vertical-align: -1rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, .14);
|
||||
}
|
||||
|
||||
.stats { display: flex; border-top: 1rpx solid var(--hairline); border-bottom: 1rpx solid var(--hairline); }
|
||||
.stat { flex: 1; padding: 28rpx 0 30rpx; text-align: center; position: relative; }
|
||||
.stat + .stat::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 28rpx;
|
||||
bottom: 30rpx;
|
||||
width: 1rpx;
|
||||
background: var(--hairline);
|
||||
}
|
||||
.stat-n {
|
||||
font-family: var(--serif);
|
||||
font-size: 48rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
letter-spacing: -.01em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.stat-n .unit { font-size: 24rpx; color: var(--ink-3); margin-left: 4rpx; }
|
||||
.stat-l {
|
||||
margin-top: 12rpx;
|
||||
font-size: 19rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: .12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
§7 进度轨 / 评分点阵 / 刻度尺
|
||||
========================================================================== */
|
||||
.track { height: 3rpx; background: var(--hairline-2); position: relative; overflow: hidden; }
|
||||
.track .bar { position: absolute; left: 0; top: 0; bottom: 0; background: var(--ink); }
|
||||
.track.is-warn .bar { background: var(--ink-2); }
|
||||
|
||||
.dots { display: inline-flex; gap: 10rpx; vertical-align: middle; }
|
||||
.dots .d { width: 16rpx; height: 16rpx; border-radius: 50%; box-shadow: inset 0 0 0 2rpx var(--ink-4); }
|
||||
.dots .d.on { background: var(--ink); box-shadow: none; }
|
||||
|
||||
.scale { position: relative; height: 52rpx; }
|
||||
.scale-line { position: absolute; left: 0; right: 0; top: 24rpx; height: 2rpx; background: var(--hairline-2); }
|
||||
.scale-fill { position: absolute; left: 0; top: 24rpx; height: 2rpx; background: var(--ink); }
|
||||
.scale-pip {
|
||||
position: absolute;
|
||||
top: 17rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--ink);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.scale-ticks {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 20rpx;
|
||||
height: 10rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.scale-ticks .tk { width: 1rpx; height: 10rpx; background: var(--hairline-2); }
|
||||
|
||||
/* ============================================================================
|
||||
§8 图片瓦片 · 拼图封面 · 勾选
|
||||
========================================================================== */
|
||||
.tile-img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: 24rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
}
|
||||
.btn-primary {
|
||||
height: 96rpx;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.btn-primary:active { opacity: 0.7; }
|
||||
|
||||
.btn-secondary {
|
||||
height: 88rpx;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-main);
|
||||
}
|
||||
.btn-secondary:active { opacity: 0.7; }
|
||||
|
||||
.btn-outline {
|
||||
height: 88rpx;
|
||||
border: 1rpx solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
background: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-outline:active { opacity: 0.7; }
|
||||
|
||||
.btn-danger-text {
|
||||
color: var(--color-red);
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 输入框 ===== */
|
||||
.input {
|
||||
height: 88rpx;
|
||||
background: var(--color-bg);
|
||||
border-radius: 20rpx;
|
||||
padding: 0 24rpx;
|
||||
.tile-img .ph { width: 56%; height: 56%; color: var(--art); }
|
||||
.tile-img.is-empty .ph { width: 34%; height: 34%; color: var(--ink-4); }
|
||||
.tile-img .pic { width: 100%; height: 100%; }
|
||||
.tile-name {
|
||||
margin-top: 18rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 28rpx;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.textarea {
|
||||
background: var(--color-bg);
|
||||
.tile-sub {
|
||||
margin-top: 4rpx;
|
||||
font-size: 21rpx;
|
||||
color: var(--ink-3);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.tile-flag {
|
||||
position: absolute;
|
||||
top: 14rpx;
|
||||
left: 14rpx;
|
||||
height: 38rpx;
|
||||
padding: 0 14rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
box-sizing: border-box;
|
||||
background: var(--glass-2);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
font-size: 19rpx;
|
||||
font-weight: 500;
|
||||
line-height: 38rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
/* ===== 胶囊标签 ===== */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
.tile-flag.is-ink { background: var(--ink); color: #fff; }
|
||||
.tile-check {
|
||||
position: absolute;
|
||||
right: 14rpx;
|
||||
bottom: 14rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, .9), 0 0 0 1rpx rgba(0, 0, 0, .12);
|
||||
background: rgba(255, 255, 255, .55);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 22rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
transition: background .18s var(--ease), color .18s var(--ease);
|
||||
}
|
||||
.tag-blue { background: rgba(0, 122, 255, 0.1); color: #007AFF; }
|
||||
.tag-gray { background: #F2F2F7; color: #8E8E93; }
|
||||
.tag-green { background: rgba(52, 199, 89, 0.12); color: #34C759; }
|
||||
.tag-orange { background: rgba(255, 149, 0, 0.12); color: #FF9500; }
|
||||
.tag-red { background: rgba(255, 59, 48, 0.12); color: #FF3B30; }
|
||||
.tag-indigo { background: rgba(88, 86, 214, 0.12); color: #5856D6; }
|
||||
.tile-check .icon { width: 24rpx; height: 24rpx; opacity: 0; }
|
||||
.is-picked .tile-check { background: var(--ink); }
|
||||
.is-picked .tile-check .icon { opacity: 1; }
|
||||
.is-picked .tile-img { box-shadow: inset 0 0 0 3rpx var(--ink); }
|
||||
|
||||
/* ===== 骨架屏 ===== */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #F2F2F7 25%, #E5E5EA 37%, #F2F2F7 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: skeleton-loading 1.4s ease infinite;
|
||||
.collage {
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 2rpx;
|
||||
background: var(--paper);
|
||||
border-radius: var(--r-img);
|
||||
overflow: hidden;
|
||||
aspect-ratio: 1 / 1;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
@keyframes skeleton-loading {
|
||||
.collage--1 { grid-template-columns: 1fr; aspect-ratio: 1 / 1; }
|
||||
.collage--2 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr; aspect-ratio: 2 / 1; }
|
||||
.collage--3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
|
||||
.collage--3 .cg:first-child { grid-row: span 2; }
|
||||
.collage .cg {
|
||||
position: relative;
|
||||
background: var(--paper-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.collage .cg .ph { width: 44%; height: 44%; color: var(--art); }
|
||||
.collage .cg .pic { width: 100%; height: 100%; }
|
||||
.cg-more {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
.cg-badge {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
bottom: 16rpx;
|
||||
height: 42rpx;
|
||||
padding: 0 16rpx;
|
||||
border-radius: 22rpx;
|
||||
background: var(--glass-2);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
backdrop-filter: blur(12px);
|
||||
font-size: 21rpx;
|
||||
line-height: 42rpx;
|
||||
color: var(--ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
§9 骨架屏 · 空态 · 页脚提示
|
||||
========================================================================== */
|
||||
.sk {
|
||||
background: linear-gradient(90deg, #F2F2F0 25%, #E9E9E6 37%, #F2F2F0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: skLoad 1.5s ease infinite;
|
||||
}
|
||||
@keyframes skLoad {
|
||||
0% { background-position: 100% 50%; }
|
||||
100% { background-position: 0 50%; }
|
||||
}
|
||||
.sk-img { width: 100%; aspect-ratio: 1 / 1; border-radius: var(--r-img); }
|
||||
.sk-ln { height: 20rpx; border-radius: 10rpx; }
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 128rpx 88rpx 0;
|
||||
}
|
||||
.empty-ic { width: 104rpx; height: 104rpx; color: var(--ink-4); margin-bottom: 40rpx; }
|
||||
.empty-t { font-family: var(--serif); font-size: 38rpx; font-weight: 500; letter-spacing: -.005em; }
|
||||
.empty-d { margin-top: 16rpx; font-size: 26rpx; line-height: 1.7; color: var(--ink-2); max-width: 480rpx; }
|
||||
.empty-b { margin-top: 48rpx; }
|
||||
.foot-note { padding: 52rpx 0 16rpx; text-align: center; font-size: 22rpx; color: var(--ink-4); letter-spacing: .06em; }
|
||||
|
||||
/* ============================================================================
|
||||
§10 分段条 / 分类条 · 下划线式极简
|
||||
========================================================================== */
|
||||
.seg { display: flex; gap: 36rpx; padding: 0 var(--pad); white-space: nowrap; }
|
||||
.seg-item {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
padding: 22rpx 0 24rpx;
|
||||
font-size: 29rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .005em;
|
||||
transition: color .2s var(--ease);
|
||||
}
|
||||
.seg-item.is-on { color: var(--ink); font-weight: 500; }
|
||||
.seg-item.is-on::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 3rpx;
|
||||
background: var(--ink);
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
.seg-scroll { white-space: nowrap; }
|
||||
|
||||
/* ============================================================================
|
||||
§11 悬浮层 · 底部操作条 / 双按钮条 / FAB
|
||||
========================================================================== */
|
||||
.dock {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 45;
|
||||
padding: 24rpx var(--pad) calc(28rpx + env(safe-area-inset-bottom));
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(22px);
|
||||
backdrop-filter: saturate(180%) blur(22px);
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
}
|
||||
.dock-row { display: flex; align-items: center; gap: 24rpx; }
|
||||
.dock-ic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-2);
|
||||
font-variant-numeric: tabular-nums;
|
||||
transition: color .18s var(--ease), transform .18s var(--ease);
|
||||
}
|
||||
.dock-ic .icon { width: 42rpx; height: 42rpx; }
|
||||
.dock-ic.is-on { color: var(--ink); }
|
||||
.dock-ic:active { transform: scale(.92); }
|
||||
|
||||
.float-duo {
|
||||
position: fixed;
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
bottom: calc(var(--tab-h) + 48rpx + env(safe-area-inset-bottom));
|
||||
z-index: 45;
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
}
|
||||
.float-duo .btn { flex: 1; height: 92rpx; font-size: 28rpx; }
|
||||
|
||||
/* ===== 悬浮按钮 ===== */
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: 180rpx;
|
||||
right: 40rpx;
|
||||
bottom: calc(var(--tab-h) + 56rpx + env(safe-area-inset-bottom));
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.35);
|
||||
z-index: 20;
|
||||
box-shadow: 0 20rpx 44rpx -16rpx rgba(0, 0, 0, .45);
|
||||
z-index: 46;
|
||||
transition: transform .18s var(--ease), opacity .18s var(--ease);
|
||||
}
|
||||
.fab:active { opacity: 0.8; }
|
||||
.fab .icon { width: 44rpx; height: 44rpx; }
|
||||
.fab:active { transform: scale(.94); opacity: .8; }
|
||||
|
||||
/* ===== 底部安全区 ===== */
|
||||
.safe-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
/* ============================================================================
|
||||
§12 表单 · 输入框 / 文本域 / 选择行
|
||||
========================================================================== */
|
||||
.input {
|
||||
height: 96rpx;
|
||||
background: var(--fill);
|
||||
border-radius: var(--r-ctrl);
|
||||
padding: 0 28rpx;
|
||||
font-size: 29rpx;
|
||||
width: 100%;
|
||||
}
|
||||
.textarea {
|
||||
background: var(--fill);
|
||||
border-radius: var(--r-ctrl);
|
||||
padding: 22rpx 28rpx;
|
||||
font-size: 29rpx;
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
}
|
||||
.input-ph { color: var(--ink-3); }
|
||||
|
||||
/* ===== 通用列表行 ===== */
|
||||
.list-row {
|
||||
/* ============================================================================
|
||||
§13 通用动效
|
||||
========================================================================== */
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translateY(16rpx); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.fade-up { animation: fadeUp .42s var(--ease) both; }
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: .35; transform: scale(.96); }
|
||||
50% { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
.pulse { animation: pulse 1.6s var(--ease) infinite; }
|
||||
|
||||
/* ============================================================================
|
||||
§14 导航右侧操作(供 navigation-bar 的 right 插槽内容使用)
|
||||
========================================================================== */
|
||||
.nav-action {
|
||||
font-size: 28rpx;
|
||||
color: var(--ink-2);
|
||||
letter-spacing: .01em;
|
||||
white-space: nowrap;
|
||||
padding: 12rpx 4rpx;
|
||||
transition: color .16s var(--ease), opacity .16s var(--ease);
|
||||
}
|
||||
.nav-action:active { opacity: .5; }
|
||||
.nav-action--ink { color: var(--ink); font-weight: 500; }
|
||||
.nav-more {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
color: var(--ink);
|
||||
}
|
||||
.list-row:active { background: #F2F2F7; }
|
||||
.nav-more .icon { width: 40rpx; height: 40rpx; }
|
||||
|
||||
/* ===== 1:1 方图占位 ===== */
|
||||
.square {
|
||||
position: relative;
|
||||
/* ============================================================================
|
||||
§15 底部抽屉(页面级 Sheet,含抓手 / 标题行 / 滚动体 / 底部操作区)
|
||||
========================================================================== */
|
||||
.scrim {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 70;
|
||||
background: rgba(0, 0, 0, .28);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: fadeIn .28s var(--ease) both;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.sheet-wrap {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 71;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.sheet {
|
||||
width: 100%;
|
||||
background: #F2F2F7;
|
||||
border-radius: 24rpx;
|
||||
max-height: 86vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--r-sheet) var(--r-sheet) 0 0;
|
||||
overflow: hidden;
|
||||
background: var(--glass-2);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
border-top: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 -36rpx 100rpx -40rpx rgba(0, 0, 0, .28);
|
||||
animation: sheetIn .46s var(--ease) both;
|
||||
}
|
||||
@keyframes sheetIn {
|
||||
from { transform: translateY(101%); }
|
||||
to { transform: none; }
|
||||
}
|
||||
.sheet-grab {
|
||||
width: 72rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 4rpx;
|
||||
background: var(--ink-4);
|
||||
margin: 16rpx auto 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sheet-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx var(--pad) 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sheet-hd .sheet-t { font-size: 32rpx; font-weight: 600; letter-spacing: -.005em; }
|
||||
.sheet-hd .sheet-x { font-size: 28rpx; color: var(--ink-2); }
|
||||
.sheet-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.sheet-foot {
|
||||
padding: 24rpx var(--pad) calc(32rpx + env(safe-area-inset-bottom));
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
§16 衣物处理过程(上传页处理中态 / 衣物详情处理中态共用)
|
||||
========================================================================== */
|
||||
.up-preview {
|
||||
margin: 52rpx var(--pad) 0;
|
||||
aspect-ratio: 4 / 5;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.square::after {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-bottom: 100%;
|
||||
.up-preview .ph { width: 44%; height: 44%; color: var(--art); }
|
||||
.up-preview .pic { width: 100%; height: 100%; }
|
||||
|
||||
.up-steps {
|
||||
padding: 64rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.up-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28rpx;
|
||||
position: relative;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
.up-step:last-child { padding-bottom: 0; }
|
||||
/* 竖向发丝线连接 */
|
||||
.up-step::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 9rpx;
|
||||
top: 26rpx;
|
||||
bottom: 0;
|
||||
width: 1rpx;
|
||||
background: var(--hairline-2);
|
||||
}
|
||||
.up-step:last-child::before { display: none; }
|
||||
.up-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: var(--paper);
|
||||
box-shadow: inset 0 0 0 2rpx var(--ink-4);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.up-step.is-done .up-dot { background: var(--ink); box-shadow: none; }
|
||||
.up-step.is-on .up-dot {
|
||||
box-shadow: inset 0 0 0 2rpx var(--ink);
|
||||
animation: dotPulse 1.8s var(--ease) infinite;
|
||||
}
|
||||
@keyframes dotPulse {
|
||||
0%, 100% { box-shadow: inset 0 0 0 2rpx var(--ink), 0 0 0 0 rgba(10, 10, 10, .18); }
|
||||
55% { box-shadow: inset 0 0 0 2rpx var(--ink), 0 0 0 12rpx rgba(10, 10, 10, 0); }
|
||||
}
|
||||
.up-lb { font-size: 27rpx; color: var(--ink-3); letter-spacing: .01em; }
|
||||
.up-step.is-done .up-lb,
|
||||
.up-step.is-on .up-lb { color: var(--ink); }
|
||||
|
||||
/* 失败态「!」 */
|
||||
.up-bang {
|
||||
width: 92rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 44rpx;
|
||||
box-shadow: inset 0 0 0 1rpx var(--hairline-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--serif);
|
||||
font-size: 46rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.up-duo { display: flex; gap: 20rpx; padding: 56rpx var(--pad) 0; }
|
||||
.up-duo .btn { flex: 1; }
|
||||
|
||||
/* ============================================================================
|
||||
§17 居中玻璃弹窗卡(页面级自定义弹窗,如登录新手礼包)
|
||||
========================================================================== */
|
||||
.modal-wrap {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 72;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 68rpx;
|
||||
}
|
||||
.modal {
|
||||
width: 100%;
|
||||
max-width: 576rpx;
|
||||
border-radius: var(--r-sheet);
|
||||
overflow: hidden;
|
||||
background: var(--glass-2);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 48rpx 120rpx -36rpx rgba(0, 0, 0, .32);
|
||||
animation: modalIn .42s var(--ease) both;
|
||||
}
|
||||
@keyframes modalIn {
|
||||
from { opacity: 0; transform: scale(.94); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.modal-body { padding: 44rpx 40rpx 36rpx; text-align: center; }
|
||||
.modal-title {
|
||||
font-family: var(--serif);
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.005em;
|
||||
}
|
||||
.modal-text {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.modal-acts { display: flex; border-top: 1rpx solid var(--hairline); }
|
||||
.modal-act {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: var(--ink-2);
|
||||
transition: background .16s var(--ease);
|
||||
}
|
||||
.modal-act:active { background: var(--fill); }
|
||||
.modal-act + .modal-act { border-left: 1rpx solid var(--hairline); }
|
||||
.modal-act--main { color: var(--ink); font-weight: 600; }
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<view class="clothes-card {{selected ? 'selected' : ''}}" bindtap="onTap">
|
||||
<view class="clothes-img">
|
||||
<!-- v2 衣物瓦片:无卡片容器,图下两行(衬线品名 + 灰色属性行) -->
|
||||
<view class="tile {{selected ? 'is-picked' : ''}}" bindtap="onTap">
|
||||
<view class="tile-img">
|
||||
<image
|
||||
wx:if="{{item.processed_image}}"
|
||||
class="clothes-img-img"
|
||||
class="pic"
|
||||
src="{{item.processed_image}}"
|
||||
mode="aspectFill"
|
||||
lazy-load
|
||||
/>
|
||||
<view wx:else class="clothes-placeholder">
|
||||
<view class="icon icon-hanger"></view>
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
<view wx:if="{{statusText}}" class="tile-flag {{statusType === 'ink' ? 'is-ink' : ''}}">{{statusText}}</view>
|
||||
<view wx:if="{{selectable}}" class="tile-check">
|
||||
<view class="icon icon-check"></view>
|
||||
</view>
|
||||
<view wx:if="{{statusText}}" class="clothes-status tag tag-{{statusType}}">{{statusText}}</view>
|
||||
</view>
|
||||
<view class="clothes-name">{{item.name || '未命名衣物'}}</view>
|
||||
<view class="clothes-sub">{{item.category_main}}{{item.category_sub ? ' · ' + item.category_sub : ''}}{{item.color_main ? ' · ' + item.color_main : ''}}</view>
|
||||
<view wx:if="{{selectable}}" class="clothes-check {{selected ? 'on' : ''}}">
|
||||
<view wx:if="{{selected}}" class="icon icon-check"></view>
|
||||
</view>
|
||||
<view class="tile-name">{{item.name || '未命名衣物'}}</view>
|
||||
<view class="tile-sub">{{item.category_main}}{{item.category_sub ? ' · ' + item.category_sub : ''}}{{item.color_main ? ' · ' + item.color_main : ''}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,91 +1,5 @@
|
||||
.clothes-card {
|
||||
/** 衣物瓦片:视觉全部来自全局令牌与 .tile-* 通用类,此处只做定位兜底 **/
|
||||
.tile {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.clothes-card.selected {
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.clothes-img {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background: #F2F2F7;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.clothes-img-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.clothes-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.clothes-placeholder .icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.clothes-status {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
}
|
||||
|
||||
.clothes-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #1C1C1E;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clothes-sub {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 4rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clothes-check {
|
||||
position: absolute;
|
||||
top: 32rpx;
|
||||
right: 32rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #C7C7CC;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clothes-check.on {
|
||||
background: #007AFF;
|
||||
border-color: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.clothes-check .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<!-- v2 空态:细描边线稿图标 + 衬线标题 + 灰字描述 + 黑胶囊/描边按钮 -->
|
||||
<view class="empty">
|
||||
<view class="empty-icon">
|
||||
<view class="icon icon-lg {{icon}}"></view>
|
||||
</view>
|
||||
<view class="empty-title">{{title}}</view>
|
||||
<view wx:if="{{desc}}" class="empty-desc">{{desc}}</view>
|
||||
<view class="icon icon-xl {{icon}} empty-ic"></view>
|
||||
<view class="empty-t">{{title}}</view>
|
||||
<view wx:if="{{desc}}" class="empty-d">{{desc}}</view>
|
||||
<view
|
||||
wx:if="{{buttonText}}"
|
||||
class="empty-btn btn {{buttonType === 'outline' ? 'btn-outline' : 'btn-primary'}}"
|
||||
class="empty-btn btn btn--sm {{buttonType === 'outline' ? 'btn--ghost' : 'btn--ink'}}"
|
||||
hover-class="none"
|
||||
bindtap="onTap"
|
||||
>{{buttonText}}</view>
|
||||
</view>
|
||||
|
||||
@@ -1,37 +1,4 @@
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 48rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: #E8F2FF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #007AFF;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.empty-desc {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/** 空态:视觉来自全局 .empty* 与 .btn* 通用类 **/
|
||||
.empty-btn {
|
||||
margin-top: 40rpx;
|
||||
width: 360rpx;
|
||||
min-width: 280rpx;
|
||||
}
|
||||
|
||||
@@ -5,78 +5,43 @@ const LOG = 'navigation-bar'
|
||||
|
||||
Component({
|
||||
options: {
|
||||
multipleSlots: true // 在组件定义时的选项中启用多slot支持
|
||||
multipleSlots: true, // 右侧操作槽(如「编辑」「取消」「更多」)
|
||||
},
|
||||
/**
|
||||
* 组件的属性列表
|
||||
*/
|
||||
properties: {
|
||||
extClass: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
// 滚动收缩后显示的居中标题
|
||||
title: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
value: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
value: ''
|
||||
value: '',
|
||||
},
|
||||
// 是否显示返回按钮
|
||||
back: {
|
||||
type: Boolean,
|
||||
value: true
|
||||
value: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
value: false
|
||||
// 返回的页面深度
|
||||
delta: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
homeButton: {
|
||||
// 是否已滚动(由页面 utils/nav.ts 的 handler 驱动)
|
||||
scrolled: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
animated: {
|
||||
// 显示隐藏的时候opacity动画效果
|
||||
type: Boolean,
|
||||
value: true
|
||||
},
|
||||
show: {
|
||||
// 显示隐藏导航,隐藏的时候navigation-bar的高度占位还在
|
||||
type: Boolean,
|
||||
value: true,
|
||||
observer: '_showChange'
|
||||
},
|
||||
// back为true的时候,返回的页面深度
|
||||
delta: {
|
||||
type: Number,
|
||||
value: 1
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的初始数据
|
||||
*/
|
||||
data: {
|
||||
displayStyle: '',
|
||||
ios: false,
|
||||
innerPaddingRight: '',
|
||||
leftWidth: '',
|
||||
safeAreaTop: ''
|
||||
// 状态栏高度(px)与微信胶囊占位(右侧 padding)
|
||||
safeTop: 0,
|
||||
capsulePad: '',
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const rect = wx.getMenuButtonBoundingClientRect()
|
||||
const apply = (windowWidth: number, platform: string, safeTop: number) => {
|
||||
const isAndroid = platform === 'android'
|
||||
const isDevtools = platform === 'devtools'
|
||||
const apply = (windowWidth: number, safeTop: number) => {
|
||||
this.setData({
|
||||
ios: !isAndroid,
|
||||
innerPaddingRight: `padding-right: ${windowWidth - rect.left}px`,
|
||||
leftWidth: `width: ${windowWidth - rect.left}px`,
|
||||
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${safeTop}px); padding-top: ${safeTop}px` : ``
|
||||
safeTop: safeTop > 0 ? safeTop : 0,
|
||||
// 预留右上角胶囊宽度,保证右侧操作不压到胶囊
|
||||
capsulePad: `padding-right: ${Math.max(windowWidth - rect.left, 0)}px;`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,35 +50,18 @@ Component({
|
||||
if (typeof wxAny.getWindowInfo === 'function') {
|
||||
const win = wxAny.getWindowInfo()
|
||||
const dev = typeof wxAny.getDeviceInfo === 'function' ? wxAny.getDeviceInfo() : {}
|
||||
apply(win.windowWidth, dev.platform || win.platform || '', win.safeArea ? win.safeArea.top : 0)
|
||||
apply(win.windowWidth, win.safeArea ? win.safeArea.top : 0)
|
||||
logger.debug(LOG, '布局计算完成, 窗口宽=', win.windowWidth, '平台=', dev.platform || win.platform)
|
||||
return
|
||||
}
|
||||
// 低版本基础库走废弃接口兜底
|
||||
logger.warn(LOG, 'getWindowInfo 不可用,降级到已废弃的 wx.getSystemInfo')
|
||||
wx.getSystemInfo({
|
||||
success: (res) => apply(res.windowWidth, res.platform, res.safeArea ? res.safeArea.top : 0)
|
||||
success: (res) => apply(res.windowWidth, res.safeArea ? res.safeArea.top : 0),
|
||||
})
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 组件的方法列表
|
||||
*/
|
||||
methods: {
|
||||
_showChange(show: boolean) {
|
||||
const animated = this.data.animated
|
||||
let displayStyle = ''
|
||||
if (animated) {
|
||||
displayStyle = `opacity: ${
|
||||
show ? '1' : '0'
|
||||
};transition:opacity 0.5s;`
|
||||
} else {
|
||||
displayStyle = `display: ${show ? '' : 'none'}`
|
||||
}
|
||||
this.setData({
|
||||
displayStyle
|
||||
})
|
||||
},
|
||||
back() {
|
||||
const data = this.data
|
||||
if (data.delta) {
|
||||
@@ -124,17 +72,17 @@ Component({
|
||||
logger.debug(LOG, '返回上一页, delta=', data.delta)
|
||||
wx.navigateBack({
|
||||
delta: data.delta,
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg)
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
})
|
||||
} else {
|
||||
logger.warn(LOG, '页面栈不足,switchTab 回衣橱兜底, 栈深=', pages.length)
|
||||
wx.switchTab({
|
||||
url: '/pages/wardrobe/wardrobe',
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg)
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
|
||||
})
|
||||
}
|
||||
}
|
||||
this.triggerEvent('back', { delta: data.delta }, {})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,64 +1,27 @@
|
||||
<view class="weui-navigation-bar {{extClass}}">
|
||||
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
|
||||
|
||||
<!-- 左侧按钮 -->
|
||||
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
|
||||
<block wx:if="{{back || homeButton}}">
|
||||
<!-- 返回上一页 -->
|
||||
<block wx:if="{{back}}">
|
||||
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
|
||||
<view class="nav-root">
|
||||
<!-- 悬浮玻璃导航:滚动收缩后显示居中标题 -->
|
||||
<view class="nav {{scrolled ? 'is-glass' : ''}}">
|
||||
<!-- 高度含状态栏:全局 box-sizing 为 border-box,需显式把状态栏高度算进总高 -->
|
||||
<view class="nav-bar" style="{{capsulePad}} height: calc(88rpx + {{safeTop}}px); padding-top: {{safeTop}}px;">
|
||||
<view class="nav-left">
|
||||
<view
|
||||
bindtap="back"
|
||||
class="weui-navigation-bar__btn_goback_wrapper"
|
||||
hover-class="weui-active"
|
||||
hover-stay-time="100"
|
||||
wx:if="{{back}}"
|
||||
class="nav-back"
|
||||
hover-class="nav-press"
|
||||
hover-stay-time="80"
|
||||
aria-role="button"
|
||||
aria-label="返回"
|
||||
bindtap="back"
|
||||
>
|
||||
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
|
||||
<view class="icon icon-chevron-left"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<!-- 返回首页 -->
|
||||
<block wx:if="{{homeButton}}">
|
||||
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
|
||||
<view
|
||||
bindtap="home"
|
||||
class="weui-navigation-bar__btn_home_wrapper"
|
||||
hover-class="weui-active"
|
||||
aria-role="button"
|
||||
aria-label="首页"
|
||||
>
|
||||
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<slot name="left"></slot>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class='weui-navigation-bar__center'>
|
||||
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
|
||||
<view
|
||||
class="weui-loading"
|
||||
aria-role="img"
|
||||
aria-label="加载中"
|
||||
></view>
|
||||
</view>
|
||||
<block wx:if="{{title}}">
|
||||
<text>{{title}}</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<slot name="center"></slot>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 右侧留空 -->
|
||||
<view class='weui-navigation-bar__right'>
|
||||
<view class="nav-title {{scrolled ? 'is-on' : ''}}">{{title}}</view>
|
||||
<view class="nav-right">
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 占位:悬浮导航不参与文档流,用等高占位撑开内容 -->
|
||||
<view class="nav-holder" style="height: calc(88rpx + {{safeTop}}px);"></view>
|
||||
</view>
|
||||
|
||||
@@ -1,96 +1,94 @@
|
||||
.weui-navigation-bar {
|
||||
--weui-FG-0:rgba(0,0,0,.9);
|
||||
--height: 44px;
|
||||
--left: 16px;
|
||||
}
|
||||
.weui-navigation-bar .android {
|
||||
--height: 48px;
|
||||
}
|
||||
|
||||
.weui-navigation-bar {
|
||||
overflow: hidden;
|
||||
color: var(--weui-FG-0);
|
||||
/** v2 导航:透明 → 滚动后毛玻璃,大标题在页面内容区 **/
|
||||
.nav-root {
|
||||
position: relative;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__inner {
|
||||
position: relative;
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: calc(var(--height) + env(safe-area-inset-top));
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
border-bottom: 1rpx solid transparent;
|
||||
transition: background .3s cubic-bezier(.32, .72, 0, 1), border-color .3s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
|
||||
.nav.is-glass {
|
||||
background: var(--glass, rgba(255, 255, 255, .72));
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom-color: var(--hairline, rgba(0, 0, 0, .08));
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: relative;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 40rpx;
|
||||
}
|
||||
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
margin-left: -14rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: env(safe-area-inset-top);
|
||||
color: var(--ink, #0A0A0A);
|
||||
transition: background .16s cubic-bezier(.32, .72, 0, 1), transform .16s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
|
||||
.nav-back .icon {
|
||||
width: 38rpx;
|
||||
height: 38rpx;
|
||||
}
|
||||
|
||||
.nav-press {
|
||||
background: var(--fill, rgba(120, 120, 128, .07));
|
||||
transform: scale(.94);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
height: 88rpx;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 320rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: -.005em;
|
||||
opacity: 0;
|
||||
transition: opacity .26s cubic-bezier(.32, .72, 0, 1);
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nav-title.is-on {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 悬浮导航不参与文档流,用等高占位撑开内容(高度由内联样式给出) */
|
||||
.nav-holder {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__left {
|
||||
position: relative;
|
||||
padding-left: var(--left);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback_wrapper {
|
||||
padding: 11px 18px 11px 16px;
|
||||
margin: -11px -18px -11px -16px;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback_wrapper.weui-active {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__btn_goback {
|
||||
font-size: 12px;
|
||||
width: 12px;
|
||||
height: 24px;
|
||||
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
|
||||
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
|
||||
-webkit-mask-size: cover;
|
||||
mask-size: cover;
|
||||
background-color: var(--weui-FG-0);
|
||||
}
|
||||
|
||||
.weui-navigation-bar__center {
|
||||
font-size: 17px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.weui-navigation-bar__loading {
|
||||
margin-right: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.weui-loading {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
|
||||
background-size: 100%;
|
||||
margin-left: 0;
|
||||
animation: loading linear infinite 1s;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
flex: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'v2-overlay'
|
||||
|
||||
/**
|
||||
* v2 统一玻璃弹层。
|
||||
*
|
||||
* 三类形态:
|
||||
* modal 居中玻璃卡(确认/提示),buttons 决定按钮,事件 confirm/cancel
|
||||
* actionsheet 底部动作表(选择),items 决定条目,事件 select/cancel
|
||||
* toast 深色玻璃胶囊,duration 后自动关闭并抛出 close
|
||||
*
|
||||
* 页面侧只维护 visible 与文案数据,回调分支与原生 wx.showModal 一一对应。
|
||||
*/
|
||||
Component({
|
||||
properties: {
|
||||
visible: { type: Boolean, value: false },
|
||||
type: { type: String, value: 'modal' },
|
||||
title: { type: String, value: '' },
|
||||
content: { type: String, value: '' },
|
||||
// modal 按钮:[{ text, value, type: 'main' | 'normal' }]
|
||||
buttons: { type: Array, value: [] },
|
||||
// actionsheet 条目:['拍照', '从相册选择'] 或 [{ text, value }]
|
||||
items: { type: Array, value: [] },
|
||||
cancelText: { type: String, value: '取消' },
|
||||
// toast 自动关闭时长(ms)
|
||||
duration: { type: Number, value: 1800 },
|
||||
},
|
||||
|
||||
observers: {
|
||||
visible(v: boolean) {
|
||||
if (!v) {
|
||||
this.clearTimer()
|
||||
return
|
||||
}
|
||||
if (this.data.type === 'toast') this.startTimer()
|
||||
},
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
detached() {
|
||||
this.clearTimer()
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
startTimer() {
|
||||
this.clearTimer()
|
||||
const t = setTimeout(() => {
|
||||
logger.debug(LOG, 'toast 自动关闭')
|
||||
this.triggerEvent('close')
|
||||
}, this.data.duration)
|
||||
;(this as any).timer = t
|
||||
},
|
||||
|
||||
clearTimer() {
|
||||
const t = (this as any).timer
|
||||
if (t) {
|
||||
clearTimeout(t)
|
||||
;(this as any).timer = null
|
||||
}
|
||||
},
|
||||
|
||||
onBtn(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = Number((e.currentTarget.dataset as any).index) || 0
|
||||
const btn = (this.data.buttons[index] || {}) as any
|
||||
logger.debug(LOG, 'modal 按钮点击, index=', index, 'value=', btn.value)
|
||||
this.triggerEvent('confirm', { index, value: btn.value || '' })
|
||||
},
|
||||
|
||||
onItem(e: WechatMiniprogram.TouchEvent) {
|
||||
const index = Number((e.currentTarget.dataset as any).index) || 0
|
||||
const raw = this.data.items[index] as any
|
||||
const value = raw && typeof raw === 'object' ? raw.value || '' : raw
|
||||
logger.debug(LOG, 'actionsheet 条目点击, index=', index, 'value=', value)
|
||||
this.triggerEvent('select', { index, value })
|
||||
},
|
||||
|
||||
onCancel() {
|
||||
logger.debug(LOG, '弹层取消')
|
||||
this.triggerEvent('cancel')
|
||||
},
|
||||
|
||||
// 阻断蒙层下的页面滚动
|
||||
noop() {},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
<!-- v2 玻璃弹层:居中玻璃卡 Modal / 底部动作表 ActionSheet / 深色玻璃胶囊 Toast -->
|
||||
<view wx:if="{{visible}}" class="ov-root" catchtouchmove="noop">
|
||||
<!-- 居中玻璃卡 -->
|
||||
<block wx:if="{{type === 'modal'}}">
|
||||
<view class="scrim"></view>
|
||||
<view class="modal-wrap">
|
||||
<view class="modal">
|
||||
<view class="modal-body">
|
||||
<view wx:if="{{title}}" class="modal-title">{{title}}</view>
|
||||
<view wx:if="{{content}}" class="modal-text">{{content}}</view>
|
||||
</view>
|
||||
<view class="modal-acts">
|
||||
<view
|
||||
wx:for="{{buttons}}"
|
||||
wx:key="index"
|
||||
class="modal-act {{item.type === 'main' ? 'modal-act--main' : ''}}"
|
||||
data-index="{{index}}"
|
||||
hover-class="ov-press"
|
||||
hover-stay-time="80"
|
||||
bindtap="onBtn"
|
||||
>{{item.text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 底部动作表 -->
|
||||
<block wx:elif="{{type === 'actionsheet'}}">
|
||||
<view class="scrim" bindtap="onCancel"></view>
|
||||
<view class="as-wrap">
|
||||
<view class="as-card">
|
||||
<view wx:if="{{title}}" class="as-hd">{{title}}</view>
|
||||
<view
|
||||
wx:for="{{items}}"
|
||||
wx:key="index"
|
||||
class="as-item"
|
||||
data-index="{{index}}"
|
||||
hover-class="ov-press"
|
||||
hover-stay-time="80"
|
||||
bindtap="onItem"
|
||||
>{{item.text || item}}</view>
|
||||
</view>
|
||||
<view class="as-cancel" bindtap="onCancel">{{cancelText}}</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 深色玻璃胶囊 -->
|
||||
<block wx:else>
|
||||
<view class="toast-wrap">
|
||||
<view class="toast">{{content}}</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,159 @@
|
||||
/** v2 弹层:毛玻璃 + 发丝线 + 苹果级缓动 **/
|
||||
.ov-root {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
}
|
||||
|
||||
.scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, .28);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
animation: ovFade .28s cubic-bezier(.32, .72, 0, 1) both;
|
||||
}
|
||||
@keyframes ovFade {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.ov-press { opacity: .6; }
|
||||
|
||||
/* Modal */
|
||||
.modal-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 68rpx;
|
||||
pointer-events: none;
|
||||
}
|
||||
.modal {
|
||||
width: 100%;
|
||||
max-width: 576rpx;
|
||||
border-radius: var(--r-sheet, 40rpx);
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 48rpx 120rpx -36rpx rgba(0, 0, 0, .32);
|
||||
animation: ovModalIn .42s cubic-bezier(.32, .72, 0, 1) both;
|
||||
}
|
||||
@keyframes ovModalIn {
|
||||
from { opacity: 0; transform: scale(.94); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.modal-body { padding: 44rpx 40rpx 36rpx; text-align: center; }
|
||||
.modal-title {
|
||||
font-family: var(--serif, serif);
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.005em;
|
||||
color: var(--ink, #0A0A0A);
|
||||
}
|
||||
.modal-text {
|
||||
margin-top: 16rpx;
|
||||
font-size: 26rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--ink-2, #6E6E73);
|
||||
}
|
||||
.modal-acts { display: flex; border-top: 1rpx solid var(--hairline, rgba(0, 0, 0, .08)); }
|
||||
.modal-act {
|
||||
flex: 1;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: var(--ink-2, #6E6E73);
|
||||
transition: background .16s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
.modal-act + .modal-act { border-left: 1rpx solid var(--hairline, rgba(0, 0, 0, .08)); }
|
||||
.modal-act--main { color: var(--ink, #0A0A0A); font-weight: 600; }
|
||||
|
||||
/* ActionSheet */
|
||||
.as-wrap {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 16rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.as-card {
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 -20rpx 80rpx -36rpx rgba(0, 0, 0, .3);
|
||||
animation: ovAsIn .4s cubic-bezier(.32, .72, 0, 1) both;
|
||||
}
|
||||
@keyframes ovAsIn {
|
||||
from { opacity: 0; transform: translateY(24rpx); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.as-hd {
|
||||
padding: 26rpx 32rpx;
|
||||
text-align: center;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-3, #AEAEB2);
|
||||
border-bottom: 1rpx solid var(--hairline, rgba(0, 0, 0, .08));
|
||||
}
|
||||
.as-item {
|
||||
height: 104rpx;
|
||||
line-height: 104rpx;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
color: var(--ink, #0A0A0A);
|
||||
border-bottom: 1rpx solid var(--hairline, rgba(0, 0, 0, .08));
|
||||
transition: background .16s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
.as-item:last-child { border-bottom: 0; }
|
||||
.as-cancel {
|
||||
margin-top: 16rpx;
|
||||
height: 104rpx;
|
||||
line-height: 104rpx;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: var(--ink, #0A0A0A);
|
||||
border-radius: 28rpx;
|
||||
background: rgba(255, 255, 255, .86);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(30px);
|
||||
backdrop-filter: saturate(180%) blur(30px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
}
|
||||
|
||||
/* Toast */
|
||||
.toast-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.toast {
|
||||
max-width: 460rpx;
|
||||
padding: 26rpx 40rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(28, 28, 30, .82);
|
||||
color: #fff;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
backdrop-filter: blur(24px);
|
||||
box-shadow: 0 36rpx 88rpx -32rpx rgba(0, 0, 0, .5);
|
||||
animation: ovToastIn .34s cubic-bezier(.32, .72, 0, 1) both;
|
||||
}
|
||||
@keyframes ovToastIn {
|
||||
from { opacity: 0; transform: scale(.92) translateY(12rpx); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
<view class="tab-bar">
|
||||
<!-- v2 悬浮玻璃胶囊标签栏 -->
|
||||
<view class="tabbar">
|
||||
<view
|
||||
wx:for="{{list}}"
|
||||
wx:key="pagePath"
|
||||
class="tab-item {{selected === index ? 'active' : ''}}"
|
||||
class="tab-item {{selected === index ? 'is-on' : ''}}"
|
||||
data-path="{{item.pagePath}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="switchTab"
|
||||
>
|
||||
<view class="tab-icon-wrap">
|
||||
<view class="icon tab-icon {{item.icon}}"></view>
|
||||
<view class="tab-ic-wrap">
|
||||
<view class="icon tab-ic {{item.icon}}"></view>
|
||||
<view wx:if="{{item.icon === 'icon-user' && unread > 0}}" class="tab-badge">{{unread > 99 ? '99+' : unread}}</view>
|
||||
</view>
|
||||
<text class="tab-text">{{item.text}}</text>
|
||||
<text class="tab-tx">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,58 +1,87 @@
|
||||
.tab-bar {
|
||||
/** v2 悬浮玻璃胶囊标签栏:左右留边、距底 + 安全区,选中态实心黑 + 顶部短横线 **/
|
||||
.tabbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
left: 32rpx;
|
||||
right: 32rpx;
|
||||
bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
height: 120rpx;
|
||||
z-index: 50;
|
||||
border-radius: 60rpx;
|
||||
background: rgba(255, 255, 255, .72);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(22px);
|
||||
backdrop-filter: saturate(180%) blur(22px);
|
||||
border: 1rpx solid rgba(255, 255, 255, .7);
|
||||
box-shadow: 0 0 0 1rpx rgba(0, 0, 0, .05), 0 20rpx 60rpx -20rpx rgba(0, 0, 0, .16);
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12rpx 0 8rpx;
|
||||
color: #8E8E93;
|
||||
gap: 6rpx;
|
||||
color: var(--ink-3, #AEAEB2);
|
||||
transition: color .2s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #007AFF;
|
||||
.tab-ic {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
}
|
||||
|
||||
.tab-icon-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -20rpx;
|
||||
min-width: 28rpx;
|
||||
height: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 14rpx;
|
||||
background: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 28rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
.tab-tx {
|
||||
font-size: 20rpx;
|
||||
letter-spacing: .02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 选中态短横线 */
|
||||
.tab-item::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 12rpx;
|
||||
left: 50%;
|
||||
width: 28rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
background: var(--ink, #0A0A0A);
|
||||
transform: translateX(-50%) scaleX(.3);
|
||||
opacity: 0;
|
||||
transition: opacity .24s cubic-bezier(.32, .72, 0, 1), transform .24s cubic-bezier(.32, .72, 0, 1);
|
||||
}
|
||||
|
||||
.tab-item.is-on {
|
||||
color: var(--ink, #0A0A0A);
|
||||
}
|
||||
|
||||
.tab-item.is-on::after {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
}
|
||||
|
||||
.tab-ic-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 未读徽标:零彩色黑底白字数字胶囊 */
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
left: calc(50% + 6rpx);
|
||||
min-width: 30rpx;
|
||||
height: 30rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--ink, #0A0A0A);
|
||||
color: #fff;
|
||||
font-size: 18rpx;
|
||||
font-weight: 600;
|
||||
line-height: 30rpx;
|
||||
text-align: center;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state"
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { AiRecommendResult, ClothesItem } from '../../types/models'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -14,8 +15,24 @@ Page({
|
||||
result: null as AiRecommendResult | null,
|
||||
clothes: [] as ClothesItem[],
|
||||
generating: false,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// v2 玻璃弹层
|
||||
confirmVisible: false,
|
||||
confirmButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '开始生成', value: 'ok', type: 'main' },
|
||||
],
|
||||
limitVisible: false,
|
||||
limitButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '去购买', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
noop() {},
|
||||
|
||||
selectDaily() {
|
||||
@@ -42,16 +59,20 @@ Page({
|
||||
|
||||
startGenerate(sceneType: number, scene: string) {
|
||||
if (this.data.generating) return
|
||||
wx.showModal({
|
||||
title: 'AI 穿搭推荐',
|
||||
content: '本次将消耗 1 次 AI 推荐额度,生成后可保存到我的穿搭',
|
||||
confirmText: '开始生成',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) this.doGenerate(sceneType, scene)
|
||||
// 待生成的场景参数暂存到实例,确认后再发起
|
||||
;(this as any).pendingScene = { sceneType, scene }
|
||||
this.setData({ confirmVisible: true })
|
||||
},
|
||||
fail: (e) => logger.debug(LOG, '取消生成确认, err=', e && e.errMsg),
|
||||
})
|
||||
|
||||
onConfirmCancel() {
|
||||
this.setData({ confirmVisible: false })
|
||||
},
|
||||
|
||||
onConfirmGenerate(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ confirmVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
const pending = (this as any).pendingScene || { sceneType: 1, scene: '' }
|
||||
this.doGenerate(pending.sceneType, pending.scene)
|
||||
},
|
||||
|
||||
doGenerate(sceneType: number, scene: string) {
|
||||
@@ -82,7 +103,8 @@ Page({
|
||||
logger.warn(LOG, 'AI 推荐生成失败, code=', code)
|
||||
if (code === 90400) {
|
||||
this.setData({ state: 'empty' })
|
||||
} else if (code === 90401) {
|
||||
} else if (code === 90401 || code === 80000) {
|
||||
// 90401=AI 推荐次数上限;80000=额度不足兜底,统一弹窗引导购买
|
||||
this.setData({ state: 'scene', showSceneSheet: true })
|
||||
this.showLimitModal()
|
||||
} else if (code === 10003) {
|
||||
@@ -95,20 +117,19 @@ Page({
|
||||
},
|
||||
|
||||
showLimitModal() {
|
||||
wx.showModal({
|
||||
title: 'AI 推荐额度不足',
|
||||
content: '购买额度包后可继续生成 AI 穿搭推荐',
|
||||
confirmText: '去购买',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ limitVisible: true })
|
||||
},
|
||||
|
||||
onLimitCancel() {
|
||||
this.setData({ limitVisible: false })
|
||||
},
|
||||
|
||||
onLimitConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ limitVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
wx.navigateTo({
|
||||
url: '/pages/mall/mall',
|
||||
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '额度弹窗失败, err=', e && e.errMsg),
|
||||
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
<view class="page rec-page">
|
||||
<navigation-bar title="AI 穿搭推荐" />
|
||||
<wxs module="cl">
|
||||
function cls(n) {
|
||||
if (n >= 4) return ''
|
||||
if (n === 3) return 'collage--3'
|
||||
if (n === 2) return 'collage--2'
|
||||
return 'collage--1'
|
||||
}
|
||||
module.exports = { cls: cls }
|
||||
</wxs>
|
||||
|
||||
<!-- 生成中 -->
|
||||
<view wx:if="{{state === 'generating'}}" class="generating">
|
||||
<view class="gen-ani">
|
||||
<view class="icon icon-shirt gen-icon"></view>
|
||||
<view class="gen-ring"></view>
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="AI 穿搭推荐" scrolled="{{navScrolled}}" />
|
||||
|
||||
<!-- 生成中:呼吸感细描边圆环 + 衬线文案 -->
|
||||
<view wx:if="{{state === 'generating'}}" class="ai-gen">
|
||||
<view class="ai-ring">
|
||||
<view class="ai-ring-core">
|
||||
<view class="icon icon-sparkles"></view>
|
||||
</view>
|
||||
<view class="gen-title">AI 正在为你搭配…</view>
|
||||
<view class="gen-desc">结合今日运势与你的衣橱</view>
|
||||
</view>
|
||||
<view class="ai-gen-t">AI 正在为你搭配…</view>
|
||||
<view class="ai-gen-d">结合今日运势与你的衣橱</view>
|
||||
</view>
|
||||
|
||||
<!-- 衣橱为空 -->
|
||||
@@ -22,54 +33,82 @@
|
||||
/>
|
||||
|
||||
<!-- 结果页 -->
|
||||
<scroll-view wx:elif="{{state === 'result'}}" scroll-y class="scroll-body">
|
||||
<view class="card result-cover">
|
||||
<view class="rc-item rc-{{index}}" wx:for="{{clothes}}" wx:key="clothes_uuid" wx:if="{{index < 5}}">
|
||||
<image wx:if="{{item.processed_image}}" class="rc-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger rc-placeholder"></view>
|
||||
<scroll-view wx:elif="{{state === 'result'}}" scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">AI Stylist</view>
|
||||
<view class="display">今日推荐</view>
|
||||
</view>
|
||||
|
||||
<!-- 无缝拼图封面 -->
|
||||
<view class="ai-cover">
|
||||
<view class="collage {{cl.cls(clothes.length)}}">
|
||||
<view class="cg" wx:for="{{clothes}}" wx:key="clothes_uuid" wx:if="{{index < 4}}">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view wx:if="{{clothes.length > 4}}" class="cg-more">+{{clothes.length - 4}}</view>
|
||||
<text class="cg-badge num">{{clothes.length}} 件</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card result-info">
|
||||
<view class="ri-title">{{result.outfit_name}}</view>
|
||||
<view class="ri-desc">{{result.outfit_desc}}</view>
|
||||
<view class="tag-row">
|
||||
<text wx:if="{{result.scene}}" class="tag tag-gray">{{result.scene}}</text>
|
||||
<text class="tag tag-indigo">AI 推荐</text>
|
||||
<!-- 信息区 -->
|
||||
<view class="ai-info">
|
||||
<view class="t-xl">{{result.outfit_name}}</view>
|
||||
<view class="ai-tags">
|
||||
<text wx:if="{{result.scene}}" class="pill pill--line">{{result.scene}}</text>
|
||||
<text class="pill pill--ink">AI 推荐</text>
|
||||
</view>
|
||||
<view class="t-bd ai-desc">{{result.outfit_desc}}</view>
|
||||
</view>
|
||||
|
||||
<view class="items-title">推荐单品</view>
|
||||
<scroll-view scroll-x class="items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="items-row">
|
||||
<view class="item-chip" wx:for="{{clothes}}" wx:key="clothes_uuid">
|
||||
<image wx:if="{{item.processed_image}}" class="item-img" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="item-img item-img-empty"><view class="icon icon-hanger"></view></view>
|
||||
<text class="item-name">{{item.name || '未命名'}}</text>
|
||||
<!-- 推荐单品:横滑 -->
|
||||
<view class="ai-items">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">推荐单品</text>
|
||||
<text class="gh-en">Items</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="ai-items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="ai-items-row">
|
||||
<view class="ai-chip" wx:for="{{clothes}}" wx:key="clothes_uuid">
|
||||
<view class="ai-thumb">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<text class="ai-chip-n">{{item.name || '未命名'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="btn btn-primary" bindtap="onSave">保存到我的穿搭</view>
|
||||
<view class="btn btn-outline reroll" bindtap="onReroll">换一套</view>
|
||||
<view class="reroll-tip">换一套将再消耗 1 次推荐额度</view>
|
||||
</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 场景选择弹窗 -->
|
||||
<view wx:if="{{showSceneSheet}}" class="mask" bindtap="onSkipScene">
|
||||
<!-- 悬浮底部操作条 -->
|
||||
<view class="dock" wx:if="{{state === 'result'}}">
|
||||
<view class="ai-reroll-tip">换一套将再消耗 1 次推荐额度</view>
|
||||
<view class="dock-row">
|
||||
<view class="btn btn--ghost" bindtap="onReroll">换一套</view>
|
||||
<view class="btn btn--ink" bindtap="onSave">保存到我的穿搭</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 场景选择抽屉 -->
|
||||
<view wx:if="{{showSceneSheet}}" class="scrim" bindtap="onSkipScene"></view>
|
||||
<view wx:if="{{showSceneSheet}}" class="sheet-wrap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheet-grabber"></view>
|
||||
<view class="sheet-title">选择推荐场景</view>
|
||||
|
||||
<view class="scene-card {{sceneType === 1 ? 'active' : ''}}" bindtap="selectDaily">
|
||||
<view class="sheet-grab"></view>
|
||||
<view class="sheet-hd">
|
||||
<text class="sheet-t">选择推荐场景</text>
|
||||
<text class="sheet-x" bindtap="onSkipScene">跳过</text>
|
||||
</view>
|
||||
<view class="sheet-body">
|
||||
<view class="scene-card {{sceneType === 1 ? 'is-on' : ''}}" bindtap="selectDaily">
|
||||
<view class="scene-name">每日推荐</view>
|
||||
<view class="scene-desc">按今日运势与衣橱自动搭配</view>
|
||||
<view wx:if="{{sceneType === 1}}" class="icon icon-check scene-check"></view>
|
||||
</view>
|
||||
|
||||
<view class="scene-card {{sceneType === 2 ? 'active' : ''}}" bindtap="selectCustom">
|
||||
<view class="scene-card {{sceneType === 2 ? 'is-on' : ''}}" bindtap="selectCustom">
|
||||
<view class="scene-name">自定义场景</view>
|
||||
<view class="scene-desc">描述你想去的场合</view>
|
||||
<view wx:if="{{sceneType === 2}}" class="icon icon-check scene-check"></view>
|
||||
@@ -81,10 +120,34 @@
|
||||
value="{{scene}}"
|
||||
bindinput="onSceneInput"
|
||||
placeholder="例如:海边度假、周一例会、初次约会"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
</view>
|
||||
<view class="sheet-foot">
|
||||
<view class="btn btn--ink btn--block" bindtap="onStartGenerate">开始生成(消耗 1 次推荐额度)</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生成确认 -->
|
||||
<v2-overlay
|
||||
visible="{{confirmVisible}}"
|
||||
type="modal"
|
||||
title="AI 穿搭推荐"
|
||||
content="本次将消耗 1 次 AI 推荐额度,生成后可保存到我的穿搭"
|
||||
buttons="{{confirmButtons}}"
|
||||
bind:confirm="onConfirmGenerate"
|
||||
bind:cancel="onConfirmCancel"
|
||||
/>
|
||||
|
||||
<view class="btn btn-primary sheet-btn" bindtap="onStartGenerate">开始生成(消耗 1 次推荐额度)</view>
|
||||
<view class="sheet-skip" bindtap="onSkipScene">跳过,使用每日推荐</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 额度不足 -->
|
||||
<v2-overlay
|
||||
visible="{{limitVisible}}"
|
||||
type="modal"
|
||||
title="AI 推荐额度不足"
|
||||
content="购买额度包后可继续生成 AI 穿搭推荐"
|
||||
buttons="{{limitButtons}}"
|
||||
bind:confirm="onLimitConfirm"
|
||||
bind:cancel="onLimitCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.rec-page {
|
||||
/** AI 推荐:呼吸感圆环生成态 + 拼图结果卡 + 悬浮玻璃操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -10,272 +12,181 @@
|
||||
}
|
||||
|
||||
/* 生成中 */
|
||||
.generating {
|
||||
.ai-gen {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 64rpx;
|
||||
padding: 0 var(--pad) 120rpx;
|
||||
}
|
||||
|
||||
.gen-ani {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gen-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.gen-ring {
|
||||
position: absolute;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
.ai-ring {
|
||||
width: 216rpx;
|
||||
height: 216rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #E5E5EA;
|
||||
border-top-color: #007AFF;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.gen-title {
|
||||
margin-top: 48rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.gen-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 结果封面 */
|
||||
.result-cover {
|
||||
margin: 24rpx;
|
||||
height: 420rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rc-item {
|
||||
position: absolute;
|
||||
width: 180rpx;
|
||||
height: 240rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
box-shadow: inset 0 0 0 1rpx var(--hairline-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
animation: aiBreath 2.4s var(--ease) infinite;
|
||||
}
|
||||
|
||||
.rc-0 { left: 30rpx; top: 70rpx; transform: rotate(-5deg); }
|
||||
.rc-1 { left: 130rpx; top: 130rpx; transform: rotate(3deg); }
|
||||
.rc-2 { left: 240rpx; top: 60rpx; transform: rotate(6deg); }
|
||||
.rc-3 { left: 350rpx; top: 140rpx; transform: rotate(-3deg); }
|
||||
.rc-4 { left: 460rpx; top: 80rpx; transform: rotate(8deg); }
|
||||
|
||||
.rc-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rc-placeholder {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
/* 结果信息 */
|
||||
.result-info {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.ri-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.ri-desc {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 16rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.items-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.items-scroll {
|
||||
margin-top: 20rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-row {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.item-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.item-img-empty {
|
||||
.ai-ring-core {
|
||||
width: 152rpx;
|
||||
height: 152rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--paper-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.item-img-empty .icon {
|
||||
.ai-ring-core .icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 20rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
max-width: 140rpx;
|
||||
@keyframes aiBreath {
|
||||
0%, 100% { transform: scale(.96); opacity: .6; }
|
||||
50% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.ai-gen-t {
|
||||
margin-top: 64rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 42rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.005em;
|
||||
}
|
||||
|
||||
.ai-gen-d {
|
||||
margin-top: 18rpx;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
/* 结果 */
|
||||
.ai-cover {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.ai-info {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.ai-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.ai-desc {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.ai-items {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.ai-items-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ai-items-row {
|
||||
display: inline-flex;
|
||||
gap: 28rpx;
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
|
||||
.ai-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.ai-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ai-thumb .ph {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.ai-thumb .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ai-chip-n {
|
||||
margin-top: 14rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.reroll {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.reroll-tip {
|
||||
.ai-reroll-tip {
|
||||
padding-bottom: 20rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 16rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 场景弹窗 */
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sheet-grabber {
|
||||
width: 72rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 auto 24rpx;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
.dock-row .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 场景抽屉 */
|
||||
.scene-card {
|
||||
position: relative;
|
||||
border: 2rpx solid #E5E5EA;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
margin: 0 var(--pad) 20rpx;
|
||||
padding: 32rpx;
|
||||
border-radius: var(--r-ctrl);
|
||||
background: var(--fill);
|
||||
transition: background .18s var(--ease);
|
||||
}
|
||||
|
||||
.scene-card.active {
|
||||
border-color: #007AFF;
|
||||
background: rgba(0, 122, 255, 0.04);
|
||||
.scene-card.is-on {
|
||||
background: var(--paper);
|
||||
box-shadow: inset 0 0 0 1rpx var(--ink);
|
||||
}
|
||||
|
||||
.scene-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
font-family: var(--serif);
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.scene-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.scene-check {
|
||||
position: absolute;
|
||||
top: 28rpx;
|
||||
right: 28rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #007AFF;
|
||||
top: 32rpx;
|
||||
right: 32rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.scene-input {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.sheet-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sheet-skip {
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 20rpx;
|
||||
padding: 12rpx;
|
||||
margin: 0 var(--pad) 20rpx;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"clothes-card": "/components/clothes-card/clothes-card"
|
||||
"clothes-card": "/components/clothes-card/clothes-card",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { request } from '../../utils/request'
|
||||
import { ClothesItem, PagedResult, UserInfo } from '../../types/models'
|
||||
import { STORAGE_KEYS } from '../../utils/config'
|
||||
import { normalizePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -45,8 +46,21 @@ Page({
|
||||
nickname: '微信用户',
|
||||
avatar: '',
|
||||
selectedCount: 0,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 额度不足引导:v2 玻璃弹窗
|
||||
quotaVisible: false,
|
||||
quotaTitle: '衣柜分享额度不足',
|
||||
quotaText: '购买额度包后可创建衣柜分享',
|
||||
quotaButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '去购买', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
},
|
||||
@@ -182,6 +196,10 @@ Page({
|
||||
.catch((e: any) => {
|
||||
logger.warn(LOG, '分享创建失败, code=', e && e.code)
|
||||
this.setData({ submitting: false })
|
||||
// 衣柜分享额度不足:弹窗引导购买
|
||||
if (e && e.code === 80000) {
|
||||
this.setData({ quotaVisible: true })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -197,4 +215,17 @@ Page({
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onQuotaCancel() {
|
||||
this.setData({ quotaVisible: false })
|
||||
},
|
||||
|
||||
onQuotaConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ quotaVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
wx.navigateTo({
|
||||
url: '/pages/mall/mall',
|
||||
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,102 +1,143 @@
|
||||
<view class="page create-page">
|
||||
<navigation-bar title="创建分享" />
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="创建分享" scrolled="{{navScrolled}}" />
|
||||
|
||||
<!-- 配置视图 -->
|
||||
<scroll-view wx:if="{{view === 'config'}}" scroll-y class="scroll-body">
|
||||
<view class="section-title">分享方式</view>
|
||||
<view class="type-card card {{shareType === 1 ? 'active' : ''}}" bindtap="selectPartial">
|
||||
<view class="type-name">部分衣物</view>
|
||||
<view class="type-desc">挑选几件衣物分享给好友</view>
|
||||
<view wx:if="{{shareType === 1}}" class="icon icon-check type-check"></view>
|
||||
<scroll-view wx:if="{{view === 'config'}}" scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">New Share</view>
|
||||
<view class="display">创建分享</view>
|
||||
</view>
|
||||
|
||||
<view class="grp-hd csc-grp"><text class="gh-cn">分享方式</text><text class="gh-en">Type</text></view>
|
||||
<view class="csc-types">
|
||||
<view class="csc-type {{shareType === 1 ? 'is-on' : ''}}" bindtap="selectPartial">
|
||||
<view class="csc-type-n">部分衣物</view>
|
||||
<view class="csc-type-d">挑选几件衣物分享给好友</view>
|
||||
<view wx:if="{{shareType === 1}}" class="icon icon-check csc-check"></view>
|
||||
</view>
|
||||
<view class="csc-type {{shareType === 2 ? 'is-on' : ''}}" bindtap="selectFull">
|
||||
<view class="csc-type-n">全量快照</view>
|
||||
<view class="csc-type-d">分享当前衣橱的全部衣物</view>
|
||||
<view wx:if="{{shareType === 2}}" class="icon icon-check csc-check"></view>
|
||||
</view>
|
||||
<view class="type-card card {{shareType === 2 ? 'active' : ''}}" bindtap="selectFull">
|
||||
<view class="type-name">全量快照</view>
|
||||
<view class="type-desc">分享当前衣橱的全部衣物</view>
|
||||
<view wx:if="{{shareType === 2}}" class="icon icon-check type-check"></view>
|
||||
</view>
|
||||
|
||||
<!-- 选择衣物入口(部分衣物) -->
|
||||
<view wx:if="{{shareType === 1}}" class="card select-entry" bindtap="openSelect">
|
||||
<view class="entry-left">
|
||||
<view class="entry-icon"><view class="icon icon-shirt"></view></view>
|
||||
<view class="entry-title">选择衣物</view>
|
||||
</view>
|
||||
<view class="entry-right">
|
||||
<text class="entry-count {{selectedCount ? 'on' : ''}}">{{selectedCount ? '已选 ' + selectedCount + ' 件' : '未选择'}}</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
<view wx:if="{{shareType === 1}}" class="rows csc-entry">
|
||||
<view class="row" bindtap="openSelect">
|
||||
<view class="icon row-ic icon-shirt"></view>
|
||||
<text class="row-lb">选择衣物</text>
|
||||
<text class="row-vl">{{selectedCount ? '已选 ' + selectedCount + ' 件' : '未选择'}}</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title">有效期</view>
|
||||
<view class="card expire-card">
|
||||
<view class="grp-hd csc-grp"><text class="gh-cn">有效期</text><text class="gh-en">Expire</text></view>
|
||||
<view class="rows">
|
||||
<view
|
||||
class="expire-item hairline"
|
||||
class="row"
|
||||
wx:for="{{expireOptions}}"
|
||||
wx:key="days"
|
||||
data-days="{{item.days}}"
|
||||
bindtap="selectExpire"
|
||||
>
|
||||
<text>{{item.label}}</text>
|
||||
<view wx:if="{{expireDays === item.days}}" class="icon icon-check expire-check"></view>
|
||||
<text class="row-lb">{{item.label}}</text>
|
||||
<view wx:if="{{expireDays === item.days}}" class="icon csc-tick icon-check"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer">
|
||||
<view class="btn btn-primary" bindtap="onConfirm">确认分享(消耗 1 次衣柜分享额度)</view>
|
||||
</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 选择衣物视图 -->
|
||||
<view wx:elif="{{view === 'select'}}" class="select-view">
|
||||
<scroll-view scroll-x class="cat-scroll" show-scrollbar="{{false}}">
|
||||
<view class="cat-row">
|
||||
<block wx:elif="{{view === 'select'}}">
|
||||
<view class="segbar">
|
||||
<scroll-view scroll-x class="seg-scroll" show-scrollbar="{{false}}">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="name"
|
||||
class="cat-item {{activeCategory === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeCategory === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onCategoryTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<scroll-view scroll-y class="select-body">
|
||||
<view wx:if="{{selectLoading}}" class="grid">
|
||||
<view class="grid-item" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this">
|
||||
<view class="skeleton sk-card"></view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="select-body pb-dock" bindscroll="onNavScroll">
|
||||
<view wx:if="{{selectLoading}}" class="csc-grid">
|
||||
<view class="csc-skel" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this">
|
||||
<view class="sk sk-img"></view>
|
||||
<view class="sk sk-ln" style="width: 62%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="grid">
|
||||
<view class="grid-item" wx:for="{{allClothes}}" wx:key="clothes_uuid">
|
||||
<view wx:else class="csc-grid">
|
||||
<view class="csc-tile" wx:for="{{allClothes}}" wx:key="clothes_uuid">
|
||||
<clothes-card item="{{item}}" selectable selected="{{item._selected}}" bind:select="onToggleSelect" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="gap-32"></view>
|
||||
</scroll-view>
|
||||
<view class="select-footer">
|
||||
<view class="sf-count">已选 <text class="t-primary">{{selectedUuids.length}}</text> 件</view>
|
||||
<view class="sf-confirm btn btn-primary" bindtap="confirmSelect">确定</view>
|
||||
</block>
|
||||
|
||||
<!-- 成功预览 -->
|
||||
<scroll-view wx:else scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Ready</view>
|
||||
<view class="display">分享已创建</view>
|
||||
</view>
|
||||
|
||||
<view class="csc-done-ic">
|
||||
<view class="icon icon-check"></view>
|
||||
</view>
|
||||
|
||||
<view class="csc-preview">
|
||||
<view class="csc-preview-hd">
|
||||
<view class="csc-av">
|
||||
<image wx:if="{{avatar}}" class="pic" src="{{avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user"></view>
|
||||
</view>
|
||||
<text class="csc-preview-n">{{nickname}} 向你分享了 {{selectedCount || '全部'}} 件衣物</text>
|
||||
</view>
|
||||
<view class="csc-preview-body">
|
||||
<view class="icon icon-hanger"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 成功预览 -->
|
||||
<view wx:else class="success-view">
|
||||
<view class="success-icon"><view class="icon icon-check"></view></view>
|
||||
<view class="success-title">分享已创建</view>
|
||||
<view class="preview-card">
|
||||
<view class="preview-top">
|
||||
<view class="avatar">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user avatar-placeholder"></view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮底部操作条 -->
|
||||
<view class="dock">
|
||||
<block wx:if="{{view === 'config'}}">
|
||||
<view class="btn btn--ink btn--block" bindtap="onConfirm">确认分享(消耗 1 次衣柜分享额度)</view>
|
||||
</block>
|
||||
<block wx:elif="{{view === 'select'}}">
|
||||
<view class="dock-row">
|
||||
<view class="csc-count-wrap">
|
||||
<view class="csc-count-t num">已选 {{selectedUuids.length}} 件</view>
|
||||
<view class="csc-count-d">最多可选择 10 件单品</view>
|
||||
</view>
|
||||
<text class="preview-name">{{nickname}} 向你分享了 {{selectedCount || '全部'}} 件衣物</text>
|
||||
<view class="btn btn--ink btn--sm csc-confirm" bindtap="confirmSelect">确定</view>
|
||||
</view>
|
||||
<view class="preview-body">
|
||||
<view class="icon icon-hanger"></view>
|
||||
</view>
|
||||
<view class="preview-btn">查看衣橱</view>
|
||||
</view>
|
||||
<view class="success-btns">
|
||||
<view class="btn btn-outline" bindtap="onDone">完成</view>
|
||||
<view class="btn btn-primary" bindtap="onViewShare">查看分享</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="dock-row">
|
||||
<view class="btn btn--ghost" bindtap="onDone">完成</view>
|
||||
<view class="btn btn--ink" bindtap="onViewShare">查看分享</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 额度不足 -->
|
||||
<v2-overlay
|
||||
visible="{{quotaVisible}}"
|
||||
type="modal"
|
||||
title="{{quotaTitle}}"
|
||||
content="{{quotaText}}"
|
||||
buttons="{{quotaButtons}}"
|
||||
bind:confirm="onQuotaConfirm"
|
||||
bind:cancel="onQuotaCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.create-page {
|
||||
/** 创建分享:三视图(配置 / 选择衣物 / 成功预览)**/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,314 +11,196 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
padding: 24rpx 24rpx 16rpx;
|
||||
.csc-grp {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.type-card {
|
||||
position: relative;
|
||||
margin: 0 24rpx 20rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.type-card.active {
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.type-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.type-check {
|
||||
position: absolute;
|
||||
top: 32rpx;
|
||||
right: 32rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.select-entry {
|
||||
margin: 0 24rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.entry-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
color: #007AFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.entry-icon .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
}
|
||||
|
||||
.entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.entry-count {
|
||||
font-size: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.entry-count.on {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.entry-right .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.expire-card {
|
||||
margin: 0 24rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.expire-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.expire-check {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 24rpx;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 选择视图 */
|
||||
.select-view {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
/* 分享方式:两张可点卡片 */
|
||||
.csc-types {
|
||||
padding: 0 var(--pad);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.cat-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
.csc-type {
|
||||
position: relative;
|
||||
padding: 34rpx 36rpx;
|
||||
border-radius: var(--r-ctrl);
|
||||
background: var(--fill);
|
||||
transition: background .18s var(--ease), box-shadow .18s var(--ease);
|
||||
}
|
||||
|
||||
.csc-type.is-on {
|
||||
background: var(--paper);
|
||||
box-shadow: inset 0 0 0 1rpx var(--ink);
|
||||
}
|
||||
|
||||
.csc-type-n {
|
||||
font-family: var(--serif);
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.csc-type-d {
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.csc-check {
|
||||
position: absolute;
|
||||
top: 34rpx;
|
||||
right: 36rpx;
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.csc-entry {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.csc-tick {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* 选择衣物视图 */
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 32rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.cat-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.select-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx;
|
||||
gap: 24rpx;
|
||||
.csc-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 44rpx 26rpx;
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: calc(50% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 360rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.select-footer {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sf-count {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
}
|
||||
|
||||
.sf-confirm {
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 成功视图 */
|
||||
.success-view {
|
||||
flex: 1;
|
||||
padding: 48rpx 24rpx;
|
||||
.csc-skel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
.csc-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 成功预览 */
|
||||
.csc-done-ic {
|
||||
margin: 56rpx auto 0;
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 50%;
|
||||
background: #34C759;
|
||||
color: #fff;
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 0 0 1rpx var(--hairline-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-icon .icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
.csc-done-ic .icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-top: 24rpx;
|
||||
.csc-preview {
|
||||
margin: 64rpx var(--pad) 0;
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
|
||||
.preview-card {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
margin-top: 40rpx;
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.preview-top {
|
||||
.csc-preview-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
.csc-av {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
.csc-av .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
.csc-av .icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
|
||||
.preview-name {
|
||||
font-size: 26rpx;
|
||||
color: #000;
|
||||
.csc-preview-n {
|
||||
font-size: 27rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
height: 320rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 24rpx;
|
||||
margin-top: 24rpx;
|
||||
.csc-preview-body {
|
||||
margin-top: 32rpx;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.preview-body .icon {
|
||||
.csc-preview-body .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
margin-top: 24rpx;
|
||||
/* 底部操作条 */
|
||||
.csc-count-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.csc-count-t {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.csc-count-d {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.csc-confirm {
|
||||
width: 240rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.success-btns .btn {
|
||||
.dock-row .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state"
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
|
||||
import { ClosetShareDetail } from '../../types/models'
|
||||
import { ShareStatus } from '../../utils/config'
|
||||
import { formatDateTime } from '../../utils/format'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -17,8 +18,19 @@ Page({
|
||||
invalidTitle: '该分享已过期或已被取消',
|
||||
invalidDesc: '分享链接已失效',
|
||||
needLogin: false,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 取消分享确认:v2 玻璃弹窗
|
||||
cancelVisible: false,
|
||||
cancelButtons: [
|
||||
{ text: '再想想', value: '' },
|
||||
{ text: '取消分享', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid)
|
||||
@@ -99,13 +111,16 @@ Page({
|
||||
},
|
||||
|
||||
onCancel() {
|
||||
wx.showModal({
|
||||
title: '取消分享',
|
||||
content: '取消后该分享链接将失效,且不返还衣柜分享额度',
|
||||
confirmText: '取消分享',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({ cancelVisible: true })
|
||||
},
|
||||
|
||||
onCancelCancel() {
|
||||
this.setData({ cancelVisible: false })
|
||||
},
|
||||
|
||||
onCancelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ cancelVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/closet-share/${this.data.uuid}/cancel`, method: 'POST' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '取消分享成功')
|
||||
@@ -113,15 +128,12 @@ Page({
|
||||
;(this as any).backTimer = setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e: any) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
fail: (err: any) => logger.warn(LOG, 'navigateBack 失败, err=', err && err.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '取消分享失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e: any) => logger.warn(LOG, '取消分享弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((err: any) => logger.warn(LOG, '取消分享失败, code=', err && err.code))
|
||||
},
|
||||
|
||||
onBackHome() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="分享详情" />
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="分享详情" scrolled="{{navScrolled}}" />
|
||||
|
||||
<empty-state
|
||||
wx:if="{{invalid && needLogin}}"
|
||||
@@ -19,36 +19,53 @@
|
||||
bind:action="onBackHome"
|
||||
/>
|
||||
|
||||
<scroll-view wx:else scroll-y class="scroll-body">
|
||||
<!-- 分享者信息 -->
|
||||
<view class="card owner-card">
|
||||
<view class="avatar">
|
||||
<image wx:if="{{detail.owner_avatar}}" class="avatar-img" src="{{detail.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user avatar-placeholder"></view>
|
||||
<scroll-view wx:else scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<!-- 分享者行 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Shared Closet</view>
|
||||
<view class="display">{{detail.owner_name}} 的衣橱</view>
|
||||
</view>
|
||||
<view class="owner-info">
|
||||
<view class="owner-name">{{detail.owner_name}} 分享了 {{detail.clothes.length}} 件衣物</view>
|
||||
<view class="owner-expire">{{expireText}}</view>
|
||||
|
||||
<view class="csd-owner">
|
||||
<view class="csd-av">
|
||||
<image wx:if="{{detail.owner_avatar}}" class="pic" src="{{detail.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user"></view>
|
||||
</view>
|
||||
<view class="csd-owner-body">
|
||||
<view class="csd-owner-n">{{detail.owner_name}} 分享了 {{detail.clothes.length}} 件衣物</view>
|
||||
<view class="csd-owner-e num">{{expireText}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 衣物网格 -->
|
||||
<view class="grid">
|
||||
<view class="grid-item" wx:for="{{detail.clothes}}" wx:key="clothes_uuid">
|
||||
<view class="clothes-card">
|
||||
<image wx:if="{{item.processed_image}}" class="clothes-img" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="clothes-placeholder"><view class="icon icon-hanger"></view></view>
|
||||
<view class="clothes-name">{{item.name || '未命名'}}</view>
|
||||
<view class="clothes-sub">{{item.category_main}}{{item.category_sub ? ' · ' + item.category_sub : ''}}</view>
|
||||
<!-- 只读网格 -->
|
||||
<view class="csd-grid">
|
||||
<view class="csd-tile" wx:for="{{detail.clothes}}" wx:key="clothes_uuid">
|
||||
<view class="tile-img">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" lazy-load />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view class="tile-name">{{item.name || '未命名'}}</view>
|
||||
<view class="tile-sub">{{item.category_main}}{{item.category_sub ? ' · ' + item.category_sub : ''}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 转发出口 -->
|
||||
<button class="share-btn" open-type="share">分享给好友</button>
|
||||
|
||||
<!-- 属主取消 -->
|
||||
<view wx:if="{{detail.is_owner}}" class="cancel-btn btn-danger-text" bindtap="onCancel">取消分享</view>
|
||||
<view class="footer-space"></view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮玻璃操作条 -->
|
||||
<view class="dock" wx:if="{{!invalid}}">
|
||||
<view wx:if="{{detail.is_owner}}" class="btn--text csd-cancel" bindtap="onCancel">取消分享</view>
|
||||
<button class="btn btn--ink btn--block" open-type="share">分享给好友</button>
|
||||
</view>
|
||||
|
||||
<!-- 取消分享确认 -->
|
||||
<v2-overlay
|
||||
visible="{{cancelVisible}}"
|
||||
type="modal"
|
||||
title="取消分享"
|
||||
content="取消后该分享链接将失效,且不返还衣柜分享额度"
|
||||
buttons="{{cancelButtons}}"
|
||||
bind:confirm="onCancelConfirm"
|
||||
bind:cancel="onCancelCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.detail-page {
|
||||
/** 衣柜分享详情:分享者行 + 只读网格 + 悬浮玻璃操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,129 +11,65 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.owner-card {
|
||||
margin: 24rpx;
|
||||
.csd-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
.csd-av {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
.csd-av .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
.csd-av .icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.owner-info {
|
||||
margin-left: 24rpx;
|
||||
.csd-owner-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.csd-owner-n {
|
||||
font-size: 27rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.owner-expire {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: calc(50% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clothes-card {
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
padding: 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.clothes-img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 24rpx;
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.clothes-placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 24rpx;
|
||||
background: #F2F2F7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.clothes-placeholder .icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.clothes-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #1C1C1E;
|
||||
margin-top: 12rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clothes-sub {
|
||||
.csd-owner-e {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 4rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
margin: 32rpx 24rpx 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary, #000);
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
.csd-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 44rpx 26rpx;
|
||||
padding: 56rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
.csd-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
.csd-cancel {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.footer-space {
|
||||
height: 40rpx;
|
||||
padding: 8rpx 0 28rpx;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, ClosetShareItem, OutfitShareItem } from '../../types/models'
|
||||
import { ShareStatus, ShareType } from '../../utils/config'
|
||||
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { normalizePaged } from '../../utils/paged'
|
||||
import { setTabBarSelected } from '../../utils/tabbar'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -37,8 +38,13 @@ Page({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
@@ -82,30 +88,36 @@ Page({
|
||||
return this.fetchMine(page)
|
||||
},
|
||||
|
||||
// 我的分享:仅衣柜分享
|
||||
// 我的分享:衣柜分享 + 穿搭分享并行拉取后按时间倒序合并
|
||||
fetchMine(page: number) {
|
||||
// 请求序号:切 tab 时旧响应可能后到,序号不匹配直接丢弃
|
||||
const seq = ((this as any).reqSeq || 0) + 1
|
||||
;(this as any).reqSeq = seq
|
||||
if (page > 1) this.setData({ loadingMore: true })
|
||||
|
||||
Promise.all([
|
||||
request<PagedResult<ClosetShareItem>>({
|
||||
url: '/closet-share',
|
||||
data: { page, page_size: this.data.pageSize },
|
||||
})
|
||||
.then((res) => {
|
||||
}),
|
||||
request<PagedResult<OutfitShareItem>>({
|
||||
url: '/outfit-share',
|
||||
data: { page, page_size: this.data.pageSize },
|
||||
}),
|
||||
])
|
||||
.then(([closetRes, outfitRes]) => {
|
||||
if ((this as any).reqSeq !== seq) return
|
||||
const { list: raw, total } = normalizePaged<ClosetShareItem>(res)
|
||||
logger.debug(LOG, '我的分享加载成功, page=', page, '条数=', raw && raw.length)
|
||||
const list = raw.map((it) => this.decorateCloset(it))
|
||||
const merged = mergePaged(this.data.list, list, page === 1)
|
||||
this.setData({
|
||||
list: merged,
|
||||
hasMore: hasMorePaged(merged, total, this.data.pageSize),
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
page,
|
||||
})
|
||||
const closetList = normalizePaged<ClosetShareItem>(closetRes).list.map((it) => this.decorateCloset(it))
|
||||
const outfitList = normalizePaged<OutfitShareItem>(outfitRes).list.map((it) => this.decorateOutfit(it))
|
||||
logger.debug(LOG, '我的分享加载成功, page=', page, '衣柜=', closetList.length, '穿搭=', outfitList.length)
|
||||
const prev = page === 1 ? [] : this.data.list
|
||||
const merged = prev
|
||||
.concat(closetList, outfitList)
|
||||
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
|
||||
// 任一侧仍返回满页则认为还有更多
|
||||
const hasMore =
|
||||
closetList.length >= this.data.pageSize || outfitList.length >= this.data.pageSize
|
||||
this.setData({ list: merged, hasMore, loading: false, loadingMore: false, page })
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if ((this as any).reqSeq !== seq) return
|
||||
|
||||
@@ -1,67 +1,79 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="分享" back="{{false}}" />
|
||||
<navigation-bar title="分享" back="{{false}}" scrolled="{{navScrolled}}">
|
||||
<view slot="right" class="nav-action nav-action--ink" bindtap="onCreate">新建</view>
|
||||
</navigation-bar>
|
||||
|
||||
<!-- 分段切换 -->
|
||||
<view class="seg-wrap">
|
||||
<view class="seg-control">
|
||||
<scroll-view scroll-y class="scroll-body pb-tab" bindscrolltolower="onReachBottom" bindscroll="onNavScroll">
|
||||
<!-- 大标题页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Sharing</view>
|
||||
<view class="display">分享</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线式文字分段 -->
|
||||
<view class="segbar">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{tabs}}"
|
||||
wx:key="name"
|
||||
class="seg-item {{activeTab === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeTab === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTabTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body" bindscrolltolower="onReachBottom">
|
||||
<view wx:if="{{activeTab === 0}}" class="new-btn btn btn-outline" bindtap="onCreate">+ 新建分享</view>
|
||||
|
||||
<empty-state
|
||||
wx:if="{{!loading && !list.length}}"
|
||||
icon="icon-share"
|
||||
title="{{activeTab === 0 ? '还没有分享' : '还没有收到分享'}}"
|
||||
desc="{{activeTab === 0 ? '把衣橱或精选衣物分享给好友' : '好友分享给你的衣橱或穿搭会出现在这里'}}"
|
||||
desc="{{activeTab === 0 ? '把衣橱、精选衣物或穿搭分享给好友' : '好友分享给你的衣橱或穿搭会出现在这里'}}"
|
||||
buttonText="{{activeTab === 0 ? '新建分享' : ''}}"
|
||||
bind:action="onCreate"
|
||||
/>
|
||||
|
||||
<view class="list">
|
||||
<!-- 横向条目卡:左缩略图 + 右标题/有效期 -->
|
||||
<view class="cs-list">
|
||||
<view
|
||||
class="share-card card {{item._invalid ? 'invalid' : ''}}"
|
||||
class="cs-card {{item._invalid ? 'is-dead' : ''}}"
|
||||
wx:for="{{list}}"
|
||||
wx:key="share_uuid"
|
||||
data-uuid="{{item.share_uuid}}"
|
||||
data-kind="{{item.kind}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<!-- 衣柜分享:四宫格占位 -->
|
||||
<view class="preview-grid" wx:if="{{item.kind === 'closet'}}">
|
||||
<view class="pg-cell"><view class="icon icon-hanger"></view></view>
|
||||
<view class="pg-cell"><view class="icon icon-hanger"></view></view>
|
||||
<view class="pg-cell"><view class="icon icon-hanger"></view></view>
|
||||
<view class="pg-cell"><view class="icon icon-hanger"></view></view>
|
||||
<!-- 衣柜分享:无缝四宫格 -->
|
||||
<view class="cs-thumb" wx:if="{{item.kind === 'closet'}}">
|
||||
<view class="c"><view class="icon icon-hanger"></view></view>
|
||||
<view class="c"><view class="icon icon-hanger"></view></view>
|
||||
<view class="c"><view class="icon icon-hanger"></view></view>
|
||||
<view class="c"><view class="icon icon-hanger"></view></view>
|
||||
</view>
|
||||
<!-- 穿搭分享:封面 -->
|
||||
<view class="preview-cover" wx:else>
|
||||
<image wx:if="{{item.cover}}" class="preview-cover-img" src="{{item.cover}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-hanger preview-cover-ph"></view>
|
||||
<view class="cs-thumb is-photo" wx:else>
|
||||
<image wx:if="{{item.cover}}" class="pic" src="{{item.cover}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
|
||||
<view class="share-info">
|
||||
<view wx:if="{{activeTab === 1}}" class="owner-row">
|
||||
<image wx:if="{{item.owner_avatar}}" class="owner-avatar" src="{{item.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="owner-avatar owner-avatar-placeholder">
|
||||
<view class="icon icon-user owner-avatar-icon"></view>
|
||||
<view class="cs-body">
|
||||
<view class="cs-owner" wx:if="{{activeTab === 1}}">
|
||||
<image wx:if="{{item.owner_avatar}}" class="oav" src="{{item.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="oav">
|
||||
<view class="icon icon-user"></view>
|
||||
</view>
|
||||
<text class="owner-name">{{item.owner_name || '好友'}}</text>
|
||||
<text class="onm">{{item.owner_name || '好友'}}</text>
|
||||
</view>
|
||||
<view class="share-title">{{item.title}}</view>
|
||||
<view class="tag tag-gray share-type">{{item.typeText}}</view>
|
||||
<view class="share-expire">{{item.expireText}}</view>
|
||||
</view>
|
||||
<view wx:if="{{item._invalid}}" class="share-badge">{{item.status === 2 ? '已取消' : '已失效'}}</view>
|
||||
<view class="cs-t">{{item.title}}</view>
|
||||
<view class="cs-tags">
|
||||
<text class="pill">{{item.typeText}}</text>
|
||||
<text wx:if="{{item._invalid}}" class="pill pill--line">{{item.expireText}}</text>
|
||||
<text wx:else class="cs-exp">{{item.expireText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="foot-note">没有更多了</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 分享列表:下划线分段 + 横向条目卡(失效项降透明) **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,166 +11,142 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.seg-wrap {
|
||||
background: #fff;
|
||||
padding: 20rpx 24rpx;
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
margin-top: 36rpx;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.seg-control {
|
||||
.cs-list {
|
||||
padding: 28rpx var(--pad) 0;
|
||||
display: flex;
|
||||
background: #F2F2F7;
|
||||
border-radius: 16rpx;
|
||||
padding: 4rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.seg-item {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.seg-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.new-btn {
|
||||
margin: 24rpx;
|
||||
width: calc(100% - 48rpx);
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.share-card {
|
||||
position: relative;
|
||||
.cs-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
gap: 26rpx;
|
||||
padding: 32rpx 0;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
transition: opacity .2s var(--ease), transform .2s var(--ease);
|
||||
}
|
||||
|
||||
.share-card.invalid {
|
||||
opacity: 0.5;
|
||||
.cs-card:active {
|
||||
transform: scale(.985);
|
||||
}
|
||||
|
||||
.preview-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
gap: 8rpx;
|
||||
.cs-card.is-dead {
|
||||
opacity: .38;
|
||||
}
|
||||
|
||||
.cs-thumb {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--r-img);
|
||||
background: var(--paper-2);
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 2rpx;
|
||||
}
|
||||
|
||||
.pg-cell {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
background: #F2F2F7;
|
||||
border-radius: 12rpx;
|
||||
.cs-thumb .c {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
background: var(--paper-2);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pg-cell .icon {
|
||||
.cs-thumb .c .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
flex-shrink: 0;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
/* 穿搭分享封面 */
|
||||
.preview-cover {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #F2F2F7;
|
||||
.cs-thumb.is-photo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.preview-cover-img {
|
||||
.cs-thumb.is-photo .ph {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.cs-thumb .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-cover-ph {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
.cs-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.owner-row {
|
||||
.cs-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.owner-avatar {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
.cs-owner .oav {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
background: #F2F2F7;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.owner-avatar-placeholder {
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.owner-avatar-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
.cs-owner .oav .icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
.cs-owner .onm {
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.share-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.share-title {
|
||||
.cs-t {
|
||||
font-family: var(--serif);
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.share-type {
|
||||
margin-top: 12rpx;
|
||||
.cs-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14rpx;
|
||||
margin-top: 18rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.share-expire {
|
||||
.cs-exp {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.share-badge {
|
||||
position: absolute;
|
||||
top: 16rpx;
|
||||
right: 16rpx;
|
||||
font-size: 20rpx;
|
||||
color: #8E8E93;
|
||||
background: #F2F2F7;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
color: var(--ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { ClothesItem } from '../../types/models'
|
||||
import { ProcessStatus } from '../../utils/config'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -122,8 +123,23 @@ Page({
|
||||
sheetOptions: [] as string[],
|
||||
sheetValue: '',
|
||||
sheetValues: [] as string[],
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 删除确认:v2 玻璃弹窗
|
||||
delVisible: false,
|
||||
delText: '',
|
||||
delButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认删除', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
// 抽屉内容点击不穿透到遮罩
|
||||
noop() {},
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
const shareUUID = query.share_uuid || ''
|
||||
@@ -155,7 +171,7 @@ Page({
|
||||
: `/clothes/${this.data.uuid}`
|
||||
return request<ClothesItem>({ url })
|
||||
.then((detail) => {
|
||||
logger.debug(LOG, '详情加载成功, 处理状态=', detail && detail.image_process_status)
|
||||
logger.debug(LOG, '详情加载成功, name=', detail && detail.name)
|
||||
this.setData({ detail, loading: false })
|
||||
})
|
||||
.catch((e: any) => {
|
||||
@@ -320,16 +336,22 @@ Page({
|
||||
.catch((e: any) => logger.warn(LOG, '衣物信息保存失败, code=', e && e.code))
|
||||
},
|
||||
|
||||
// 删除入口:打开 v2 玻璃确认弹窗
|
||||
onDelete() {
|
||||
const canRefund = this.data.imageStatus !== ProcessStatus.FAILED
|
||||
const content = canRefund ? '删除后将退回 1 次上传额度,确认删除?' : '确认删除?'
|
||||
wx.showModal({
|
||||
title: '删除这件衣物',
|
||||
content,
|
||||
confirmText: '确认删除',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({
|
||||
delVisible: true,
|
||||
delText: canRefund ? '删除后将退回 1 次上传额度,确认删除?' : '确认删除?',
|
||||
})
|
||||
},
|
||||
|
||||
onDelCancel() {
|
||||
this.setData({ delVisible: false })
|
||||
},
|
||||
|
||||
onDelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ delVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/clothes/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '衣物删除成功')
|
||||
@@ -337,14 +359,11 @@ Page({
|
||||
setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
fail: (e2) => logger.warn(LOG, 'navigateBack 失败, err=', e2 && e2.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '衣物删除失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((e2: any) => logger.warn(LOG, '衣物删除失败, code=', e2 && e2.code))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -40,17 +40,21 @@ function tags(v) {
|
||||
module.exports = { hex: hex, tags: tags }
|
||||
</wxs>
|
||||
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="衣物详情">
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="衣物详情" scrolled="{{navScrolled}}">
|
||||
<view
|
||||
wx:if="{{!readonly && imageStatus !== 1 && imageStatus !== 2 && imageStatus !== 3}}"
|
||||
slot="right"
|
||||
class="nav-action"
|
||||
class="nav-action nav-action--ink"
|
||||
bindtap="onToggleEdit"
|
||||
>{{editMode ? '取消' : '编辑'}}</view>
|
||||
</navigation-bar>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-body {{editMode ? 'pb-dock' : 'pb-safe'}}"
|
||||
bindscroll="onNavScroll"
|
||||
>
|
||||
<!-- 缺少衣物标识 -->
|
||||
<empty-state
|
||||
wx:if="{{notFound}}"
|
||||
@@ -61,161 +65,173 @@ module.exports = { hex: hex, tags: tags }
|
||||
bind:action="onBackWardrobe"
|
||||
/>
|
||||
|
||||
<!-- 处理中:显示步骤进度 -->
|
||||
<!-- 处理中:三步发丝线进度轨 -->
|
||||
<block wx:elif="{{imageStatus === 1 || imageStatus === 2 || imageStatus === 3}}">
|
||||
<view class="card processing-card">
|
||||
<view class="proc-icon"><view class="icon icon-hanger"></view></view>
|
||||
<view class="steps">
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 0 ? 'active' : ''}}"></view>
|
||||
<text class="step-text {{step >= 0 ? 'active-text' : ''}}">排队中</text>
|
||||
<view class="head">
|
||||
<view class="eyebrow">Processing</view>
|
||||
<view class="display">AI 正在识别你的衣物…</view>
|
||||
<view class="cd-note">通常需要 10 秒左右,请稍候</view>
|
||||
</view>
|
||||
<view class="step-line {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<text class="step-text {{step >= 1 ? 'active-text' : ''}}">AI 抠图</text>
|
||||
<view class="up-preview">
|
||||
<view class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view class="step-line {{step >= 2 ? 'active' : ''}}"></view>
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 2 ? 'active' : ''}}"></view>
|
||||
<text class="step-text {{step >= 2 ? 'active-text' : ''}}">智能分类</text>
|
||||
<view class="up-steps">
|
||||
<view class="up-step {{step > 0 ? 'is-done' : ''}} {{step === 0 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">排队中</text>
|
||||
</view>
|
||||
<view class="up-step {{step > 1 ? 'is-done' : ''}} {{step === 1 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">AI 抠图</text>
|
||||
</view>
|
||||
<view class="up-step {{step === 2 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">智能分类</text>
|
||||
</view>
|
||||
<view class="proc-text">AI 正在识别你的衣物…</view>
|
||||
<view class="proc-desc">通常需要 10 秒左右,请稍候</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 识别失败 -->
|
||||
<block wx:elif="{{imageStatus === 5}}">
|
||||
<view class="img-box failed-box">
|
||||
<view class="fail-big-icon">!</view>
|
||||
<view class="up-preview">
|
||||
<view class="up-bang">!</view>
|
||||
</view>
|
||||
<view class="card name-card">
|
||||
<view class="name-text">{{detail.name || '未命名衣物'}}</view>
|
||||
<view class="head">
|
||||
<view class="display">识别失败</view>
|
||||
<view class="cd-note">{{failReason || '未知原因'}} · 本次上传额度已自动退回,你可以删除或重新上传</view>
|
||||
</view>
|
||||
<view class="card fail-card">
|
||||
<view class="fail-title">识别失败:{{failReason || '未知原因'}}</view>
|
||||
<view class="fail-desc">本次上传额度已自动退回,你可以删除或重新上传</view>
|
||||
<view class="up-duo">
|
||||
<view class="btn btn--ghost" bindtap="onDelete">删除</view>
|
||||
<view class="btn btn--ink" bindtap="onBackWardrobe">重新上传</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 正常详情 -->
|
||||
<block wx:else>
|
||||
<!-- 大图区 -->
|
||||
<view class="img-box">
|
||||
<!-- 大图:买手店目录式线稿/实拍 -->
|
||||
<view class="cd-photo">
|
||||
<image
|
||||
wx:if="{{detail.processed_image}}"
|
||||
class="img"
|
||||
class="pic"
|
||||
src="{{detail.processed_image}}"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view wx:else class="img-placeholder"><view class="icon icon-hanger"></view></view>
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
|
||||
<!-- 名称 -->
|
||||
<view class="card name-card">
|
||||
<view class="cd-name">
|
||||
<input
|
||||
wx:if="{{editMode}}"
|
||||
class="input"
|
||||
class="cd-input"
|
||||
value="{{formData.name}}"
|
||||
bindinput="onNameInput"
|
||||
placeholder="衣物名称"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
<view wx:else class="name-text">{{detail.name || '未命名衣物'}}</view>
|
||||
<view wx:else class="t-lg">{{detail.name || '未命名衣物'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 属性分组(基础属性 / 版型与材质 / 智能量化评估) -->
|
||||
<view class="card attr-card" wx:for="{{attrGroups}}" wx:key="title" wx:for-item="group">
|
||||
<view class="group-title">{{group.title}}</view>
|
||||
<!-- 属性规格表 -->
|
||||
<view class="cd-group" wx:for="{{attrGroups}}" wx:key="title" wx:for-item="group">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">{{group.title}}</text>
|
||||
</view>
|
||||
<view class="rows">
|
||||
<view
|
||||
class="attr-row hairline {{editMode ? 'edit' : ''}}"
|
||||
class="spec {{editMode ? 'is-edit' : ''}}"
|
||||
wx:for="{{group.attrs}}"
|
||||
wx:key="key"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="onAttrTap"
|
||||
>
|
||||
<view class="attr-label">{{item.label}}</view>
|
||||
<view class="attr-value">
|
||||
<!-- 评分:5 段圆条 + 分值 -->
|
||||
<text class="spec-k">{{item.label}}</text>
|
||||
<view class="spec-v">
|
||||
<!-- 量化评分:细描边方点 -->
|
||||
<block wx:if="{{item.type === 'score'}}">
|
||||
<view class="score-dots">
|
||||
<view class="dots">
|
||||
<view
|
||||
class="score-dot {{dotIdx < ((editMode ? formData[item.key] : detail[item.key]) || 0) ? 'on' : ''}} {{item.key === 'visual_impact' ? 'violet' : ''}}"
|
||||
class="d {{dotIdx < ((editMode ? formData[item.key] : detail[item.key]) || 0) ? 'on' : ''}}"
|
||||
wx:for="{{[1, 2, 3, 4, 5]}}"
|
||||
wx:for-item="dot"
|
||||
wx:for-index="dotIdx"
|
||||
wx:key="*this"
|
||||
></view>
|
||||
</view>
|
||||
<text class="score-num">{{(editMode ? formData[item.key] : detail[item.key]) || 0}}/5</text>
|
||||
<text class="cd-score num">{{(editMode ? formData[item.key] : detail[item.key]) || 0}}/5</text>
|
||||
</block>
|
||||
<!-- 适用场合:胶囊标签 -->
|
||||
<!-- 适用场合:无底色描边胶囊 -->
|
||||
<block wx:elif="{{item.type === 'multi'}}">
|
||||
<view
|
||||
class="scene-list"
|
||||
wx:if="{{fmt.tags(editMode ? formData.occasions : detail.occasions).length}}"
|
||||
>
|
||||
<view class="cd-tags" wx:if="{{fmt.tags(editMode ? formData.occasions : detail.occasions).length}}">
|
||||
<text
|
||||
class="tag tag-blue scene-tag"
|
||||
class="pill pill--line"
|
||||
wx:for="{{fmt.tags(editMode ? formData.occasions : detail.occasions)}}"
|
||||
wx:for-item="scene"
|
||||
wx:key="*this"
|
||||
>{{scene}}</text>
|
||||
</view>
|
||||
<text wx:else class="attr-text weak">—</text>
|
||||
<text wx:else class="c-3">—</text>
|
||||
</block>
|
||||
<!-- 普通文本 / 主色 -->
|
||||
<block wx:else>
|
||||
<text class="attr-text">{{(editMode ? formData[item.key] : detail[item.key]) || '—'}}</text>
|
||||
<text class="spec-t">{{(editMode ? formData[item.key] : detail[item.key]) || '—'}}</text>
|
||||
<view
|
||||
wx:if="{{item.key === 'color_main' && fmt.hex(editMode ? formData.color_main : detail.color_main)}}"
|
||||
class="color-dot"
|
||||
style="background: {{fmt.hex(editMode ? formData.color_main : detail.color_main)}}"
|
||||
class="dot"
|
||||
style="background: {{fmt.hex(editMode ? formData.color_main : detail.color_main)}};"
|
||||
></view>
|
||||
</block>
|
||||
</view>
|
||||
<view wx:if="{{editMode}}" class="icon icon-chevron-right edit-arrow"></view>
|
||||
<view wx:if="{{editMode}}" class="icon icon-chevron-right cd-arrow"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 备注 -->
|
||||
<view class="card remark-card">
|
||||
<view class="group-title">备注信息</view>
|
||||
<view class="remark-body">
|
||||
<view class="cd-group">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">备注信息</text>
|
||||
<text class="gh-en">Note</text>
|
||||
</view>
|
||||
<view class="cd-remark">
|
||||
<textarea
|
||||
wx:if="{{editMode}}"
|
||||
class="textarea"
|
||||
value="{{formData.remark}}"
|
||||
bindinput="onRemarkInput"
|
||||
placeholder="添加备注…"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
<view wx:elif="{{detail.remark}}" class="remark-text">{{detail.remark}}</view>
|
||||
<view wx:else class="remark-text weak">暂无备注</view>
|
||||
<view wx:elif="{{detail.remark}}" class="t-bd">{{detail.remark}}</view>
|
||||
<view wx:else class="c-3">暂无备注</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 底部操作:处理中隐藏,其他显示 -->
|
||||
<view
|
||||
wx:if="{{!readonly && !notFound && imageStatus !== 1 && imageStatus !== 2 && imageStatus !== 3}}"
|
||||
class="footer"
|
||||
>
|
||||
<block wx:if="{{editMode}}">
|
||||
<view class="btn btn-primary" bindtap="onSave">保存</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="btn-danger-text delete-btn" bindtap="onDelete">删除这件衣物</view>
|
||||
</block>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部悬浮操作条 -->
|
||||
<view
|
||||
wx:if="{{!readonly && !notFound && imageStatus !== 1 && imageStatus !== 2 && imageStatus !== 3 && imageStatus !== 5}}"
|
||||
class="dock"
|
||||
>
|
||||
<block wx:if="{{editMode}}">
|
||||
<view class="btn btn--ink btn--block" bindtap="onSave">保存</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="dock-row">
|
||||
<view class="btn--text" bindtap="onDelete">删除这件衣物</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 底部属性选择面板 -->
|
||||
<view class="sheet-mask" wx:if="{{sheetVisible}}" bindtap="onSheetClose"></view>
|
||||
<view class="sheet" wx:if="{{sheetVisible}}">
|
||||
<view class="sheet-head">
|
||||
<text class="sheet-title">{{sheetLabel}}</text>
|
||||
<view class="sheet-done" bindtap="onSheetConfirm">完成</view>
|
||||
<view wx:if="{{sheetVisible}}" class="scrim" bindtap="onSheetClose"></view>
|
||||
<view wx:if="{{sheetVisible}}" class="sheet-wrap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheet-grab"></view>
|
||||
<view class="sheet-hd">
|
||||
<text class="sheet-t">{{sheetLabel}}</text>
|
||||
<text class="sheet-x" bindtap="onSheetConfirm">完成</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="sheet-body">
|
||||
<!-- 评分:大号点选圆点 -->
|
||||
@@ -254,7 +270,7 @@ module.exports = { hex: hex, tags: tags }
|
||||
data-name="{{item.name}}"
|
||||
bindtap="onSheetColorTap"
|
||||
>
|
||||
<view class="sheet-color-dot" style="background: {{item.value}}"></view>
|
||||
<view class="sheet-color-dot" style="background: {{item.value}};"></view>
|
||||
</view>
|
||||
</view>
|
||||
<input
|
||||
@@ -262,6 +278,7 @@ module.exports = { hex: hex, tags: tags }
|
||||
value="{{sheetValue}}"
|
||||
bindinput="onSheetInput"
|
||||
placeholder="也可输入色值,如 #FFFFFF"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
</block>
|
||||
|
||||
@@ -281,9 +298,21 @@ module.exports = { hex: hex, tags: tags }
|
||||
value="{{sheetValue}}"
|
||||
bindinput="onSheetInput"
|
||||
placeholder="请输入{{sheetLabel}}"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
</block>
|
||||
</scroll-view>
|
||||
<view class="safe-bottom"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 删除确认:v2 玻璃弹窗 -->
|
||||
<v2-overlay
|
||||
visible="{{delVisible}}"
|
||||
type="modal"
|
||||
title="删除这件衣物"
|
||||
content="{{delText}}"
|
||||
buttons="{{delButtons}}"
|
||||
bind:confirm="onDelConfirm"
|
||||
bind:cancel="onDelCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.detail-page {
|
||||
/** 衣物详情:买手店目录式大图 + 规格表 + 悬浮底部操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,474 +11,159 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
font-size: 30rpx;
|
||||
color: #007AFF;
|
||||
padding: 0 8rpx;
|
||||
.cd-note {
|
||||
margin-top: 20rpx;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.img-box {
|
||||
margin: 24rpx;
|
||||
height: 640rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
.cd-photo {
|
||||
margin: 32rpx var(--pad) 0;
|
||||
aspect-ratio: 4 / 5;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.img {
|
||||
.cd-photo .ph {
|
||||
width: 46%;
|
||||
height: 46%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.cd-photo .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.img-placeholder {
|
||||
color: #C7C7CC;
|
||||
.cd-name {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.img-placeholder .icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
.name-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.name-text {
|
||||
.cd-input {
|
||||
font-family: var(--serif);
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
width: 100%;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 1rpx solid var(--hairline-2);
|
||||
}
|
||||
|
||||
/* ===== 属性分组卡片 ===== */
|
||||
.card.attr-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
.cd-group {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
padding: 14rpx 32rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #8E8E93;
|
||||
background: rgba(242, 242, 247, 0.6);
|
||||
border-bottom: 1rpx solid #E5E5EA;
|
||||
.cd-group .grp-hd {
|
||||
padding-bottom: 22rpx;
|
||||
}
|
||||
|
||||
.attr-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 92rpx;
|
||||
padding: 0 32rpx;
|
||||
box-sizing: border-box;
|
||||
.spec.is-edit:active {
|
||||
background: var(--fill);
|
||||
}
|
||||
|
||||
.attr-row:last-child {
|
||||
border-bottom: none;
|
||||
.spec-t {
|
||||
font-size: 29rpx;
|
||||
}
|
||||
|
||||
.attr-row.edit:active {
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.attr-label {
|
||||
width: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.attr-value {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.attr-text {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.attr-text.weak {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
.cd-score {
|
||||
margin-left: 16rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
flex-shrink: 0;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* ===== 评分:5 段圆条 ===== */
|
||||
.score-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.score-dot {
|
||||
width: 28rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background: #E5E5EA;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.score-dot:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.score-dot.on {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.score-dot.violet.on {
|
||||
background: #5856D6;
|
||||
}
|
||||
|
||||
.score-num {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-left: 16rpx;
|
||||
min-width: 60rpx;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ===== 适用场合标签 ===== */
|
||||
.scene-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.cd-tags {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.scene-tag {
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.scene-tag:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.edit-arrow {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
.cd-arrow {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
color: var(--ink-4);
|
||||
margin-left: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 备注卡片 ===== */
|
||||
.card.remark-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
.cd-remark {
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
|
||||
.remark-body {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.remark-text {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.remark-text.weak {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
/* ===== 底部属性选择面板 ===== */
|
||||
.sheet-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 99;
|
||||
animation: mask-in 0.2s ease;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
max-height: 72vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: sheet-up 0.25s ease;
|
||||
}
|
||||
|
||||
.sheet-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx 12rpx;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.sheet-done {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #007AFF;
|
||||
padding: 8rpx 0 8rpx 32rpx;
|
||||
}
|
||||
|
||||
.sheet-done:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
padding: 12rpx 32rpx 32rpx;
|
||||
max-height: 56vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 选项胶囊 */
|
||||
.sheet-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sheet-tag {
|
||||
padding: 16rpx 30rpx;
|
||||
margin: 0 16rpx 16rpx 0;
|
||||
border-radius: 18rpx;
|
||||
background: #F2F2F7;
|
||||
color: #3C3C43;
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sheet-tag.on {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sheet-tag:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.sheet-input {
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 色板 */
|
||||
.sheet-colors {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sheet-color {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin: 0 20rpx 20rpx 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 4rpx solid transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sheet-color.on {
|
||||
border-color: #007AFF;
|
||||
}
|
||||
|
||||
.sheet-color-dot {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
}
|
||||
|
||||
/* 评分点选 */
|
||||
/* ===== 属性选择面板 ===== */
|
||||
.sheet-score {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 0 8rpx;
|
||||
gap: 20rpx;
|
||||
padding: 32rpx var(--pad);
|
||||
}
|
||||
|
||||
.sheet-score-dot {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
color: #8E8E93;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 108rpx;
|
||||
line-height: 108rpx;
|
||||
text-align: center;
|
||||
border-radius: var(--r-ctrl);
|
||||
background: var(--fill);
|
||||
font-size: 32rpx;
|
||||
color: var(--ink-2);
|
||||
transition: background .18s var(--ease), color .18s var(--ease);
|
||||
}
|
||||
|
||||
.sheet-score-dot.on {
|
||||
background: #007AFF;
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sheet-score-dot:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes sheet-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes mask-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 16rpx 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
text-align: center;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
/* 处理中 */
|
||||
.processing-card {
|
||||
margin: 24rpx;
|
||||
padding: 64rpx 32rpx;
|
||||
.sheet-tags {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx var(--pad);
|
||||
}
|
||||
|
||||
.proc-icon {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
color: #C7C7CC;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 48rpx;
|
||||
.sheet-tag {
|
||||
padding: 16rpx 32rpx;
|
||||
border-radius: var(--r-pill);
|
||||
background: var(--fill);
|
||||
font-size: 27rpx;
|
||||
color: var(--ink-2);
|
||||
transition: background .18s var(--ease), color .18s var(--ease);
|
||||
}
|
||||
|
||||
.proc-icon .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.step-text.active-text {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #E5E5EA;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 100rpx;
|
||||
height: 2rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 16rpx 34rpx;
|
||||
}
|
||||
|
||||
.step-line.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.proc-text {
|
||||
margin-top: 48rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.proc-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 识别失败 */
|
||||
.failed-box {
|
||||
background: #FFF3F3;
|
||||
}
|
||||
|
||||
.fail-big-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background: #FF3B30;
|
||||
.sheet-tag.on {
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
font-size: 72rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sheet-colors {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx var(--pad) 8rpx;
|
||||
}
|
||||
|
||||
.fail-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
.sheet-color {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, .12);
|
||||
}
|
||||
|
||||
.fail-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.sheet-color.on {
|
||||
box-shadow: 0 0 0 4rpx var(--ink);
|
||||
}
|
||||
|
||||
.fail-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
line-height: 1.5;
|
||||
.sheet-color-dot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.sheet-input {
|
||||
margin: 24rpx var(--pad);
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { HoroscopeData } from '../../types/models'
|
||||
import { setTabBarSelected } from '../../utils/tabbar'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { STORAGE_KEYS } from '../../utils/config'
|
||||
|
||||
@@ -32,13 +33,19 @@ Page({
|
||||
data: {
|
||||
horoscope: null as HoroscopeData | null,
|
||||
statDate: '',
|
||||
luckyColorHex: '#007AFF',
|
||||
// 幸运色是唯一允许出现彩色的业务数据;取不到色值时退回中性灰
|
||||
luckyColorHex: '#AEAEB2',
|
||||
loading: true,
|
||||
noBirth: false,
|
||||
error: false,
|
||||
errorText: '',
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
@@ -72,7 +79,7 @@ Page({
|
||||
this.setData({
|
||||
horoscope: res,
|
||||
statDate: this.formatStatDate(res.stat_date),
|
||||
luckyColorHex: COLOR_MAP[res.lucky_color] || '#007AFF',
|
||||
luckyColorHex: COLOR_MAP[res.lucky_color] || '#AEAEB2',
|
||||
loading: false,
|
||||
noBirth: false,
|
||||
error: false,
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="每日运势" back="{{false}}">
|
||||
<navigation-bar title="每日运势" back="{{false}}" scrolled="{{navScrolled}}">
|
||||
<view slot="right" class="nav-action" bindtap="onGoProfile">去设置生日</view>
|
||||
</navigation-bar>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<scroll-view scroll-y class="scroll-body pb-tab" bindscroll="onNavScroll">
|
||||
<!-- 大标题页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Daily Fortune</view>
|
||||
<view class="display">每日运势</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading}}" class="sk-wrap">
|
||||
<view class="skeleton sk-big"></view>
|
||||
<view class="skeleton sk-mid"></view>
|
||||
<view class="skeleton sk-mid"></view>
|
||||
<view wx:if="{{loading}}" class="hs-skel">
|
||||
<view class="sk sk-ln" style="width: 38%; height: 28rpx;"></view>
|
||||
<view class="sk hs-skel-num"></view>
|
||||
<view class="sk sk-ln" style="width: 100%;"></view>
|
||||
<view class="sk sk-ln" style="width: 100%;"></view>
|
||||
<view class="sk sk-ln" style="width: 92%;"></view>
|
||||
<view class="sk sk-ln" style="width: 84%;"></view>
|
||||
</view>
|
||||
|
||||
<!-- 未设置生日 -->
|
||||
@@ -31,72 +40,104 @@
|
||||
bind:action="onRetry"
|
||||
/>
|
||||
|
||||
<!-- 运势内容 -->
|
||||
<view wx:elif="{{horoscope}}" class="content">
|
||||
<!-- 星座大卡 -->
|
||||
<view class="card horo-card">
|
||||
<view class="horo-top">
|
||||
<view class="zodiac">
|
||||
<view class="icon icon-sparkles zodiac-icon"></view>
|
||||
<text class="zodiac-name">{{horoscope.zodiac}}</text>
|
||||
</view>
|
||||
<view class="horo-date">
|
||||
<text>{{statDate}}</text>
|
||||
<text class="nav-action" bindtap="onGoProfile">去设置生日</text>
|
||||
<!-- 运势内容:杂志内页排版 -->
|
||||
<block wx:elif="{{horoscope}}">
|
||||
<view class="hs-hero">
|
||||
<view class="hs-zodiac">
|
||||
<view class="icon icon-sparkles"></view>
|
||||
<text class="t-xl">{{horoscope.zodiac}}</text>
|
||||
</view>
|
||||
<text class="hs-date num">{{statDate}}</text>
|
||||
</view>
|
||||
|
||||
<view class="overall">
|
||||
<text class="overall-label">综合运势</text>
|
||||
<view class="overall-score">
|
||||
<text class="score-num">{{horoscope.overall_score}}.0</text>
|
||||
<view class="stars">
|
||||
<view class="hs-score">
|
||||
<view class="hs-score-main">
|
||||
<view class="eyebrow">综合运势</view>
|
||||
<view class="hs-num num">{{horoscope.overall_score}}.0</view>
|
||||
</view>
|
||||
<view class="hs-stars">
|
||||
<view
|
||||
class="icon star {{index < horoscope.overall_score ? 'icon-star-fill on' : 'icon-star'}}"
|
||||
wx:for="{{[1, 2, 3, 4, 5]}}"
|
||||
wx:key="*this"
|
||||
class="icon {{index < horoscope.overall_score ? 'icon-star-fill' : 'icon-star'}}"
|
||||
></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 三项评分 -->
|
||||
<view class="scores">
|
||||
<view class="score-row">
|
||||
<text class="score-label">爱情运势</text>
|
||||
<view class="score-bar"><view class="score-fill" style="width: {{horoscope.love_score * 20}}%"></view></view>
|
||||
<text class="score-value">{{horoscope.love_score}}/5</text>
|
||||
<!-- 三项评分:横向发丝线刻度尺 -->
|
||||
<view class="hs-scales">
|
||||
<view class="hs-scale">
|
||||
<view class="hs-st">
|
||||
<text class="k">爱情运势</text>
|
||||
<text class="v num">{{horoscope.love_score}}/5</text>
|
||||
</view>
|
||||
<view class="score-row">
|
||||
<text class="score-label">工作运势</text>
|
||||
<view class="score-bar"><view class="score-fill" style="width: {{horoscope.career_score * 20}}%"></view></view>
|
||||
<text class="score-value">{{horoscope.career_score}}/5</text>
|
||||
<view class="scale">
|
||||
<view class="scale-ticks">
|
||||
<view class="tk" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this"></view>
|
||||
</view>
|
||||
<view class="score-row">
|
||||
<text class="score-label">财富运势</text>
|
||||
<view class="score-bar"><view class="score-fill" style="width: {{horoscope.wealth_score * 20}}%"></view></view>
|
||||
<text class="score-value">{{horoscope.wealth_score}}/5</text>
|
||||
<view class="scale-line"></view>
|
||||
<view class="scale-fill" style="width: {{horoscope.love_score * 20}}%;"></view>
|
||||
<view class="scale-pip" style="left: {{horoscope.love_score * 20}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="daily-text">
|
||||
<text class="daily-label">今日运势</text>
|
||||
<text class="daily-content">{{horoscope.daily_text}}</text>
|
||||
<view class="hs-scale">
|
||||
<view class="hs-st">
|
||||
<text class="k">工作运势</text>
|
||||
<text class="v num">{{horoscope.career_score}}/5</text>
|
||||
</view>
|
||||
<view class="scale">
|
||||
<view class="scale-ticks">
|
||||
<view class="tk" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this"></view>
|
||||
</view>
|
||||
<view class="scale-line"></view>
|
||||
<view class="scale-fill" style="width: {{horoscope.career_score * 20}}%;"></view>
|
||||
<view class="scale-pip" style="left: {{horoscope.career_score * 20}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="lucky-row">
|
||||
<view class="lucky-card">
|
||||
<view class="lucky-dot" style="background: {{luckyColorHex}}"></view>
|
||||
<text class="lucky-label">幸运色 · {{horoscope.lucky_color}}</text>
|
||||
<view class="hs-scale">
|
||||
<view class="hs-st">
|
||||
<text class="k">财富运势</text>
|
||||
<text class="v num">{{horoscope.wealth_score}}/5</text>
|
||||
</view>
|
||||
<view class="lucky-card">
|
||||
<text class="lucky-number">{{horoscope.lucky_number}}</text>
|
||||
<text class="lucky-label">幸运数字</text>
|
||||
<view class="scale">
|
||||
<view class="scale-ticks">
|
||||
<view class="tk" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this"></view>
|
||||
</view>
|
||||
<view class="scale-line"></view>
|
||||
<view class="scale-fill" style="width: {{horoscope.wealth_score * 20}}%;"></view>
|
||||
<view class="scale-pip" style="left: {{horoscope.wealth_score * 20}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="gen-btn btn btn-outline" bindtap="onGenerate">生成今日推荐穿搭</view>
|
||||
<!-- 今日运势:衬线引文排版 -->
|
||||
<view class="hs-quote">
|
||||
<view class="eyebrow">Today</view>
|
||||
<view class="t-quote">{{horoscope.daily_text}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 幸运色 / 幸运数字:两栏发丝线小格 -->
|
||||
<view class="hs-lucky">
|
||||
<view class="hs-lucky-cell">
|
||||
<view class="lk">幸运色</view>
|
||||
<view class="lv">
|
||||
<view class="dot" style="background: {{luckyColorHex}};"></view>
|
||||
<text>{{horoscope.lucky_color}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="hs-lucky-cell">
|
||||
<view class="lk">幸运数字</view>
|
||||
<view class="lv num">{{horoscope.lucky_number}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="hs-foot">
|
||||
<view class="btn btn--ghost btn--block" bindtap="onGenerate">生成今日推荐穿搭</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 运势:杂志内页 —— 巨大衬线星座名 / 超大数字 / 发丝线刻度尺 / 衬线引文 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,203 +11,165 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-action {
|
||||
font-size: 26rpx;
|
||||
color: #007AFF;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.sk-wrap {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.sk-big {
|
||||
height: 560rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.sk-mid {
|
||||
height: 200rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.horo-card {
|
||||
background: #E8F2FF;
|
||||
}
|
||||
|
||||
.horo-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.zodiac {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zodiac-icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
color: #007AFF;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.zodiac-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.horo-date {
|
||||
.hs-skel {
|
||||
padding: 60rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
gap: 44rpx;
|
||||
}
|
||||
|
||||
.overall {
|
||||
margin-top: 40rpx;
|
||||
.hs-skel-num {
|
||||
width: 56%;
|
||||
height: 128rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.overall-label {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
.hs-hero {
|
||||
padding: 44rpx var(--pad) 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.overall-score {
|
||||
.hs-zodiac {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12rpx;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.score-num {
|
||||
font-size: 120rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
line-height: 1;
|
||||
margin-right: 32rpx;
|
||||
}
|
||||
|
||||
.stars {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.star {
|
||||
.hs-zodiac .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: #C7C7CC;
|
||||
color: var(--ink-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.star.on {
|
||||
color: #007AFF;
|
||||
.hs-date {
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.scores {
|
||||
margin-top: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
.hs-score {
|
||||
padding: 56rpx var(--pad) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
align-items: flex-end;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
width: 160rpx;
|
||||
font-size: 26rpx;
|
||||
color: #000;
|
||||
.hs-score-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.score-bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
background: #F2F2F7;
|
||||
border-radius: 6rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.score-fill {
|
||||
height: 100%;
|
||||
background: #007AFF;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
width: 80rpx;
|
||||
text-align: right;
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.daily-text {
|
||||
margin-top: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.daily-label {
|
||||
/* 单项评分块(行内纵向排布,由 .hs-scales 的 gap 控制间距) */
|
||||
.hs-scale {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hs-num {
|
||||
margin-top: 24rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 152rpx;
|
||||
font-weight: 500;
|
||||
line-height: .84;
|
||||
letter-spacing: -.035em;
|
||||
}
|
||||
|
||||
.hs-stars {
|
||||
display: flex;
|
||||
gap: 8rpx;
|
||||
padding-bottom: 18rpx;
|
||||
}
|
||||
|
||||
.hs-stars .icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.hs-scales {
|
||||
padding: 64rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 38rpx;
|
||||
}
|
||||
|
||||
.hs-st {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
|
||||
.hs-st .k {
|
||||
font-size: 26rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.hs-st .v {
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.hs-quote {
|
||||
padding: 64rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.hs-quote .t-quote {
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.hs-lucky {
|
||||
display: flex;
|
||||
margin: 64rpx var(--pad) 0;
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.hs-lucky-cell {
|
||||
flex: 1;
|
||||
padding: 32rpx 0 34rpx;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hs-lucky-cell + .hs-lucky-cell::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 32rpx;
|
||||
bottom: 34rpx;
|
||||
width: 1rpx;
|
||||
background: var(--hairline);
|
||||
}
|
||||
|
||||
.hs-lucky .lk {
|
||||
font-size: 19rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.daily-content {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 12rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.lucky-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.lucky-card {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
.hs-lucky .lv {
|
||||
margin-top: 20rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 38rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.lucky-dot {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
.hs-lucky .lv .dot {
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 16rpx;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(0, 0, 0, .14);
|
||||
}
|
||||
|
||||
.lucky-number {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.lucky-label {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.gen-btn {
|
||||
margin-top: 24rpx;
|
||||
width: 100%;
|
||||
.hs-foot {
|
||||
padding: 56rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
"usingComponents": {
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@ Page({
|
||||
userInfo: {} as UserInfo,
|
||||
giftVisible: false,
|
||||
giftItems: [] as GiftItem[],
|
||||
// 生日引导:v2 玻璃弹窗
|
||||
birthVisible: false,
|
||||
birthButtons: [
|
||||
{ text: '稍后再说', value: '' },
|
||||
{ text: '去完善', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
@@ -180,29 +186,30 @@ Page({
|
||||
},
|
||||
|
||||
showBirthGuide() {
|
||||
wx.showModal({
|
||||
title: '完善个人信息',
|
||||
content: '设置生日后即可查看今日星座运势',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ birthVisible: true })
|
||||
},
|
||||
|
||||
onBirthCancel() {
|
||||
this.setData({ birthVisible: false })
|
||||
this.goAfterLogin()
|
||||
},
|
||||
|
||||
onBirthConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ birthVisible: false })
|
||||
if (e.detail.value !== 'ok') {
|
||||
this.goAfterLogin()
|
||||
return
|
||||
}
|
||||
// switchTab 不支持 query 传参,用 storage 标记让「我的」页 onShow 自动弹出编辑弹窗
|
||||
wx.setStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT, 1)
|
||||
wx.switchTab({
|
||||
url: '/pages/profile/profile',
|
||||
fail: (e) => {
|
||||
logger.warn(LOG, 'switchTab 个人资料失败, err=', e && e.errMsg)
|
||||
fail: (err) => {
|
||||
logger.warn(LOG, 'switchTab 个人资料失败, err=', err && err.errMsg)
|
||||
// 跳转失败时清除标记,避免下次进入「我的」页意外弹窗
|
||||
wx.removeStorageSync(STORAGE_KEYS.OPEN_PROFILE_EDIT)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.goAfterLogin()
|
||||
}
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '生日引导弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
openAgreement() {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<view class="login-page">
|
||||
<!-- 顶部插画区 -->
|
||||
<view class="hero">
|
||||
<view class="icon hero-icon hero-1 icon-hanger"></view>
|
||||
<view class="icon hero-icon hero-2 icon-shirt"></view>
|
||||
<view class="icon hero-icon hero-3 icon-sparkles"></view>
|
||||
<view class="icon hero-icon hero-4 icon-camera"></view>
|
||||
<view class="icon hero-icon hero-5 icon-star"></view>
|
||||
<!-- 悬浮线稿装饰:唯一色彩仍是黑白灰 -->
|
||||
<view class="lg-hero">
|
||||
<view class="icon lg-ic lg-ic1 icon-hanger"></view>
|
||||
<view class="icon lg-ic lg-ic2 icon-shirt"></view>
|
||||
<view class="icon lg-ic lg-ic3 icon-sparkles"></view>
|
||||
<view class="icon lg-ic lg-ic4 icon-camera"></view>
|
||||
<view class="icon lg-ic lg-ic5 icon-star"></view>
|
||||
</view>
|
||||
|
||||
<!-- Logo 与产品名 -->
|
||||
<view class="brand">
|
||||
<view class="brand-logo">
|
||||
<!-- 品牌区 -->
|
||||
<view class="lg-top">
|
||||
<view class="lg-logo">
|
||||
<view class="icon icon-hanger"></view>
|
||||
</view>
|
||||
<view class="brand-name">纸条AI衣橱</view>
|
||||
<view class="brand-slogan">把你的衣橱,交给 AI 打理</view>
|
||||
<view class="lg-name">纸条AI衣橱</view>
|
||||
<view class="lg-slogan">把你的衣橱,交给 AI 打理</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部登录区 -->
|
||||
<view class="login-footer">
|
||||
<view class="btn btn-primary login-btn {{loading ? 'disabled' : ''}}" bindtap="onLogin">
|
||||
<view class="lg-foot">
|
||||
<view class="btn btn--ink btn--block {{loading ? 'is-off' : ''}}" bindtap="onLogin">
|
||||
<view class="wechat-icon"></view>
|
||||
<text>{{loading ? '登录中…' : '微信一键登录'}}</text>
|
||||
</view>
|
||||
<view class="agreement">
|
||||
<view class="lg-terms">
|
||||
<text>登录即代表同意</text>
|
||||
<text class="link" bindtap="openAgreement">《用户协议》</text>
|
||||
<text>和</text>
|
||||
@@ -31,20 +31,38 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新手礼包弹窗 -->
|
||||
<view wx:if="{{giftVisible}}" class="gift-mask">
|
||||
<view class="gift-card">
|
||||
<view class="gift-badge"><view class="icon icon-sparkles"></view></view>
|
||||
<view class="gift-title">欢迎加入纸条AI衣橱</view>
|
||||
<view class="gift-sub">新人礼包已到账,请查收</view>
|
||||
<view class="gift-list">
|
||||
<view class="gift-item" wx:for="{{giftItems}}" wx:key="name">
|
||||
<view class="gift-item-icon icon {{item.icon}}"></view>
|
||||
<text class="gift-item-name">{{item.name}}</text>
|
||||
<text class="gift-item-val">{{item.value}}</text>
|
||||
<!-- 新手礼包:居中玻璃卡 -->
|
||||
<view wx:if="{{giftVisible}}" class="scrim"></view>
|
||||
<view wx:if="{{giftVisible}}" class="modal-wrap">
|
||||
<view class="modal">
|
||||
<view class="modal-body">
|
||||
<view class="lg-gift-ic">
|
||||
<view class="icon icon-sparkles"></view>
|
||||
</view>
|
||||
<view class="modal-title">欢迎加入纸条AI衣橱</view>
|
||||
<view class="modal-text">新人礼包已到账,请查收</view>
|
||||
<view class="lg-gift-list">
|
||||
<view class="lg-gift-row" wx:for="{{giftItems}}" wx:key="name">
|
||||
<view class="icon {{item.icon}}"></view>
|
||||
<text class="gn">{{item.name}}</text>
|
||||
<text class="gv num">{{item.value}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="btn btn-primary gift-btn" bindtap="onGiftConfirm">开心收下</view>
|
||||
</view>
|
||||
<view class="modal-acts">
|
||||
<view class="modal-act modal-act--main" bindtap="onGiftConfirm">开心收下</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 生日引导 -->
|
||||
<v2-overlay
|
||||
visible="{{birthVisible}}"
|
||||
type="modal"
|
||||
title="完善个人信息"
|
||||
content="设置生日后即可查看今日星座运势"
|
||||
buttons="{{birthButtons}}"
|
||||
bind:confirm="onBirthConfirm"
|
||||
bind:cancel="onBirthCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,194 +1,150 @@
|
||||
/** 登录:全屏纯白 + 居中线稿 Logo + 衬线品牌名 + 底部实心黑胶囊 **/
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: #fff;
|
||||
background: var(--paper);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 顶部插画区 */
|
||||
.hero {
|
||||
/* 装饰线稿 */
|
||||
.lg-hero {
|
||||
position: relative;
|
||||
height: 480rpx;
|
||||
background: #E8F2FF;
|
||||
border-radius: 0 0 48rpx 48rpx;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 340rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.hero-icon {
|
||||
.lg-ic {
|
||||
position: absolute;
|
||||
color: #007AFF;
|
||||
opacity: 0.85;
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
.hero-1 { width: 140rpx; height: 140rpx; left: 60rpx; top: 120rpx; transform: rotate(-12deg); }
|
||||
.hero-2 { width: 120rpx; height: 120rpx; right: 80rpx; top: 80rpx; transform: rotate(10deg); color: #5856D6; }
|
||||
.hero-3 { width: 100rpx; height: 100rpx; left: 160rpx; top: 280rpx; transform: rotate(8deg); }
|
||||
.hero-4 { width: 110rpx; height: 110rpx; right: 170rpx; top: 260rpx; transform: rotate(-6deg); color: #5856D6; }
|
||||
.hero-5 { width: 70rpx; height: 70rpx; right: 60rpx; top: 340rpx; transform: rotate(15deg); }
|
||||
.lg-ic1 { left: 5%; top: 12%; width: 60rpx; height: 60rpx; transform: rotate(-13deg); opacity: .52; }
|
||||
.lg-ic2 { left: 27%; top: 62%; width: 42rpx; height: 42rpx; transform: rotate(9deg); opacity: .34; }
|
||||
.lg-ic3 { right: 7%; top: 16%; width: 52rpx; height: 52rpx; transform: rotate(15deg); opacity: .46; }
|
||||
.lg-ic4 { right: 24%; bottom: 4%; width: 40rpx; height: 40rpx; transform: rotate(-7deg); opacity: .3; }
|
||||
.lg-ic5 { left: 13%; bottom: 8%; width: 34rpx; height: 34rpx; opacity: .28; }
|
||||
|
||||
/* 品牌区 */
|
||||
.brand {
|
||||
.lg-top {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 64rpx;
|
||||
padding: 0 64rpx 120rpx;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
.lg-logo {
|
||||
width: 124rpx;
|
||||
height: 124rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 44rpx;
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 0 0 1rpx var(--hairline-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 122, 255, 0.25);
|
||||
}
|
||||
|
||||
.brand-logo .icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
.lg-logo .icon {
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
.lg-name {
|
||||
font-family: var(--serif);
|
||||
font-size: 54rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: .08em;
|
||||
text-indent: .08em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-slogan {
|
||||
font-size: 28rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 16rpx;
|
||||
.lg-slogan {
|
||||
margin-top: 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--ink-2);
|
||||
letter-spacing: .03em;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.login-footer {
|
||||
padding: 0 64rpx;
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
.lg-foot {
|
||||
padding: 0 56rpx calc(52rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
margin-right: 12rpx;
|
||||
background-color: #fff;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M9.5 3C5.36 3 2 5.69 2 9c0 1.87 1.03 3.54 2.62 4.65L4 16.5l3.27-1.72c.73.2 1.47.3 2.23.3h.5A6.2 6.2 0 0 1 9 12c0-3.3 3.36-6 7.5-6h.5A5.7 5.7 0 0 0 9.5 3z'/%3E%3Cpath d='M22 12c0-2.76-2.69-5-6-5s-6 2.24-6 5 2.69 5 6 5c.72 0 1.42-.1 2.06-.28L20.5 18l-.42-2.6c1.17-.88 1.92-2.06 1.92-3.4z'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Cpath d='M9.5 3C5.36 3 2 5.69 2 9c0 1.87 1.03 3.54 2.62 4.65L4 16.5l3.27-1.72c.73.2 1.47.3 2.23.3h.5A6.2 6.2 0 0 1 9 12c0-3.3 3.36-6 7.5-6h.5A5.7 5.7 0 0 0 9.5 3z'/%3E%3Cpath d='M22 12c0-2.76-2.69-5-6-5s-6 2.24-6 5 2.69 5 6 5c.72 0 1.42-.1 2.06-.28L20.5 18l-.42-2.6c1.17-.88 1.92-2.06 1.92-3.4z'/%3E%3C/svg%3E") no-repeat center / 100% 100%;
|
||||
}
|
||||
|
||||
.agreement {
|
||||
margin-top: 24rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
.lg-terms {
|
||||
margin-top: 28rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
line-height: 1.75;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #007AFF;
|
||||
.lg-terms .link {
|
||||
color: var(--ink-2);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 6rpx;
|
||||
text-decoration-thickness: 1rpx;
|
||||
text-decoration-color: var(--ink-4);
|
||||
}
|
||||
|
||||
/* 新手礼包弹窗 */
|
||||
.gift-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 300;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gift-card {
|
||||
width: 560rpx;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
padding: 48rpx 40rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.gift-badge {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
/* 新手礼包 */
|
||||
.lg-gift-ic {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #FF9500, #FF2D55);
|
||||
color: #fff;
|
||||
margin: 0 auto 32rpx;
|
||||
color: var(--ink);
|
||||
box-shadow: inset 0 0 0 1rpx var(--hairline-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.gift-badge .icon {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
}
|
||||
|
||||
.gift-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.gift-sub {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.gift-list {
|
||||
width: 100%;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.gift-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid #F2F2F7;
|
||||
}
|
||||
|
||||
.gift-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.gift-item-icon {
|
||||
.lg-gift-ic .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
color: #007AFF;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.lg-gift-list {
|
||||
margin-top: 32rpx;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.lg-gift-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 22rpx 0;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.lg-gift-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.lg-gift-row .icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
color: var(--ink-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gift-item-name {
|
||||
.lg-gift-row .gn {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.gift-item-val {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.gift-btn {
|
||||
width: 100%;
|
||||
.lg-gift-row .gv {
|
||||
font-size: 24rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { QuotaItem, UserInfo, GoodsItem, QuotaFlowItem, PagedResult, VirtualPayR
|
||||
import { QUOTA_NAMES, GoodsType, GOODS_TYPE_NAMES } from '../../utils/config'
|
||||
import { fenToYuan, formatDate, formatDateTime } from '../../utils/format'
|
||||
import { normalizePaged } from '../../utils/paged'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -55,8 +56,13 @@ Page({
|
||||
orderGoods: null as GoodsItem | null,
|
||||
submitting: false, // 下单防连点:连点「立即支付」会重复创建虚拟支付订单
|
||||
loading: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
logger.debug(LOG, 'onLoad, query=', JSON.stringify(query))
|
||||
if (query.tab === 'flow') this.setData({ view: 'flow' })
|
||||
|
||||
@@ -1,95 +1,123 @@
|
||||
<view class="page mall-page">
|
||||
<navigation-bar title="额度与会员" />
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="额度与会员" scrolled="{{navScrolled}}" />
|
||||
|
||||
<!-- 商城视图 -->
|
||||
<scroll-view wx:if="{{view === 'mall'}}" scroll-y class="scroll-body">
|
||||
<!-- 会员卡 -->
|
||||
<view class="card member-card">
|
||||
<view class="member-left">
|
||||
<view class="icon icon-crown member-icon"></view>
|
||||
<view>
|
||||
<view class="member-name">{{isVip ? '月度会员' : '开通会员'}}</view>
|
||||
<view class="member-desc">{{isVip ? vipExpireText : '开通会员,解锁更多 AI 与上传权益'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-btn" bindtap="onTapMember">{{isVip ? '立即续费' : '立即开通'}}</view>
|
||||
<scroll-view wx:if="{{view === 'mall'}}" scroll-y class="scroll-body pb-safe" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Membership</view>
|
||||
<view class="display">额度与会员</view>
|
||||
</view>
|
||||
|
||||
<!-- 额度网格 -->
|
||||
<view class="quota-grid">
|
||||
<view class="quota-cell" wx:for="{{quotas}}" wx:key="quota_type">
|
||||
<view class="quota-name">{{item._name}}</view>
|
||||
<view class="quota-num">{{item.remain}}<text class="quota-total">/ {{item.total}}</text></view>
|
||||
<view class="quota-bar">
|
||||
<view class="quota-fill {{item._percent >= 80 ? 'warn' : ''}}" style="width: {{item._percent}}%"></view>
|
||||
<!-- 会员卡:全稿唯一深色块 -->
|
||||
<view class="ml-member">
|
||||
<view class="ml-member-l">
|
||||
<view class="icon icon-crown"></view>
|
||||
<view class="ml-member-body">
|
||||
<view class="ml-member-n">{{isVip ? '月度会员' : '开通会员'}}</view>
|
||||
<view class="ml-member-d">{{isVip ? vipExpireText : '开通会员,解锁更多 AI 与上传权益'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ml-member-b" bindtap="onTapMember">{{isVip ? '立即续费' : '立即开通'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的额度:四栏发丝线数字条 -->
|
||||
<view class="grp-hd ml-grp"><text class="gh-cn">我的额度</text><text class="gh-en">Quota</text></view>
|
||||
<view class="stats">
|
||||
<view class="stat" wx:for="{{quotas}}" wx:key="quota_type">
|
||||
<view class="stat-n num">{{item.remain}}<text class="unit">/{{item.total}}</text></view>
|
||||
<view class="stat-l">{{item._name}}</view>
|
||||
<view class="track ml-track">
|
||||
<view class="bar" style="width: {{item._percent}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 额度明细入口 -->
|
||||
<view class="card flow-entry" bindtap="onSwitchFlow">
|
||||
<text>额度明细</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
<view class="rows ml-flow-entry">
|
||||
<view class="row" bindtap="onSwitchFlow">
|
||||
<view class="icon row-ic icon-doc"></view>
|
||||
<text class="row-lb">额度明细</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商品筛选 -->
|
||||
<scroll-view scroll-x class="goods-filter" show-scrollbar="{{false}}">
|
||||
<view class="filter-row">
|
||||
<view class="ml-filters">
|
||||
<scroll-view scroll-x class="seg-scroll" show-scrollbar="{{false}}">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{goodsFilters}}"
|
||||
wx:key="name"
|
||||
class="filter-item {{activeFilter === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeFilter === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onGoodsFilter"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="goods-list">
|
||||
<view class="goods-card card" wx:for="{{goods}}" wx:key="goods_code">
|
||||
<view class="icon {{item._icon}} goods-icon"></view>
|
||||
<view class="goods-info">
|
||||
<view class="goods-name">{{item.goods_name}}</view>
|
||||
<view class="goods-desc">{{item.description}}</view>
|
||||
<!-- 商品列表:左线稿图标 / 中名称描述 / 右价格 + 黑胶囊购买 -->
|
||||
<view class="rows">
|
||||
<view class="row ml-goods" wx:for="{{goods}}" wx:key="goods_code">
|
||||
<view class="icon row-ic {{item._icon}}"></view>
|
||||
<view class="ml-goods-body">
|
||||
<view class="ml-goods-n">{{item.goods_name}}</view>
|
||||
<view class="ml-goods-d">{{item.description}}</view>
|
||||
</view>
|
||||
<view class="goods-right">
|
||||
<view class="goods-price num">{{item._priceText}}</view>
|
||||
<view class="goods-origin num" wx:if="{{item.price > item.discount_price}}">{{item._originText}}</view>
|
||||
<view class="buy-btn" data-goods="{{item}}" bindtap="onBuy">购买</view>
|
||||
<view class="ml-goods-r">
|
||||
<view class="ml-price num">{{item._priceText}}</view>
|
||||
<view class="ml-origin num" wx:if="{{item.price > item.discount_price}}">{{item._originText}}</view>
|
||||
</view>
|
||||
<view class="btn btn--ink btn--sm ml-buy" data-goods="{{item}}" bindtap="onBuy">购买</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-space"></view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 额度明细视图 -->
|
||||
<scroll-view wx:else scroll-y class="scroll-body">
|
||||
<view class="flow-head" bindtap="onSwitchMall">← 返回商城</view>
|
||||
<view class="flow-list">
|
||||
<view class="flow-row card" wx:for="{{flowList}}" wx:key="quota_flow_uuid">
|
||||
<view class="flow-left">
|
||||
<view class="flow-reason">{{item.reason || item._name}}</view>
|
||||
<view class="flow-time">{{item._time}}</view>
|
||||
<scroll-view wx:else scroll-y class="scroll-body pb-safe" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Quota Ledger</view>
|
||||
<view class="display">额度明细</view>
|
||||
<view class="ml-back" bindtap="onSwitchMall">← 返回商城</view>
|
||||
</view>
|
||||
<view class="flow-right">
|
||||
<view class="flow-num {{item.change_num > 0 ? 'plus' : ''}}">{{item._sign}}</view>
|
||||
<view class="flow-after">剩余 {{item.new_value}}</view>
|
||||
|
||||
<view class="ml-flows">
|
||||
<view class="ml-flow-row" wx:for="{{flowList}}" wx:key="quota_flow_uuid">
|
||||
<view class="ml-flow-l">
|
||||
<view class="ml-flow-reason">{{item.reason || item._name}}</view>
|
||||
<view class="ml-flow-time num">{{item._time}}</view>
|
||||
</view>
|
||||
<view class="ml-flow-r">
|
||||
<view class="ml-flow-n num">{{item._sign}}</view>
|
||||
<view class="ml-flow-after num">剩余 {{item.new_value}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<empty-state wx:if="{{!flowList.length}}" icon="icon-doc" title="暂无额度明细" />
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 订单确认弹窗 -->
|
||||
<view wx:if="{{orderVisible}}" class="mask" bindtap="onCloseOrder">
|
||||
<!-- 订单确认:底部玻璃抽屉 -->
|
||||
<view wx:if="{{orderVisible}}" class="scrim" bindtap="onCloseOrder"></view>
|
||||
<view wx:if="{{orderVisible}}" class="sheet-wrap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheet-grabber"></view>
|
||||
<view class="order-goods">{{orderGoods.goods_name}}</view>
|
||||
<view class="order-desc">{{orderGoods.description}}</view>
|
||||
<view class="order-amount">实付 <text class="order-price num">{{orderGoods._priceText}}</text></view>
|
||||
<view class="btn btn-primary {{submitting ? 'disabled' : ''}}" bindtap="confirmPay">{{submitting ? '下单中…' : '立即支付'}}</view>
|
||||
<view class="sheet-grab"></view>
|
||||
<view class="sheet-hd">
|
||||
<text class="sheet-t">{{orderGoods.goods_name}}</text>
|
||||
<text class="sheet-x" bindtap="onCloseOrder">取消</text>
|
||||
</view>
|
||||
<view class="sheet-body">
|
||||
<view class="ml-order-d">{{orderGoods.description}}</view>
|
||||
<view class="ml-order-a">实付 <text class="num">{{orderGoods._priceText}}</text></view>
|
||||
</view>
|
||||
<view class="sheet-foot">
|
||||
<view
|
||||
class="btn btn--ink btn--block {{submitting ? 'is-off' : ''}}"
|
||||
bindtap="confirmPay"
|
||||
>{{submitting ? '下单中…' : '立即支付'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
+137
-252
@@ -1,7 +1,9 @@
|
||||
.mall-page {
|
||||
/** 额度与会员:唯一深色会员卡 + 四栏额度条 + 商品行 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,322 +11,205 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* 会员卡 */
|
||||
.member-card {
|
||||
margin: 24rpx;
|
||||
background: #E8F2FF;
|
||||
/* 会员卡(全稿唯一深色块) */
|
||||
.ml-member {
|
||||
margin: 40rpx var(--pad) 0;
|
||||
padding: 40rpx 36rpx;
|
||||
border-radius: var(--r-ctrl);
|
||||
background: var(--block);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.member-left {
|
||||
.ml-member-l {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.member-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
color: #007AFF;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.member-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.member-btn {
|
||||
height: 64rpx;
|
||||
padding: 0 32rpx;
|
||||
border: 1rpx solid #007AFF;
|
||||
border-radius: 32rpx;
|
||||
color: #007AFF;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.ml-member-l .icon {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
flex-shrink: 0;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
/* 额度网格 */
|
||||
.quota-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 24rpx;
|
||||
gap: 20rpx;
|
||||
.ml-member-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quota-cell {
|
||||
width: calc(50% - 10rpx);
|
||||
.ml-member-n {
|
||||
font-family: var(--serif);
|
||||
font-size: 36rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.005em;
|
||||
}
|
||||
|
||||
.ml-member-d {
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
line-height: 1.5;
|
||||
color: rgba(255, 255, 255, .66);
|
||||
}
|
||||
|
||||
.ml-member-b {
|
||||
flex-shrink: 0;
|
||||
height: 68rpx;
|
||||
padding: 0 30rpx;
|
||||
border-radius: var(--r-pill);
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.quota-name {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.quota-num {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.quota-total {
|
||||
font-size: 24rpx;
|
||||
color: #C7C7CC;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.quota-bar {
|
||||
height: 10rpx;
|
||||
background: #F2F2F7;
|
||||
border-radius: 5rpx;
|
||||
margin-top: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quota-fill {
|
||||
height: 100%;
|
||||
background: #007AFF;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
|
||||
.quota-fill.warn {
|
||||
background: #FF9500;
|
||||
}
|
||||
|
||||
.flow-entry {
|
||||
margin: 24rpx;
|
||||
color: var(--block);
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
transition: opacity .18s var(--ease);
|
||||
}
|
||||
|
||||
.flow-entry .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
.ml-member-b:active {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
/* 额度 */
|
||||
.ml-grp {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.ml-track {
|
||||
margin: 18rpx 24rpx 0;
|
||||
}
|
||||
|
||||
.ml-flow-entry {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
/* 商品筛选 */
|
||||
.goods-filter {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 20rpx;
|
||||
.ml-filters {
|
||||
margin-top: 64rpx;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
/* 商品行 */
|
||||
.ml-goods {
|
||||
min-height: 152rpx;
|
||||
padding-top: 20rpx;
|
||||
padding-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 28rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.goods-list {
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.goods-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
color: #007AFF;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
.ml-goods-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.ml-goods-n {
|
||||
font-size: 29rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.goods-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 6rpx;
|
||||
.ml-goods-d {
|
||||
margin-top: 8rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.goods-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.ml-goods-r {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-price {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #007AFF;
|
||||
.ml-price {
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.goods-origin {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
.ml-origin {
|
||||
margin-top: 4rpx;
|
||||
font-size: 21rpx;
|
||||
color: var(--ink-4);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.buy-btn {
|
||||
margin-top: 12rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.ml-buy {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.footer-space {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 额度明细视图 */
|
||||
.flow-head {
|
||||
padding: 24rpx;
|
||||
/* 额度明细 */
|
||||
.ml-back {
|
||||
margin-top: 20rpx;
|
||||
font-size: 26rpx;
|
||||
color: #007AFF;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.flow-list {
|
||||
padding: 0 24rpx;
|
||||
.ml-flows {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
.ml-flow-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 24rpx;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx 0;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.flow-reason {
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
.ml-flow-l {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.flow-time {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
.ml-flow-reason {
|
||||
font-size: 29rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.ml-flow-time {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.flow-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.flow-num {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.flow-num.plus {
|
||||
color: #34C759;
|
||||
}
|
||||
|
||||
.flow-after {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
margin-top: 4rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 订单弹窗 */
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 200;
|
||||
.ml-flow-r {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sheet-grabber {
|
||||
width: 72rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 auto 24rpx;
|
||||
}
|
||||
|
||||
.order-goods {
|
||||
.ml-flow-n {
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.order-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
text-align: center;
|
||||
margin-top: 12rpx;
|
||||
.ml-flow-after {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
/* 订单抽屉 */
|
||||
.ml-order-d {
|
||||
padding: 0 var(--pad);
|
||||
font-size: 26rpx;
|
||||
line-height: 1.7;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.ml-order-a {
|
||||
margin: 40rpx var(--pad) 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
margin: 32rpx 0;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.order-price {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #FF9500;
|
||||
.ml-order-a .num {
|
||||
font-family: var(--serif);
|
||||
font-size: 48rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { request } from '../../utils/request'
|
||||
import { NotificationItem } from '../../types/models'
|
||||
import { formatDateTime } from '../../utils/format'
|
||||
import { syncTabBarUnread } from '../../utils/tabbar'
|
||||
import { navPageScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -12,8 +13,13 @@ Page({
|
||||
uuid: '',
|
||||
item: {} as NotificationItem,
|
||||
time: '',
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// 页面级滚动 → 导航收缩为玻璃小标题
|
||||
onPageScroll: navPageScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="通知详情" />
|
||||
<view class="page nd-page">
|
||||
<navigation-bar title="通知详情" scrolled="{{navScrolled}}" />
|
||||
|
||||
<view class="card detail-card">
|
||||
<view class="icon-row">
|
||||
<view class="n-icon {{item.msg_type === 1 ? 'like' : item.msg_type === 2 ? 'order' : 'sys'}}">
|
||||
<view class="head">
|
||||
<view class="eyebrow">{{item.msg_type === 1 ? 'Like' : item.msg_type === 2 ? 'Order' : 'System'}}</view>
|
||||
<view class="t-xl nd-title">{{item.title}}</view>
|
||||
<view class="nd-time num">{{time}}</view>
|
||||
</view>
|
||||
|
||||
<view class="nd-body">
|
||||
<view class="nd-ic">
|
||||
<view class="icon {{item.msg_type === 1 ? 'icon-heart' : item.msg_type === 2 ? 'icon-doc' : 'icon-bell'}}"></view>
|
||||
</view>
|
||||
<view class="t-bd nd-content">{{item.content}}</view>
|
||||
</view>
|
||||
<view class="detail-title">{{item.title}}</view>
|
||||
<view class="detail-time">{{time}}</view>
|
||||
<view class="detail-content">{{item.content}}</view>
|
||||
</view>
|
||||
|
||||
<view class="gap-64"></view>
|
||||
</view>
|
||||
|
||||
@@ -1,65 +1,43 @@
|
||||
.detail-page {
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
/** 通知详情:衬线标题 + 发丝线正文排版 **/
|
||||
.nd-page {
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
margin-top: 24rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
.nd-title {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.icon-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
.nd-time {
|
||||
margin-top: 18rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
|
||||
.n-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
.nd-body {
|
||||
margin: 56rpx var(--pad) 0;
|
||||
padding-top: 56rpx;
|
||||
border-top: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.nd-ic {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--fill);
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.n-icon.like {
|
||||
background: #FFE8EC;
|
||||
color: #FF3B30;
|
||||
.nd-ic .icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.n-icon.order {
|
||||
background: #E8F2FF;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.n-icon.sys {
|
||||
background: #F2F2F7;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.n-icon .icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-time {
|
||||
font-size: 24rpx;
|
||||
color: #C7C7CC;
|
||||
text-align: center;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 32rpx;
|
||||
line-height: 1.7;
|
||||
.nd-content {
|
||||
line-height: 1.85;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { STORAGE_KEYS } from '../../utils/config'
|
||||
import { formatRelativeTime } from '../../utils/format'
|
||||
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { getTabBarInstance } from '../../utils/tabbar'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -27,8 +28,13 @@ Page({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onShow() {
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
if (!auth.isLoggedIn()) {
|
||||
|
||||
@@ -1,53 +1,64 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="通知" />
|
||||
<navigation-bar title="通知" scrolled="{{navScrolled}}" />
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-body"
|
||||
bindscrolltolower="onReachBottom"
|
||||
>
|
||||
<!-- 筛选 -->
|
||||
<scroll-view scroll-x class="filter-scroll" show-scrollbar="{{false}}">
|
||||
<view class="filter-row">
|
||||
<scroll-view scroll-y class="scroll-body" bindscrolltolower="onReachBottom" bindscroll="onNavScroll">
|
||||
<!-- 大标题页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Notifications</view>
|
||||
<view class="display">通知</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线式筛选 -->
|
||||
<view class="segbar">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{filters}}"
|
||||
wx:key="name"
|
||||
class="filter-item {{activeFilter === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeFilter === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onFilterTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading}}" class="nt-skel">
|
||||
<view class="nt-skel-item" wx:for="{{[1, 2, 3]}}" wx:key="*this">
|
||||
<view class="sk sk-ln" style="width: 52%;"></view>
|
||||
<view class="sk sk-ln" style="width: 86%;"></view>
|
||||
<view class="sk sk-ln" style="width: 30%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<empty-state
|
||||
wx:if="{{!loading && !list.length}}"
|
||||
wx:elif="{{!list.length}}"
|
||||
icon="icon-bell"
|
||||
title="还没有消息"
|
||||
desc="穿搭被点赞、订单动态都会出现在这里"
|
||||
/>
|
||||
|
||||
<!-- 列表 -->
|
||||
<view class="list">
|
||||
<!-- 列表:未读左侧黑竖条 + 黑字标题,已读降为 ink-3 -->
|
||||
<view wx:else class="nt-list">
|
||||
<view
|
||||
class="n-card card {{item.is_read === 1 ? 'unread' : ''}}"
|
||||
class="nt-item {{item.is_read === 1 ? 'is-unread' : ''}}"
|
||||
wx:for="{{list}}"
|
||||
wx:key="notification_uuid"
|
||||
data-uuid="{{item.notification_uuid}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<view class="n-icon {{item.msg_type === 1 ? 'like' : item.msg_type === 2 ? 'order' : 'sys'}}">
|
||||
<view class="nt-ic">
|
||||
<view class="icon {{item.msg_type === 1 ? 'icon-heart' : item.msg_type === 2 ? 'icon-doc' : 'icon-bell'}}"></view>
|
||||
</view>
|
||||
<view class="n-body">
|
||||
<view class="n-title-row">
|
||||
<view wx:if="{{item.is_read === 1}}" class="n-dot"></view>
|
||||
<text class="n-title {{item.is_read === 2 ? 'read' : ''}}">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="n-content">{{item.content}}</view>
|
||||
<view class="n-time">{{item._time}}</view>
|
||||
<view class="nt-body">
|
||||
<view class="nt-t">{{item.title}}</view>
|
||||
<view class="nt-c">{{item.content}}</view>
|
||||
<view class="nt-time num">{{item._time}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="foot-note">没有更多了</view>
|
||||
<view class="gap-32"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 通知列表:下划线筛选 + 未读黑竖条 + 发丝线分行 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,117 +11,104 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
margin-top: 36rpx;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
.nt-skel {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 52rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 32rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
.nt-skel-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
.nt-list {
|
||||
padding: 8rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.n-card {
|
||||
.nt-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
gap: 26rpx;
|
||||
padding: 36rpx 0;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.n-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
/* 未读:左侧 4rpx 黑竖条 */
|
||||
.nt-item.is-unread::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -20rpx;
|
||||
top: 44rpx;
|
||||
width: 4rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 2rpx;
|
||||
background: var(--ink);
|
||||
}
|
||||
|
||||
.nt-ic {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--fill);
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.n-icon.like {
|
||||
background: #FFE8EC;
|
||||
color: #FF3B30;
|
||||
.nt-ic .icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.n-icon.order {
|
||||
background: #E8F2FF;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.n-icon.sys {
|
||||
background: #F2F2F7;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.n-icon .icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.n-body {
|
||||
.nt-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.n-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.n-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: #007AFF;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.n-title {
|
||||
.nt-t {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.n-title.read {
|
||||
color: #8E8E93;
|
||||
font-weight: 500;
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.n-content {
|
||||
.nt-item.is-unread .nt-t {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nt-c {
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 8rpx;
|
||||
line-height: 1.6;
|
||||
color: var(--ink-2);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.n-time {
|
||||
.nt-time {
|
||||
margin-top: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
margin-top: 8rpx;
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state"
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { request } from '../../utils/request'
|
||||
import { PagedResult, OrderItem } from '../../types/models'
|
||||
import { fenToYuan, formatDateTime, formatCountdown } from '../../utils/format'
|
||||
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -17,7 +18,14 @@ const STATUS_FILTERS = [
|
||||
]
|
||||
|
||||
const STATUS_TEXT: Record<number, string> = { 1: '待支付', 2: '已支付', 3: '已取消', 4: '已过期', 5: '已退款' }
|
||||
const STATUS_TYPE: Record<number, string> = { 1: 'orange', 2: 'green', 3: 'gray', 4: 'gray', 5: 'blue' }
|
||||
// 零彩色状态语义:待支付 = 实心黑;已支付/已退款 = 描边;已取消/已过期 = 降透明
|
||||
const STATUS_CLS: Record<number, string> = {
|
||||
1: 'pill--ink',
|
||||
2: 'pill--line',
|
||||
3: 'pill--dim',
|
||||
4: 'pill--dim',
|
||||
5: 'pill--line pill--dim',
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -29,8 +37,28 @@ Page({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 取消订单确认
|
||||
cancelVisible: false,
|
||||
cancelOrderNo: '',
|
||||
cancelButtons: [
|
||||
{ text: '再想想', value: '' },
|
||||
{ text: '确认取消', value: 'ok', type: 'main' },
|
||||
],
|
||||
// 退款申请(抽屉 + 原因输入)
|
||||
refundVisible: false,
|
||||
refundOrderNo: '',
|
||||
refundReason: '',
|
||||
refundSubmitting: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
// 抽屉内容点击不穿透到遮罩
|
||||
noop() {},
|
||||
|
||||
onShow() {
|
||||
this.refresh()
|
||||
this.startCountdown()
|
||||
@@ -103,7 +131,7 @@ Page({
|
||||
const list = raw.map((it) => ({
|
||||
...it,
|
||||
_statusText: STATUS_TEXT[it.order_status] || '',
|
||||
_statusType: STATUS_TYPE[it.order_status] || 'gray',
|
||||
_statusCls: STATUS_CLS[it.order_status] || 'pill--line',
|
||||
_amount: fenToYuan(it.pay_amount),
|
||||
_time: formatDateTime(it.created_at),
|
||||
_orderTail: `…${it.order_no.slice(-4)}`,
|
||||
@@ -137,21 +165,24 @@ Page({
|
||||
|
||||
onCancel(e: WechatMiniprogram.TouchEvent) {
|
||||
const orderNo = e.currentTarget.dataset.no as string
|
||||
wx.showModal({
|
||||
title: '取消订单',
|
||||
content: '确认取消该订单?',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({ cancelVisible: true, cancelOrderNo: orderNo })
|
||||
},
|
||||
|
||||
onCancelCancel() {
|
||||
this.setData({ cancelVisible: false })
|
||||
},
|
||||
|
||||
onCancelConfirm(ev: WechatMiniprogram.CustomEvent) {
|
||||
const orderNo = this.data.cancelOrderNo
|
||||
this.setData({ cancelVisible: false })
|
||||
if (ev.detail.value !== 'ok' || !orderNo) return
|
||||
request({ url: `/order/${orderNo}/cancel`, method: 'POST' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '取消订单成功, orderNo=', orderNo)
|
||||
wx.showToast({ title: '已取消', icon: 'success' })
|
||||
this.refresh()
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '取消订单失败, orderNo=', orderNo, 'code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '取消订单弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((err: any) => logger.warn(LOG, '取消订单失败, orderNo=', orderNo, 'code=', err && err.code))
|
||||
},
|
||||
|
||||
onPay() {
|
||||
@@ -160,26 +191,36 @@ Page({
|
||||
|
||||
onRefund(e: WechatMiniprogram.TouchEvent) {
|
||||
const orderNo = e.currentTarget.dataset.no as string
|
||||
wx.showModal({
|
||||
title: '申请退款',
|
||||
editable: true,
|
||||
placeholderText: '请填写退款原因',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
const reason = (res.content || '').trim()
|
||||
this.setData({ refundVisible: true, refundOrderNo: orderNo, refundReason: '' })
|
||||
},
|
||||
|
||||
onRefundClose() {
|
||||
this.setData({ refundVisible: false })
|
||||
},
|
||||
|
||||
onRefundInput(e: WechatMiniprogram.Input) {
|
||||
this.setData({ refundReason: e.detail.value })
|
||||
},
|
||||
|
||||
onRefundSubmit() {
|
||||
if (this.data.refundSubmitting) return
|
||||
const orderNo = this.data.refundOrderNo
|
||||
const reason = (this.data.refundReason || '').trim()
|
||||
if (!reason) {
|
||||
wx.showToast({ title: '请填写退款原因', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ refundSubmitting: true })
|
||||
request({ url: '/order/refund', method: 'POST', data: { order_no: orderNo, reason } })
|
||||
.then(() => {
|
||||
logger.info(LOG, '退款申请提交成功, orderNo=', orderNo)
|
||||
wx.showToast({ title: '退款申请已提交,等待审核', icon: 'none' })
|
||||
this.setData({ refundVisible: false, refundSubmitting: false })
|
||||
this.refresh()
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '退款申请失败, orderNo=', orderNo, 'code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '退款弹窗失败, err=', e && e.errMsg),
|
||||
.catch((err: any) => {
|
||||
logger.warn(LOG, '退款申请失败, orderNo=', orderNo, 'code=', err && err.code)
|
||||
this.setData({ refundSubmitting: false })
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="我的订单" />
|
||||
<navigation-bar title="我的订单" scrolled="{{navScrolled}}" />
|
||||
|
||||
<scroll-view scroll-y class="scroll-body" bindscrolltolower="onReachBottom">
|
||||
<!-- 筛选 -->
|
||||
<scroll-view scroll-x class="filter-scroll" show-scrollbar="{{false}}">
|
||||
<view class="filter-row">
|
||||
<scroll-view scroll-y class="scroll-body pb-safe" bindscrolltolower="onReachBottom" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Orders</view>
|
||||
<view class="display">我的订单</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线式筛选 -->
|
||||
<view class="segbar">
|
||||
<scroll-view scroll-x class="seg-scroll" show-scrollbar="{{false}}">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{filters}}"
|
||||
wx:key="name"
|
||||
class="filter-item {{activeFilter === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeFilter === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onFilterTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<empty-state
|
||||
wx:if="{{!loading && !list.length}}"
|
||||
@@ -24,32 +31,79 @@
|
||||
bind:action="onGoMall"
|
||||
/>
|
||||
|
||||
<!-- 订单列表 -->
|
||||
<view class="list">
|
||||
<view class="order-card card" wx:for="{{list}}" wx:key="order_no">
|
||||
<view class="order-top">
|
||||
<text class="order-no">订单 {{item._orderTail}}</text>
|
||||
<text wx:if="{{item._payChannelText}}" class="tag tag-gray">{{item._payChannelText}}</text>
|
||||
<text class="tag tag-{{item._statusType}}">{{item._statusText}}</text>
|
||||
<!-- 订单列表:状态用描边胶囊表达,不依赖颜色 -->
|
||||
<view class="od-list">
|
||||
<view class="od-card" wx:for="{{list}}" wx:key="order_no">
|
||||
<view class="od-top">
|
||||
<text class="od-no num">订单 {{item._orderTail}}</text>
|
||||
<text wx:if="{{item._payChannelText}}" class="pill">{{item._payChannelText}}</text>
|
||||
<text class="pill {{item._statusCls}}">{{item._statusText}}</text>
|
||||
</view>
|
||||
<view class="order-body">
|
||||
<view class="order-goods">{{item.goods_name}}</view>
|
||||
<view class="order-benefit" wx:if="{{item._benefit}}">{{item._benefit}}</view>
|
||||
<view class="order-bottom">
|
||||
<text class="order-amount">实付 <text class="num">{{item._amount}}</text></text>
|
||||
<view class="order-actions">
|
||||
|
||||
<view class="od-goods">{{item.goods_name}}</view>
|
||||
<view class="od-benefit" wx:if="{{item._benefit}}">{{item._benefit}}</view>
|
||||
|
||||
<view class="od-bottom">
|
||||
<view class="od-amount">实付 <text class="num">{{item._amount}}</text></view>
|
||||
<view class="od-actions">
|
||||
<block wx:if="{{item.order_status === 1}}">
|
||||
<text wx:if="{{item._countdown}}" class="countdown">剩余 {{item._countdown}}</text>
|
||||
<view wx:if="{{item._canCancel}}" class="action-btn outline" data-no="{{item.order_no}}" bindtap="onCancel">取消订单</view>
|
||||
<view class="action-btn primary" data-no="{{item.order_no}}" bindtap="onPay">去支付</view>
|
||||
<text wx:if="{{item._countdown}}" class="od-countdown num">剩余 {{item._countdown}}</text>
|
||||
<view
|
||||
wx:if="{{item._canCancel}}"
|
||||
class="btn btn--ghost btn--sm"
|
||||
data-no="{{item.order_no}}"
|
||||
bindtap="onCancel"
|
||||
>取消订单</view>
|
||||
<view class="btn btn--ink btn--sm" data-no="{{item.order_no}}" bindtap="onPay">去支付</view>
|
||||
</block>
|
||||
<block wx:elif="{{item.order_status === 2}}">
|
||||
<view class="action-btn outline" data-no="{{item.order_no}}" bindtap="onRefund">申请退款</view>
|
||||
<view class="btn btn--ghost btn--sm" data-no="{{item.order_no}}" bindtap="onRefund">申请退款</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="foot-note">没有更多了</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 取消订单确认 -->
|
||||
<v2-overlay
|
||||
visible="{{cancelVisible}}"
|
||||
type="modal"
|
||||
title="取消订单"
|
||||
content="确认取消该订单?"
|
||||
buttons="{{cancelButtons}}"
|
||||
bind:confirm="onCancelConfirm"
|
||||
bind:cancel="onCancelCancel"
|
||||
/>
|
||||
|
||||
<!-- 退款申请:底部玻璃抽屉 -->
|
||||
<view wx:if="{{refundVisible}}" class="scrim" bindtap="onRefundClose"></view>
|
||||
<view wx:if="{{refundVisible}}" class="sheet-wrap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheet-grab"></view>
|
||||
<view class="sheet-hd">
|
||||
<text class="sheet-t">申请退款</text>
|
||||
<text class="sheet-x" bindtap="onRefundClose">取消</text>
|
||||
</view>
|
||||
<view class="sheet-body">
|
||||
<textarea
|
||||
class="textarea od-refund-input"
|
||||
value="{{refundReason}}"
|
||||
bindinput="onRefundInput"
|
||||
placeholder="请填写退款原因"
|
||||
placeholder-class="input-ph"
|
||||
maxlength="200"
|
||||
/>
|
||||
</view>
|
||||
<view class="sheet-foot">
|
||||
<view
|
||||
class="btn btn--ink btn--block {{refundSubmitting ? 'is-off' : ''}}"
|
||||
bindtap="onRefundSubmit"
|
||||
>{{refundSubmitting ? '提交中…' : '提交申请'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 订单:下划线筛选 + 描边状态胶囊 + tabular 倒计时 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,113 +11,87 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.filter-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
margin-top: 36rpx;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
.od-list {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 28rpx;
|
||||
height: 56rpx;
|
||||
line-height: 56rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
.od-card {
|
||||
padding-bottom: 36rpx;
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx;
|
||||
}
|
||||
|
||||
.order-top {
|
||||
.od-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.order-no {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
.od-no {
|
||||
flex: 1;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.order-body {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.order-goods {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.order-benefit {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.order-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.od-goods {
|
||||
margin-top: 24rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
.od-benefit {
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.order-amount .num {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.od-bottom {
|
||||
margin-top: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
.od-amount {
|
||||
flex: 1;
|
||||
font-size: 24rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.od-amount .num {
|
||||
font-family: var(--serif);
|
||||
font-size: 36rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.od-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
.od-countdown {
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
font-family: var(--serif);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
height: 56rpx;
|
||||
padding: 0 28rpx;
|
||||
border-radius: 28rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-btn.outline {
|
||||
border: 1rpx solid #C7C7CC;
|
||||
color: #3C3C43;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
.od-refund-input {
|
||||
margin: 0 var(--pad);
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,187 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
|
||||
import { OutfitItem, ClothesItem } from '../../types/models'
|
||||
import { IsPublic } from '../../utils/config'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'outfit-detail'
|
||||
|
||||
// 接收者打开「分享者额度不足」链接后的自动返回倒计时(秒)
|
||||
const SHARE_FAIL_BACK_SECONDS = 10
|
||||
|
||||
Page({
|
||||
data: {
|
||||
uuid: '',
|
||||
isOwner: true,
|
||||
// 仅显式 owner=1 视为属主:无参数链接(如微信默认分享路径)安全降级为非属主视角
|
||||
isOwner: false,
|
||||
outfit: {} as OutfitItem,
|
||||
clothes: [] as ClothesItem[],
|
||||
isLiked: false,
|
||||
likeCount: 0,
|
||||
isCollected: false,
|
||||
loading: true,
|
||||
// 页面状态:normal 正常;quota 接收者命中「分享者额度不足」;invalid 加载失败
|
||||
pageState: 'normal' as 'normal' | 'quota' | 'invalid',
|
||||
invalidTitle: '',
|
||||
invalidDesc: '',
|
||||
needLogin: false,
|
||||
countdown: SHARE_FAIL_BACK_SECONDS,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// v2 玻璃弹层
|
||||
tipVisible: false,
|
||||
tipTitle: '',
|
||||
tipText: '',
|
||||
tipButtons: [{ text: '我知道了', value: 'ok', type: 'main' }],
|
||||
pubVisible: false,
|
||||
pubButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认公开', value: 'ok', type: 'main' },
|
||||
],
|
||||
delVisible: false,
|
||||
delButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认删除', value: 'ok', type: 'main' },
|
||||
],
|
||||
moreVisible: false,
|
||||
moreItems: [
|
||||
{ text: '编辑穿搭', value: 'edit' },
|
||||
{ text: '删除穿搭', value: 'delete' },
|
||||
],
|
||||
// 额度不足引导:v2 玻璃弹窗
|
||||
quotaVisible: false,
|
||||
quotaTitle: '',
|
||||
quotaText: '',
|
||||
quotaButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '去购买', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
const isOwner = query.owner !== '0'
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid, 'isOwner=', isOwner)
|
||||
const isOwner = query.owner === '1'
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid, 'isOwner=', isOwner, 'shareFail=', query.share_fail)
|
||||
this.setData({ uuid, isOwner })
|
||||
if (uuid) this.fetchDetail()
|
||||
else logger.warn(LOG, 'onLoad 缺少 uuid 参数')
|
||||
|
||||
// 分享者额度不足时分享出去的链接:不请求后端(也不依赖登录),直接进入提示态
|
||||
if (query.share_fail === 'quota') {
|
||||
this.showShareFailQuota()
|
||||
return
|
||||
}
|
||||
|
||||
if (!uuid) {
|
||||
logger.warn(LOG, 'onLoad 缺少 uuid 参数')
|
||||
this.setInvalid('链接不完整', '缺少穿搭标识,无法打开')
|
||||
return
|
||||
}
|
||||
this.fetchDetail()
|
||||
},
|
||||
|
||||
// 接收者命中「分享者额度不足」:弹提醒 + 倒计时后返回「我的」页
|
||||
showShareFailQuota() {
|
||||
this.setData({
|
||||
pageState: 'quota',
|
||||
loading: false,
|
||||
countdown: SHARE_FAIL_BACK_SECONDS,
|
||||
tipVisible: true,
|
||||
tipTitle: '提示',
|
||||
tipText: '给您分享的好友额度不足哦',
|
||||
})
|
||||
this.startBackCountdown()
|
||||
},
|
||||
|
||||
onTipConfirm() {
|
||||
this.setData({ tipVisible: false })
|
||||
},
|
||||
|
||||
startBackCountdown() {
|
||||
this.clearBackTimer()
|
||||
;(this as any).backTimer = setInterval(() => {
|
||||
const next = this.data.countdown - 1
|
||||
if (next > 0) {
|
||||
this.setData({ countdown: next })
|
||||
return
|
||||
}
|
||||
this.clearBackTimer()
|
||||
this.onBackProfile()
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
clearBackTimer() {
|
||||
const timer = (this as any).backTimer
|
||||
if (timer) {
|
||||
clearInterval(timer)
|
||||
;(this as any).backTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
onBackProfile() {
|
||||
logger.info(LOG, '额度不足提示结束,返回「我的」页')
|
||||
wx.switchTab({
|
||||
url: '/pages/profile/profile',
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 我的失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onBackHome() {
|
||||
wx.switchTab({
|
||||
url: '/pages/wardrobe/wardrobe',
|
||||
fail: (e) => logger.warn(LOG, 'switchTab 衣橱失败, err=', e && e.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onGoLogin() {
|
||||
// 透传视角参数:属主登录失效后重登,仍回到属主视角
|
||||
const owner = this.data.isOwner ? '1' : '0'
|
||||
const redirect = `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=${owner}`
|
||||
const loginUrl = `/pages/login/login?redirect=${encodeURIComponent(redirect)}`
|
||||
wx.navigateTo({
|
||||
url: loginUrl,
|
||||
fail: () => {
|
||||
wx.redirectTo({
|
||||
url: loginUrl,
|
||||
fail: () => wx.switchTab({ url: '/pages/profile/profile' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
setInvalid(title: string, desc: string, needLogin = false) {
|
||||
this.setData({
|
||||
loading: false,
|
||||
pageState: 'invalid',
|
||||
invalidTitle: title,
|
||||
invalidDesc: desc,
|
||||
needLogin,
|
||||
})
|
||||
},
|
||||
|
||||
async fetchDetail() {
|
||||
let outfit: OutfitItem
|
||||
try {
|
||||
const outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}` })
|
||||
outfit = await request<OutfitItem>({ url: `/outfit/${this.data.uuid}`, showError: false })
|
||||
} catch (e: any) {
|
||||
const code = e && e.code
|
||||
logger.warn(LOG, '穿搭详情加载失败, code=', code)
|
||||
if (AUTH_EXPIRED_CODES.indexOf(code) >= 0) {
|
||||
// 未登录:给「去登录」出口,登录成功后回到本页
|
||||
this.setInvalid('请先登录', '登录后即可查看该穿搭', true)
|
||||
} else if (code === 40004 || code === 40000) {
|
||||
// 他人未公开 / 已删除:不暴露具体原因,统一提示内容不可见
|
||||
this.setInvalid('内容不可见', '该穿搭可能已被删除或未公开')
|
||||
} else {
|
||||
this.setInvalid('加载失败', '请稍后重试')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug(LOG, '穿搭详情加载成功, 单品数=', outfit.items && outfit.items.length)
|
||||
this.setData({ outfit })
|
||||
this.setData({ outfit, loading: false })
|
||||
|
||||
// 封面与衣物缩略图直接使用穿搭明细里的抠图(processed_image),
|
||||
// 避免非属主逐件请求 /clothes/:uuid 被拒(50005)导致图片与名称全部丢失。
|
||||
@@ -55,11 +205,13 @@ Page({
|
||||
this.setData({ clothes })
|
||||
}
|
||||
|
||||
const like = await request<{ is_liked: boolean; like_count: number }>({
|
||||
// 点赞状态:失败静默保持默认,不影响详情展示
|
||||
request<{ is_liked: boolean; like_count: number }>({
|
||||
url: `/like/${this.data.uuid}`,
|
||||
showError: false,
|
||||
})
|
||||
this.setData({ isLiked: like.is_liked, likeCount: like.like_count, loading: false })
|
||||
.then((like) => this.setData({ isLiked: like.is_liked, likeCount: like.like_count }))
|
||||
.catch((e: any) => logger.warn(LOG, '点赞状态拉取失败(保持默认), code=', e && e.code))
|
||||
|
||||
// 收藏状态:调用后端 /collect/:targetUUID 初始化,失败静默保持未收藏
|
||||
request<{ is_collected: boolean; collect_count: number }>({
|
||||
@@ -68,10 +220,6 @@ Page({
|
||||
})
|
||||
.then((collect) => this.setData({ isCollected: collect.is_collected }))
|
||||
.catch((e: any) => logger.warn(LOG, '收藏状态拉取失败(保持未收藏), code=', e && e.code))
|
||||
} catch (e: any) {
|
||||
logger.warn(LOG, '穿搭详情加载失败, code=', e && e.code)
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
onLike() {
|
||||
@@ -99,18 +247,20 @@ Page({
|
||||
if (isPublic) {
|
||||
this.doPublic(IsPublic.PRIVATE)
|
||||
} else {
|
||||
wx.showModal({
|
||||
title: '公开到社区',
|
||||
content: '公开后将展示在社区供他人点赞收藏,并消耗 1 次社区分享额度',
|
||||
confirmText: '确认公开',
|
||||
success: (res) => {
|
||||
if (res.confirm) this.doPublic(IsPublic.PUBLIC)
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '公开弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
this.setData({ pubVisible: true })
|
||||
}
|
||||
},
|
||||
|
||||
onPubCancel() {
|
||||
this.setData({ pubVisible: false })
|
||||
},
|
||||
|
||||
onPubConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ pubVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
this.doPublic(IsPublic.PUBLIC)
|
||||
},
|
||||
|
||||
doPublic(isPublic: number) {
|
||||
// 后端 OutfitPublicRequest.OutfitUUID 为必填(binding required),请求体需携带
|
||||
request({
|
||||
@@ -123,18 +273,32 @@ Page({
|
||||
this.setData({ 'outfit.is_public': isPublic })
|
||||
wx.showToast({ title: isPublic === IsPublic.PUBLIC ? '已公开' : '已取消公开', icon: 'none' })
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '公开状态变更失败, code=', e && e.code))
|
||||
.catch((e: any) => {
|
||||
logger.warn(LOG, '公开状态变更失败, code=', e && e.code)
|
||||
// 社区分享额度不足(后端已回滚为私有):弹窗引导购买
|
||||
if (e && e.code === 80000) {
|
||||
this.setData({
|
||||
quotaVisible: true,
|
||||
quotaTitle: '社区分享额度不足',
|
||||
quotaText: '购买额度包后可公开到社区',
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onMore() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['编辑穿搭', '删除穿搭'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) this.onEdit()
|
||||
else this.onDelete()
|
||||
this.setData({ moreVisible: true })
|
||||
},
|
||||
fail: (e) => logger.debug(LOG, '取消更多操作, err=', e && e.errMsg),
|
||||
})
|
||||
|
||||
onMoreCancel() {
|
||||
this.setData({ moreVisible: false })
|
||||
},
|
||||
|
||||
onMoreSelect(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ moreVisible: false })
|
||||
const value = e.detail.value
|
||||
if (value === 'edit') this.onEdit()
|
||||
else if (value === 'delete') this.onDelete()
|
||||
},
|
||||
|
||||
onEdit() {
|
||||
@@ -145,13 +309,16 @@ Page({
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
wx.showModal({
|
||||
title: '确认删除这套穿搭?',
|
||||
content: '删除后不可恢复,但不会影响衣橱内的单品。',
|
||||
confirmText: '确认删除',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({ delVisible: true })
|
||||
},
|
||||
|
||||
onDelCancel() {
|
||||
this.setData({ delVisible: false })
|
||||
},
|
||||
|
||||
onDelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ delVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '穿搭删除成功')
|
||||
@@ -159,15 +326,12 @@ Page({
|
||||
setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
fail: (e2) => logger.warn(LOG, 'navigateBack 失败, err=', e2 && e2.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '穿搭删除失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((e2: any) => logger.warn(LOG, '穿搭删除失败, code=', e2 && e2.code))
|
||||
},
|
||||
|
||||
onItemTap(e: WechatMiniprogram.TouchEvent) {
|
||||
@@ -184,14 +348,21 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 分享前先创建穿搭分享记录拿到 share_uuid,链接指向分享详情页;
|
||||
// 失败(如穿搭未公开)则回退到普通穿搭详情链接,保证分享动作仍可用。
|
||||
// 分享采用微信官方「同步返回 + promise 字段」异步分享模式(基础库 2.12.0+):
|
||||
// 同步返回兜底链接(3 秒内未 resolve 时生效),promise resolve 后以结果为准。
|
||||
// 非属主不创建分享记录(对他人穿搭创建会被拒),直接转发普通详情链接。
|
||||
onShareAppMessage(): any {
|
||||
const name = this.data.outfit.outfit_name || '我的穿搭'
|
||||
return request<{ share_uuid: string }>({
|
||||
const fallback = `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`
|
||||
if (!this.data.isOwner) {
|
||||
return { title: name, path: fallback }
|
||||
}
|
||||
|
||||
const promise = request<{ share_uuid: string }>({
|
||||
url: '/outfit-share',
|
||||
method: 'POST',
|
||||
data: { outfit_uuid: this.data.uuid },
|
||||
showError: false,
|
||||
})
|
||||
.then((res) => {
|
||||
logger.info(LOG, '穿搭分享创建成功, share_uuid=', res.share_uuid)
|
||||
@@ -201,11 +372,37 @@ Page({
|
||||
}
|
||||
})
|
||||
.catch((e: any) => {
|
||||
logger.warn(LOG, '穿搭分享创建失败,回退普通分享链接, code=', e && e.code)
|
||||
return {
|
||||
title: name,
|
||||
path: `/pages/outfit-detail/outfit-detail?uuid=${this.data.uuid}&owner=0`,
|
||||
logger.warn(LOG, '穿搭分享创建失败, code=', e && e.code)
|
||||
if (e && e.code === 80000) {
|
||||
// 额度不足:提示去购买,链接指向「额度不足」提示页(接收者侧提示后自动返回)
|
||||
this.setData({
|
||||
quotaVisible: true,
|
||||
quotaTitle: '分享额度不足',
|
||||
quotaText: '购买额度包后可继续分享',
|
||||
})
|
||||
return { title: name, path: `${fallback}&share_fail=quota` }
|
||||
}
|
||||
// 其他失败:回退普通详情链接(接收者侧展示通用「内容不可见」空态)
|
||||
return { title: name, path: fallback }
|
||||
})
|
||||
|
||||
return { title: name, path: fallback, promise }
|
||||
},
|
||||
|
||||
onQuotaCancel() {
|
||||
this.setData({ quotaVisible: false })
|
||||
},
|
||||
|
||||
onQuotaConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ quotaVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
wx.navigateTo({
|
||||
url: '/pages/mall/mall',
|
||||
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.clearBackTimer()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,79 +1,179 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}">
|
||||
<view wx:if="{{isOwner}}" slot="right" class="nav-more" bindtap="onMore">
|
||||
<wxs module="cl">
|
||||
function cls(n) {
|
||||
if (n >= 4) return ''
|
||||
if (n === 3) return 'collage--3'
|
||||
if (n === 2) return 'collage--2'
|
||||
return 'collage--1'
|
||||
}
|
||||
module.exports = { cls: cls }
|
||||
</wxs>
|
||||
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="{{outfit.outfit_name || '穿搭详情'}}" scrolled="{{navScrolled}}">
|
||||
<view
|
||||
wx:if="{{isOwner && pageState === 'normal' && !loading}}"
|
||||
slot="right"
|
||||
class="nav-more"
|
||||
bindtap="onMore"
|
||||
>
|
||||
<view class="icon icon-ellipsis"></view>
|
||||
</view>
|
||||
</navigation-bar>
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<!-- 拼图封面 -->
|
||||
<view class="cover">
|
||||
<view
|
||||
class="cover-item cover-{{index}}"
|
||||
wx:for="{{clothes}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="cover-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger cover-placeholder"></view>
|
||||
<!-- 接收者:分享者额度不足(不请求后端,倒计时后返回「我的」) -->
|
||||
<empty-state
|
||||
wx:if="{{pageState === 'quota'}}"
|
||||
icon="icon-user"
|
||||
title="好友分享额度不足"
|
||||
desc="给您分享的好友额度不足哦,{{countdown}} 秒后自动返回"
|
||||
buttonText="立即返回"
|
||||
bind:action="onBackProfile"
|
||||
/>
|
||||
|
||||
<!-- 未登录:去登录后回本页 -->
|
||||
<empty-state
|
||||
wx:elif="{{pageState === 'invalid' && needLogin}}"
|
||||
icon="icon-user"
|
||||
title="{{invalidTitle}}"
|
||||
desc="{{invalidDesc}}"
|
||||
buttonText="去登录"
|
||||
bind:action="onGoLogin"
|
||||
/>
|
||||
|
||||
<!-- 其他加载失败:内容不可见 / 加载失败 -->
|
||||
<empty-state
|
||||
wx:elif="{{pageState === 'invalid'}}"
|
||||
icon="icon-shirt"
|
||||
title="{{invalidTitle}}"
|
||||
desc="{{invalidDesc}}"
|
||||
buttonText="返回首页"
|
||||
bind:action="onBackHome"
|
||||
/>
|
||||
|
||||
<block wx:else>
|
||||
<scroll-view scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<!-- 无缝拼图封面 -->
|
||||
<view class="od-cover">
|
||||
<view class="collage {{cl.cls(clothes.length)}}">
|
||||
<view class="cg" wx:for="{{clothes}}" wx:key="clothes_uuid" wx:if="{{index < 4}}">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view wx:if="{{!clothes.length}}" class="cover-empty">
|
||||
<view class="icon icon-hanger"></view>
|
||||
<view wx:if="{{clothes.length > 4}}" class="cg-more">+{{clothes.length - 4}}</view>
|
||||
<text class="cg-badge num">{{clothes.length}} 件</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息卡片 -->
|
||||
<view class="card info-card">
|
||||
<view class="title-row">
|
||||
<text class="title">{{outfit.outfit_name}}</text>
|
||||
<!-- 信息区 -->
|
||||
<view class="od-info">
|
||||
<view class="eyebrow">{{outfit.source === 2 ? 'AI Stylist' : 'My Look'}}</view>
|
||||
<view class="t-xl od-title">{{outfit.outfit_name}}</view>
|
||||
<view class="od-tags">
|
||||
<text wx:if="{{outfit.scene}}" class="pill pill--line">{{outfit.scene}}</text>
|
||||
<text class="pill {{outfit.source === 2 ? 'pill--ink' : ''}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
|
||||
<text class="pill {{outfit.is_public === 2 ? 'pill--line' : ''}}">{{outfit.is_public === 2 ? '已公开' : '仅自己可见'}}</text>
|
||||
</view>
|
||||
<view class="tag-row">
|
||||
<text wx:if="{{outfit.scene}}" class="tag tag-gray">{{outfit.scene}}</text>
|
||||
<text class="tag {{outfit.source === 2 ? 'tag-indigo' : 'tag-gray'}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
|
||||
<text class="tag {{outfit.is_public === 2 ? 'tag-blue' : 'tag-gray'}}">{{outfit.is_public === 2 ? '已公开' : '仅自己可见'}}</text>
|
||||
<view class="t-bd od-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
</view>
|
||||
<view class="desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
|
||||
<!-- 包含衣物 -->
|
||||
<view class="items-title">包含衣物</view>
|
||||
<scroll-view scroll-x class="items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="items-row">
|
||||
<!-- 包含衣物:横滑单品 -->
|
||||
<view class="od-items">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">包含衣物</text>
|
||||
<text class="gh-en">Items</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="od-items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="od-items-row">
|
||||
<view
|
||||
class="item-chip"
|
||||
class="od-chip"
|
||||
wx:for="{{clothes}}"
|
||||
wx:key="clothes_uuid"
|
||||
data-uuid="{{item.clothes_uuid}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="item-img" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="item-img item-img-empty"><view class="icon icon-hanger"></view></view>
|
||||
<text class="item-name">{{item.name || '未命名'}}</text>
|
||||
<view class="od-thumb">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<text class="od-chip-n">{{item.name || '未命名'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="footer-placeholder"></view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="footer">
|
||||
<view class="footer-left">
|
||||
<view class="fb-btn" bindtap="onLike">
|
||||
<view class="icon icon-heart {{isLiked ? 'liked' : ''}}"></view>
|
||||
<text class="{{isLiked ? 'liked' : ''}}">{{likeCount}}</text>
|
||||
<!-- 悬浮玻璃操作条 -->
|
||||
<view class="dock">
|
||||
<view class="dock-row">
|
||||
<view class="dock-ic {{isLiked ? 'is-on' : ''}}" bindtap="onLike">
|
||||
<view class="icon icon-heart"></view>
|
||||
<text class="num">{{likeCount}}</text>
|
||||
</view>
|
||||
<view class="fb-btn" wx:if="{{!isOwner}}" bindtap="onCollect">
|
||||
<view class="icon {{isCollected ? 'icon-star-fill collected' : 'icon-star'}}"></view>
|
||||
<view class="dock-ic" wx:if="{{!isOwner}}" bindtap="onCollect">
|
||||
<view class="icon {{isCollected ? 'icon-star-fill' : 'icon-star'}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="footer-right" wx:if="{{isOwner}}">
|
||||
<view class="fb-public {{outfit.is_public === 2 ? 'public' : ''}}" bindtap="onTogglePublic">
|
||||
{{outfit.is_public === 2 ? '取消公开' : '公开到社区'}}
|
||||
</view>
|
||||
<button class="fb-share" open-type="share">
|
||||
<view class="spacer"></view>
|
||||
<block wx:if="{{isOwner}}">
|
||||
<view class="btn btn--ghost btn--sm" bindtap="onTogglePublic">{{outfit.is_public === 2 ? '取消公开' : '公开到社区'}}</view>
|
||||
<button class="od-share" open-type="share">
|
||||
<view class="icon icon-share"></view>
|
||||
</button>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 提示(分享者额度不足) -->
|
||||
<v2-overlay
|
||||
visible="{{tipVisible}}"
|
||||
type="modal"
|
||||
title="{{tipTitle}}"
|
||||
content="{{tipText}}"
|
||||
buttons="{{tipButtons}}"
|
||||
bind:confirm="onTipConfirm"
|
||||
/>
|
||||
|
||||
<!-- 公开到社区确认 -->
|
||||
<v2-overlay
|
||||
visible="{{pubVisible}}"
|
||||
type="modal"
|
||||
title="公开到社区"
|
||||
content="公开后将展示在社区供他人点赞收藏,并消耗 1 次社区分享额度"
|
||||
buttons="{{pubButtons}}"
|
||||
bind:confirm="onPubConfirm"
|
||||
bind:cancel="onPubCancel"
|
||||
/>
|
||||
|
||||
<!-- 删除确认 -->
|
||||
<v2-overlay
|
||||
visible="{{delVisible}}"
|
||||
type="modal"
|
||||
title="确认删除这套穿搭?"
|
||||
content="删除后不可恢复,但不会影响衣橱内的单品。"
|
||||
buttons="{{delButtons}}"
|
||||
bind:confirm="onDelConfirm"
|
||||
bind:cancel="onDelCancel"
|
||||
/>
|
||||
|
||||
<!-- 更多操作 -->
|
||||
<v2-overlay
|
||||
visible="{{moreVisible}}"
|
||||
type="actionsheet"
|
||||
items="{{moreItems}}"
|
||||
bind:select="onMoreSelect"
|
||||
bind:cancel="onMoreCancel"
|
||||
/>
|
||||
|
||||
<!-- 额度不足 -->
|
||||
<v2-overlay
|
||||
visible="{{quotaVisible}}"
|
||||
type="modal"
|
||||
title="{{quotaTitle}}"
|
||||
content="{{quotaText}}"
|
||||
buttons="{{quotaButtons}}"
|
||||
bind:confirm="onQuotaConfirm"
|
||||
bind:cancel="onQuotaCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.detail-page {
|
||||
/** 穿搭详情:无缝拼图封面 + 衬线大标题 + 横滑单品 + 悬浮玻璃操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,233 +11,101 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-more {
|
||||
padding: 0 8rpx;
|
||||
color: #000;
|
||||
.od-cover {
|
||||
padding: 32rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
/* 封面 */
|
||||
.cover {
|
||||
margin: 24rpx;
|
||||
height: 480rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
.od-info {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.od-title {
|
||||
margin-top: 22rpx;
|
||||
}
|
||||
|
||||
.od-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.od-desc {
|
||||
margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.od-items {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.od-items-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.od-items-row {
|
||||
display: inline-flex;
|
||||
gap: 28rpx;
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
|
||||
.od-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.od-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-item {
|
||||
position: absolute;
|
||||
width: 200rpx;
|
||||
height: 260rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
.od-thumb .ph {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
.cover-0 { left: 30rpx; top: 80rpx; transform: rotate(-6deg); }
|
||||
.cover-1 { left: 120rpx; top: 140rpx; transform: rotate(2deg); }
|
||||
.cover-2 { left: 220rpx; top: 70rpx; transform: rotate(5deg); }
|
||||
.cover-3 { left: 320rpx; top: 150rpx; transform: rotate(-2deg); }
|
||||
.cover-4 { left: 420rpx; top: 90rpx; transform: rotate(7deg); }
|
||||
|
||||
.cover-img {
|
||||
.od-thumb .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.cover-empty {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.cover-empty .icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
/* 信息卡 */
|
||||
.info-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 20rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.items-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.items-scroll {
|
||||
margin-top: 20rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-row {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.item-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.item-img-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.item-img-empty .icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 20rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
max-width: 140rpx;
|
||||
.od-chip-n {
|
||||
margin-top: 14rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.footer-placeholder {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* 底部操作栏 */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 32rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.fb-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.fb-btn .icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.liked {
|
||||
color: #FF2D55;
|
||||
}
|
||||
|
||||
.collected {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.fb-public {
|
||||
height: 76rpx;
|
||||
padding: 0 40rpx;
|
||||
border-radius: 38rpx;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fb-public.public {
|
||||
background: #fff;
|
||||
border: 1rpx solid #007AFF;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.fb-share {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
/* 分享:原生 button 去默认外观 */
|
||||
.od-share {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
border: 0;
|
||||
background: var(--fill);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #007AFF;
|
||||
color: var(--ink);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.fb-share::after {
|
||||
.od-share::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fb-share .icon {
|
||||
.od-share .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"clothes-card": "/components/clothes-card/clothes-card"
|
||||
"clothes-card": "/components/clothes-card/clothes-card",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { request } from '../../utils/request'
|
||||
import { OutfitItem, ClothesItem, PagedResult } from '../../types/models'
|
||||
import { normalizePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -36,8 +37,19 @@ Page({
|
||||
hasMore: true,
|
||||
page: 1,
|
||||
saving: false,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 删除确认:v2 玻璃弹窗
|
||||
delVisible: false,
|
||||
delButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '确认删除', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid, '编辑态=', !!uuid)
|
||||
@@ -233,13 +245,16 @@ Page({
|
||||
},
|
||||
|
||||
onDelete() {
|
||||
wx.showModal({
|
||||
title: '确认删除这套穿搭?',
|
||||
content: '删除后不可恢复,但不会影响衣橱内的单品。',
|
||||
confirmText: '确认删除',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({ delVisible: true })
|
||||
},
|
||||
|
||||
onDelCancel() {
|
||||
this.setData({ delVisible: false })
|
||||
},
|
||||
|
||||
onDelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ delVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/outfit/${this.data.uuid}`, method: 'DELETE' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '穿搭删除成功')
|
||||
@@ -247,14 +262,11 @@ Page({
|
||||
setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
fail: (e2) => logger.warn(LOG, 'navigateBack 失败, err=', e2 && e2.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '穿搭删除失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '删除弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((e2: any) => logger.warn(LOG, '穿搭删除失败, code=', e2 && e2.code))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,114 +1,170 @@
|
||||
<view class="page edit-page">
|
||||
<navigation-bar title="{{view === 'select' ? '选择衣橱单品' : (isEdit ? '编辑穿搭' : '创建穿搭')}}">
|
||||
<view wx:if="{{view === 'select'}}" slot="right" class="nav-cancel" bindtap="backToForm">取消</view>
|
||||
<wxs module="cl">
|
||||
function cls(n) {
|
||||
if (n >= 4) return ''
|
||||
if (n === 3) return 'collage--3'
|
||||
if (n === 2) return 'collage--2'
|
||||
return 'collage--1'
|
||||
}
|
||||
module.exports = { cls: cls }
|
||||
</wxs>
|
||||
|
||||
<view class="page page-flex">
|
||||
<navigation-bar
|
||||
title="{{view === 'select' ? '选择衣橱单品' : (isEdit ? '编辑穿搭' : '创建穿搭')}}"
|
||||
scrolled="{{navScrolled}}"
|
||||
>
|
||||
<view wx:if="{{view === 'select'}}" slot="right" class="nav-action" bindtap="backToForm">取消</view>
|
||||
</navigation-bar>
|
||||
|
||||
<!-- 表单视图 -->
|
||||
<scroll-view wx:if="{{view === 'form'}}" scroll-y class="scroll-body">
|
||||
<scroll-view wx:if="{{view === 'form'}}" scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<view class="head">
|
||||
<view class="eyebrow">{{isEdit ? 'Edit Look' : 'New Look'}}</view>
|
||||
<view class="display">{{isEdit ? '编辑穿搭' : '创建穿搭'}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 拼图预览 -->
|
||||
<view class="card preview">
|
||||
<view wx:if="{{selectedItems.length}}" class="preview-items">
|
||||
<view
|
||||
class="preview-item preview-{{index}}"
|
||||
wx:for="{{selectedItems}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="preview-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger preview-placeholder"></view>
|
||||
<view class="oe-preview">
|
||||
<view wx:if="{{selectedItems.length}}" class="collage {{cl.cls(selectedItems.length)}}">
|
||||
<view class="cg" wx:for="{{selectedItems}}" wx:key="clothes_uuid" wx:if="{{index < 4}}">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view wx:if="{{selectedItems.length > 5}}" class="preview-more">+{{selectedItems.length - 5}}</view>
|
||||
<view wx:if="{{selectedItems.length > 4}}" class="cg-more">+{{selectedItems.length - 4}}</view>
|
||||
<text class="cg-badge num">{{selectedItems.length}} 件</text>
|
||||
</view>
|
||||
<view wx:else class="preview-empty">
|
||||
<view wx:else class="oe-empty">
|
||||
<view class="icon icon-hanger"></view>
|
||||
<text class="pe-title">还没有选择衣物</text>
|
||||
<text class="pe-desc">点击下方「选择衣物」添加你的单品</text>
|
||||
<text class="oe-empty-t">还没有选择衣物</text>
|
||||
<text class="oe-empty-d">点击下方「选择衣橱单品」添加你的单品</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="card form-card">
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
<text>穿搭名称 <text class="req">*</text></text>
|
||||
<text class="count">{{outfitName.length}}/32</text>
|
||||
<view class="oe-form">
|
||||
<view class="oe-field">
|
||||
<view class="oe-label">
|
||||
<text>穿搭名称 <text class="oe-req">*</text></text>
|
||||
<text class="oe-count num">{{outfitName.length}}/32</text>
|
||||
</view>
|
||||
<input class="input" value="{{outfitName}}" bindinput="onNameInput" placeholder="给这套穿搭起个名字" maxlength="32" />
|
||||
<input
|
||||
class="input"
|
||||
value="{{outfitName}}"
|
||||
bindinput="onNameInput"
|
||||
placeholder="给这套穿搭起个名字"
|
||||
placeholder-class="input-ph"
|
||||
maxlength="32"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
|
||||
<view class="oe-field">
|
||||
<view class="oe-label">
|
||||
<text>穿搭描述</text>
|
||||
<text class="count">{{outfitDesc.length}}/500</text>
|
||||
<text class="oe-count num">{{outfitDesc.length}}/500</text>
|
||||
</view>
|
||||
<textarea class="textarea" value="{{outfitDesc}}" bindinput="onDescInput" placeholder="简单描述这套穿搭的灵感或亮点" maxlength="500" />
|
||||
<textarea
|
||||
class="textarea"
|
||||
value="{{outfitDesc}}"
|
||||
bindinput="onDescInput"
|
||||
placeholder="简单描述这套穿搭的灵感或亮点"
|
||||
placeholder-class="input-ph"
|
||||
maxlength="500"
|
||||
/>
|
||||
</view>
|
||||
<view class="field">
|
||||
<view class="field-label">
|
||||
|
||||
<view class="oe-field">
|
||||
<view class="oe-label">
|
||||
<text>适合场景</text>
|
||||
<text class="count">{{scene.length}}/100</text>
|
||||
<text class="oe-count num">{{scene.length}}/100</text>
|
||||
</view>
|
||||
<input class="input" value="{{scene}}" bindinput="onSceneInput" placeholder="例如:通勤、约会、旅行" maxlength="100" />
|
||||
<input
|
||||
class="input"
|
||||
value="{{scene}}"
|
||||
bindinput="onSceneInput"
|
||||
placeholder="例如:通勤、约会、旅行"
|
||||
placeholder-class="input-ph"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 选择衣物入口 -->
|
||||
<view class="card select-entry" bindtap="openSelect">
|
||||
<view class="entry-left">
|
||||
<view class="entry-icon"><view class="icon icon-shirt"></view></view>
|
||||
<view>
|
||||
<view class="entry-title">选择衣橱单品</view>
|
||||
<view class="entry-desc">{{selectedUuids.length ? '点击调整当前穿搭包含的衣物' : '选择 1~10 件衣物合成画报'}}</view>
|
||||
<view class="grp-hd oe-grp"><text class="gh-cn">单品</text><text class="gh-en">Items</text></view>
|
||||
<view class="rows">
|
||||
<view class="row" bindtap="openSelect">
|
||||
<view class="icon row-ic icon-shirt"></view>
|
||||
<view class="row-lb">
|
||||
<view>选择衣橱单品</view>
|
||||
<view class="oe-entry-d">{{selectedUuids.length ? '点击调整当前穿搭包含的衣物' : '选择 1~10 件衣物合成画报'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="entry-right">
|
||||
<text class="entry-count {{selectedUuids.length ? 'on' : ''}}">{{selectedUuids.length ? '已选 ' + selectedUuids.length + ' 件' : '未选择'}}</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
<text class="row-vl">{{selectedUuids.length ? '已选 ' + selectedUuids.length + ' 件' : '未选择'}}</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="footer">
|
||||
<view wx:if="{{isEdit}}" class="btn-danger-text delete-link" bindtap="onDelete">删除穿搭</view>
|
||||
<view
|
||||
class="btn btn-primary {{(!outfitName || !selectedUuids.length) ? 'disabled' : ''}}"
|
||||
bindtap="onSave"
|
||||
>{{isEdit ? '保存修改' : '保存'}}</view>
|
||||
</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 多选视图 -->
|
||||
<view wx:else class="select-view">
|
||||
<scroll-view scroll-x class="cat-scroll" show-scrollbar="{{false}}">
|
||||
<view class="cat-row">
|
||||
<block wx:else>
|
||||
<view class="segbar">
|
||||
<scroll-view scroll-x class="seg-scroll" show-scrollbar="{{false}}">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="name"
|
||||
class="cat-item {{activeCategory === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeCategory === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onCategoryTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<scroll-view scroll-y class="select-body">
|
||||
<view wx:if="{{selectLoading}}" class="grid">
|
||||
<view class="grid-item" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this">
|
||||
<view class="skeleton sk-card"></view>
|
||||
<scroll-view scroll-y class="select-body pb-dock" bindscroll="onNavScroll">
|
||||
<view wx:if="{{selectLoading}}" class="oe-grid">
|
||||
<view class="oe-skel" wx:for="{{[1, 2, 3, 4]}}" wx:key="*this">
|
||||
<view class="sk sk-img"></view>
|
||||
<view class="sk sk-ln" style="width: 62%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="grid">
|
||||
<view class="grid-item" wx:for="{{allClothes}}" wx:key="clothes_uuid">
|
||||
<view wx:else class="oe-grid">
|
||||
<view class="oe-tile" wx:for="{{allClothes}}" wx:key="clothes_uuid">
|
||||
<clothes-card item="{{item}}" selectable selected="{{item._selected}}" bind:select="onToggleSelect" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="gap-32"></view>
|
||||
</scroll-view>
|
||||
</block>
|
||||
|
||||
<view class="select-footer">
|
||||
<view>
|
||||
<view class="sf-count">已选 <text class="t-primary">{{selectedUuids.length}}</text> 件</view>
|
||||
<view class="sf-desc">最多可选择 10 件单品</view>
|
||||
<!-- 悬浮底部操作条 -->
|
||||
<view class="dock">
|
||||
<block wx:if="{{view === 'form'}}">
|
||||
<view wx:if="{{isEdit}}" class="btn--text oe-del" bindtap="onDelete">删除穿搭</view>
|
||||
<view
|
||||
class="btn btn--ink btn--block {{(!outfitName || !selectedUuids.length || saving) ? 'is-off' : ''}}"
|
||||
bindtap="onSave"
|
||||
>{{saving ? '保存中…' : (isEdit ? '保存修改' : '保存')}}</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="dock-row">
|
||||
<view class="oe-count-wrap">
|
||||
<view class="oe-count-t num">已选 {{selectedUuids.length}} 件</view>
|
||||
<view class="oe-count-d">最多可选择 10 件单品</view>
|
||||
</view>
|
||||
<view class="sf-confirm btn btn-primary" bindtap="confirmSelect">确定</view>
|
||||
<view class="btn btn--ink btn--sm oe-confirm" bindtap="confirmSelect">确定</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 删除确认 -->
|
||||
<v2-overlay
|
||||
visible="{{delVisible}}"
|
||||
type="modal"
|
||||
title="确认删除这套穿搭?"
|
||||
content="删除后不可恢复,但不会影响衣橱内的单品。"
|
||||
buttons="{{delButtons}}"
|
||||
bind:confirm="onDelConfirm"
|
||||
bind:cancel="onDelCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.edit-page {
|
||||
/** 穿搭编辑:无缝拼图预览 + 发丝线表单 + 悬浮底部操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,288 +11,142 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.nav-cancel {
|
||||
font-size: 30rpx;
|
||||
color: #007AFF;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 预览区 */
|
||||
.preview {
|
||||
margin: 24rpx;
|
||||
height: 320rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.oe-preview {
|
||||
margin: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
.oe-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #C7C7CC;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1 / 1;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
.preview-empty .icon {
|
||||
.oe-empty .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.pe-title {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 16rpx;
|
||||
.oe-empty-t {
|
||||
margin-top: 24rpx;
|
||||
font-family: var(--serif);
|
||||
font-size: 32rpx;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.pe-desc {
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.preview-items {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
position: absolute;
|
||||
width: 140rpx;
|
||||
height: 180rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 16rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-0 { left: 30rpx; top: 60rpx; transform: rotate(-5deg); }
|
||||
.preview-1 { left: 120rpx; top: 100rpx; transform: rotate(3deg); }
|
||||
.preview-2 { left: 210rpx; top: 50rpx; transform: rotate(6deg); }
|
||||
.preview-3 { left: 300rpx; top: 110rpx; transform: rotate(-3deg); }
|
||||
.preview-4 { left: 390rpx; top: 70rpx; transform: rotate(8deg); }
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.preview-more {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 24rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.oe-empty-d {
|
||||
margin-top: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 表单 */
|
||||
.form-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
.oe-form {
|
||||
padding: 64rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 32rpx;
|
||||
.oe-field {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.field:last-child {
|
||||
.oe-field:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
.oe-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
color: var(--ink-2);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.req {
|
||||
color: #FF3B30;
|
||||
.oe-req {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.count {
|
||||
.oe-count {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
font-weight: 400;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 选择衣物入口 */
|
||||
.select-entry {
|
||||
margin: 0 24rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.oe-grp {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.entry-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
color: #007AFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.entry-icon .icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
}
|
||||
|
||||
.entry-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.entry-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.entry-count {
|
||||
font-size: 26rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.entry-count.on {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.entry-right .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
/* 底部按钮 */
|
||||
.footer {
|
||||
padding: 16rpx 24rpx;
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.delete-link {
|
||||
text-align: center;
|
||||
padding: 16rpx 0 24rpx;
|
||||
.oe-entry-d {
|
||||
margin-top: 6rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 多选视图 */
|
||||
.select-view {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cat-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 32rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.cat-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.select-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.oe-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 44rpx 26rpx;
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.oe-skel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx;
|
||||
gap: 24rpx;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: calc(50% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
.oe-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 360rpx;
|
||||
border-radius: 32rpx;
|
||||
/* 底部操作条 */
|
||||
.oe-del {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 8rpx 0 28rpx;
|
||||
}
|
||||
|
||||
.select-footer {
|
||||
flex-shrink: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
.oe-count-wrap {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sf-count {
|
||||
.oe-count-t {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #1C1C1E;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.sf-desc {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
.oe-count-d {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.sf-confirm {
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
.oe-confirm {
|
||||
width: 240rpx;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"empty-state": "/components/empty-state/empty-state"
|
||||
"empty-state": "/components/empty-state/empty-state",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { request, AUTH_EXPIRED_CODES } from '../../utils/request'
|
||||
import { OutfitShareDetail, OutfitItem } from '../../types/models'
|
||||
import { ShareStatus } from '../../utils/config'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -10,14 +11,26 @@ Page({
|
||||
data: {
|
||||
uuid: '',
|
||||
detail: null as OutfitShareDetail | null,
|
||||
outfit: { items: [] } as OutfitItem,
|
||||
outfit: { items: [] } as unknown as OutfitItem,
|
||||
loading: true,
|
||||
invalid: false,
|
||||
invalidTitle: '该分享已取消',
|
||||
invalidDesc: '分享链接已失效',
|
||||
needLogin: false,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 取消分享确认:v2 玻璃弹窗
|
||||
cancelVisible: false,
|
||||
cancelContent: '取消后该分享链接将失效',
|
||||
cancelButtons: [
|
||||
{ text: '再想想', value: '' },
|
||||
{ text: '取消分享', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad(query: Record<string, string>) {
|
||||
const uuid = query.uuid || ''
|
||||
logger.debug(LOG, 'onLoad, uuid=', uuid)
|
||||
@@ -93,13 +106,16 @@ Page({
|
||||
},
|
||||
|
||||
onCancel() {
|
||||
wx.showModal({
|
||||
title: '取消分享',
|
||||
content: '取消后该分享链接将失效',
|
||||
confirmText: '取消分享',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this.setData({ cancelVisible: true })
|
||||
},
|
||||
|
||||
onCancelCancel() {
|
||||
this.setData({ cancelVisible: false })
|
||||
},
|
||||
|
||||
onCancelConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ cancelVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
request({ url: `/outfit-share/${this.data.uuid}/cancel`, method: 'POST' })
|
||||
.then(() => {
|
||||
logger.info(LOG, '取消分享成功')
|
||||
@@ -107,15 +123,12 @@ Page({
|
||||
;(this as any).backTimer = setTimeout(
|
||||
() =>
|
||||
wx.navigateBack({
|
||||
fail: (e: any) => logger.warn(LOG, 'navigateBack 失败, err=', e && e.errMsg),
|
||||
fail: (err: any) => logger.warn(LOG, 'navigateBack 失败, err=', err && err.errMsg),
|
||||
}),
|
||||
500,
|
||||
)
|
||||
})
|
||||
.catch((e: any) => logger.warn(LOG, '取消分享失败, code=', e && e.code))
|
||||
},
|
||||
fail: (e: any) => logger.warn(LOG, '取消分享弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
.catch((err: any) => logger.warn(LOG, '取消分享失败, code=', err && err.code))
|
||||
},
|
||||
|
||||
onBackHome() {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<view class="page detail-page">
|
||||
<navigation-bar title="穿搭分享" />
|
||||
<wxs module="cl">
|
||||
function cls(n) {
|
||||
if (n >= 4) return ''
|
||||
if (n === 3) return 'collage--3'
|
||||
if (n === 2) return 'collage--2'
|
||||
return 'collage--1'
|
||||
}
|
||||
module.exports = { cls: cls }
|
||||
</wxs>
|
||||
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="穿搭分享" scrolled="{{navScrolled}}" />
|
||||
|
||||
<empty-state
|
||||
wx:if="{{invalid && needLogin}}"
|
||||
@@ -19,69 +29,86 @@
|
||||
bind:action="onBackHome"
|
||||
/>
|
||||
|
||||
<scroll-view wx:else scroll-y class="scroll-body">
|
||||
<!-- 分享者信息 -->
|
||||
<view class="card owner-card">
|
||||
<view class="avatar">
|
||||
<image wx:if="{{detail.owner_avatar}}" class="avatar-img" src="{{detail.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user avatar-placeholder"></view>
|
||||
<scroll-view wx:else scroll-y class="scroll-body pb-dock" bindscroll="onNavScroll">
|
||||
<!-- 分享者行 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Shared Look</view>
|
||||
<view class="display">{{outfit.outfit_name}}</view>
|
||||
</view>
|
||||
<view class="owner-info">
|
||||
<view class="owner-name">{{detail.owner_name || '好友'}} 分享了穿搭</view>
|
||||
<view class="owner-expire">共 {{outfit.items.length}} 件单品</view>
|
||||
|
||||
<view class="osd-owner">
|
||||
<view class="osd-av">
|
||||
<image wx:if="{{detail.owner_avatar}}" class="pic" src="{{detail.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user"></view>
|
||||
</view>
|
||||
<view class="osd-owner-body">
|
||||
<view class="osd-owner-n">{{detail.owner_name || '好友'}} 分享了穿搭</view>
|
||||
<view class="osd-owner-e num">共 {{outfit.items.length}} 件单品</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 拼图封面 -->
|
||||
<view class="cover">
|
||||
<!-- 无缝拼图封面 -->
|
||||
<view class="osd-cover">
|
||||
<view class="collage {{cl.cls(outfit.items.length)}}">
|
||||
<view class="cg" wx:for="{{outfit.items}}" wx:key="clothes_uuid" wx:if="{{index < 4}}">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view wx:if="{{outfit.items.length > 4}}" class="cg-more">+{{outfit.items.length - 4}}</view>
|
||||
<text class="cg-badge num">{{outfit.items.length}} 件</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息区 -->
|
||||
<view class="osd-info">
|
||||
<view class="osd-tags">
|
||||
<text wx:if="{{outfit.scene}}" class="pill pill--line">{{outfit.scene}}</text>
|
||||
<text class="pill {{outfit.source === 2 ? 'pill--ink' : ''}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
|
||||
</view>
|
||||
<view class="t-bd osd-desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 包含衣物:横滑(点击只读查看) -->
|
||||
<view class="osd-items">
|
||||
<view class="grp-hd">
|
||||
<text class="gh-cn">包含衣物</text>
|
||||
<text class="gh-en">Items</text>
|
||||
</view>
|
||||
<scroll-view scroll-x class="osd-items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="osd-items-row">
|
||||
<view
|
||||
class="cover-item cover-{{index}}"
|
||||
wx:for="{{outfit.items}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="cover-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger cover-placeholder"></view>
|
||||
</view>
|
||||
<view wx:if="{{!outfit.items.length}}" class="cover-empty">
|
||||
<view class="icon icon-hanger"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 信息卡片 -->
|
||||
<view class="card info-card">
|
||||
<view class="title-row">
|
||||
<text class="title">{{outfit.outfit_name}}</text>
|
||||
</view>
|
||||
<view class="tag-row">
|
||||
<text wx:if="{{outfit.scene}}" class="tag tag-gray">{{outfit.scene}}</text>
|
||||
<text class="tag {{outfit.source === 2 ? 'tag-indigo' : 'tag-gray'}}">{{outfit.source === 2 ? 'AI 推荐' : '自建'}}</text>
|
||||
</view>
|
||||
<view class="desc" wx:if="{{outfit.outfit_desc}}">{{outfit.outfit_desc}}</view>
|
||||
|
||||
<!-- 包含衣物(点击只读查看) -->
|
||||
<view class="items-title">包含衣物</view>
|
||||
<scroll-view scroll-x class="items-scroll" show-scrollbar="{{false}}">
|
||||
<view class="items-row">
|
||||
<view
|
||||
class="item-chip"
|
||||
class="osd-chip"
|
||||
wx:for="{{outfit.items}}"
|
||||
wx:key="clothes_uuid"
|
||||
data-uuid="{{item.clothes_uuid}}"
|
||||
bindtap="onItemTap"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="item-img" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="item-img item-img-empty"><view class="icon icon-hanger"></view></view>
|
||||
<view class="osd-thumb">
|
||||
<image wx:if="{{item.processed_image}}" class="pic" src="{{item.processed_image}}" mode="aspectFill" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 转发出口 -->
|
||||
<button class="share-btn" open-type="share">分享给好友</button>
|
||||
|
||||
<!-- 属主取消 -->
|
||||
<view wx:if="{{detail.is_owner}}" class="cancel-btn btn-danger-text" bindtap="onCancel">取消分享</view>
|
||||
<view class="footer-space"></view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮玻璃操作条 -->
|
||||
<view class="dock" wx:if="{{!invalid}}">
|
||||
<view wx:if="{{detail.is_owner}}" class="btn--text osd-cancel" bindtap="onCancel">取消分享</view>
|
||||
<button class="btn btn--ink btn--block" open-type="share">分享给好友</button>
|
||||
</view>
|
||||
|
||||
<!-- 取消分享确认 -->
|
||||
<v2-overlay
|
||||
visible="{{cancelVisible}}"
|
||||
type="modal"
|
||||
title="取消分享"
|
||||
content="{{cancelContent}}"
|
||||
buttons="{{cancelButtons}}"
|
||||
bind:confirm="onCancelConfirm"
|
||||
bind:cancel="onCancelCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
.detail-page {
|
||||
/** 穿搭分享详情:分享者行 + 无缝拼图 + 横滑单品 + 悬浮玻璃操作条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-body {
|
||||
@@ -9,193 +11,111 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* 分享者信息 */
|
||||
.owner-card {
|
||||
margin: 24rpx;
|
||||
.osd-owner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
.osd-av {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
.osd-av .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
.osd-av .icon {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.owner-info {
|
||||
margin-left: 24rpx;
|
||||
.osd-owner-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.osd-owner-n {
|
||||
font-size: 27rpx;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.owner-expire {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 8rpx;
|
||||
.osd-owner-e {
|
||||
margin-top: 6rpx;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
/* 封面 */
|
||||
.cover {
|
||||
margin: 0 24rpx 24rpx;
|
||||
height: 480rpx;
|
||||
background: #fff;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.osd-cover {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.cover-item {
|
||||
position: absolute;
|
||||
width: 200rpx;
|
||||
height: 260rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
.osd-info {
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.cover-0 { left: 30rpx; top: 80rpx; transform: rotate(-6deg); }
|
||||
.cover-1 { left: 120rpx; top: 140rpx; transform: rotate(2deg); }
|
||||
.cover-2 { left: 220rpx; top: 70rpx; transform: rotate(5deg); }
|
||||
.cover-3 { left: 320rpx; top: 150rpx; transform: rotate(-2deg); }
|
||||
.cover-4 { left: 420rpx; top: 90rpx; transform: rotate(7deg); }
|
||||
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cover-placeholder {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.cover-empty {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.cover-empty .icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
}
|
||||
|
||||
/* 信息卡 */
|
||||
.info-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
.osd-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
margin-top: 20rpx;
|
||||
line-height: 1.6;
|
||||
.osd-desc {
|
||||
margin-top: 26rpx;
|
||||
}
|
||||
|
||||
.items-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-top: 32rpx;
|
||||
.osd-items {
|
||||
margin-top: 64rpx;
|
||||
}
|
||||
|
||||
.items-scroll {
|
||||
margin-top: 20rpx;
|
||||
.osd-items-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.items-row {
|
||||
.osd-items-row {
|
||||
display: inline-flex;
|
||||
gap: 24rpx;
|
||||
padding: 0 var(--pad);
|
||||
}
|
||||
|
||||
.item-chip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
.osd-chip {
|
||||
width: 160rpx;
|
||||
}
|
||||
|
||||
.item-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.item-img-empty {
|
||||
.osd-thumb {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: var(--paper-2);
|
||||
border-radius: var(--r-img);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.item-img-empty .icon {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
.osd-thumb .ph {
|
||||
width: 52%;
|
||||
height: 52%;
|
||||
color: var(--art);
|
||||
}
|
||||
|
||||
/* 转发出口 */
|
||||
.share-btn {
|
||||
margin: 32rpx 24rpx 0;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary, #000);
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
.osd-thumb .pic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
.osd-cancel {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 32rpx 0;
|
||||
}
|
||||
|
||||
.footer-space {
|
||||
height: 40rpx;
|
||||
padding: 8rpx 0 28rpx;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,49 @@
|
||||
import { request, redirectToLogin } from '../../utils/request'
|
||||
import * as auth from '../../utils/auth'
|
||||
import { PagedResult, OutfitItem } from '../../types/models'
|
||||
import { PagedResult, OutfitItem, CommunityOutfitItem } from '../../types/models'
|
||||
import { OutfitSource, IsPublic, STORAGE_KEYS } from '../../utils/config'
|
||||
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { setTabBarSelected } from '../../utils/tabbar'
|
||||
import { formatRelativeTime } from '../../utils/format'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'outfit'
|
||||
|
||||
// 拼图封面单元:无图时前端渲染线稿占位
|
||||
type CoverCell = { k: string; img: string }
|
||||
|
||||
// 列表行:我的穿搭/已收藏来自 OutfitItem;社区来自 CommunityOutfitItem(额外带作者与互动计数)。
|
||||
// 统一为交叉类型,模板按 activeTab 条件渲染差异字段。
|
||||
type OutfitRow = OutfitItem & Partial<CommunityOutfitItem> & {
|
||||
_likeCount?: number
|
||||
_collectCount?: number
|
||||
// cls 为胶囊类名(零彩色:pill--ink / pill--line / 空)
|
||||
_status?: { text: string; cls: string }
|
||||
_time?: string
|
||||
_covers?: CoverCell[]
|
||||
_collageCls?: string
|
||||
_moreCount?: number
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
activeTab: 0,
|
||||
list: [] as OutfitItem[],
|
||||
list: [] as OutfitRow[],
|
||||
loading: true,
|
||||
refreshing: false,
|
||||
loadingMore: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
@@ -73,16 +96,16 @@ Page({
|
||||
},
|
||||
|
||||
fetchList(page: number): Promise<void> {
|
||||
const isCollect = this.data.activeTab === 1
|
||||
const url = isCollect ? '/collect' : '/outfit'
|
||||
const tab = this.data.activeTab
|
||||
const url = tab === 1 ? '/collect' : tab === 2 ? '/outfit/community' : '/outfit'
|
||||
|
||||
const seq = ((this as any).reqSeq || 0) + 1
|
||||
;(this as any).reqSeq = seq
|
||||
if (page > 1) this.setData({ loadingMore: true })
|
||||
|
||||
return request<PagedResult<OutfitItem>>({ url, data: { page, page_size: this.data.pageSize } })
|
||||
return request<PagedResult<OutfitRow>>({ url, data: { page, page_size: this.data.pageSize } })
|
||||
.then(async (res) => {
|
||||
const { list: raw, total } = normalizePaged<OutfitItem>(res)
|
||||
const { list: raw, total } = normalizePaged<OutfitRow>(res)
|
||||
logger.debug(LOG, '列表加载成功, url=', url, 'page=', page, '条数=', raw && raw.length)
|
||||
const items = await this.decorateList(raw)
|
||||
// decorateList 是异步的,期间可能已切换 tab:序号不匹配则丢弃
|
||||
@@ -103,7 +126,18 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
decorateList(list: OutfitItem[]): Promise<OutfitItem[]> {
|
||||
decorateList(list: OutfitRow[]): Promise<OutfitRow[]> {
|
||||
// 社区分段:作者与互动计数由后端一次性下发,本地仅补相对时间,避免逐条请求点赞接口
|
||||
if (this.data.activeTab === 2) {
|
||||
return Promise.resolve(
|
||||
list.map((it) => ({
|
||||
...it,
|
||||
_likeCount: it.like_count || 0,
|
||||
_collectCount: it.collect_count || 0,
|
||||
_time: formatRelativeTime(it.created_at),
|
||||
})),
|
||||
)
|
||||
}
|
||||
return Promise.all(
|
||||
list.map(async (it) => {
|
||||
let likeCount = 0
|
||||
@@ -116,15 +150,31 @@ Page({
|
||||
} catch (e: any) {
|
||||
logger.warn(LOG, '点赞数拉取失败, uuid=', it.outfit_uuid, 'code=', e && e.code)
|
||||
}
|
||||
return { ...it, _likeCount: likeCount, _status: this.getStatusText(it) }
|
||||
return { ...it, ...this.decorateCover(it), _likeCount: likeCount, _status: this.getStatusText(it) }
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
getStatusText(it: OutfitItem): { text: string; type: string } {
|
||||
if (it.source === OutfitSource.AI) return { text: 'AI 推荐', type: 'indigo' }
|
||||
if (it.is_public === IsPublic.PUBLIC) return { text: '已公开', type: 'blue' }
|
||||
return { text: '仅自己可见', type: 'gray' }
|
||||
// 无缝拼图封面:取前 4 件,超出用 +N 角标;不足 1 件时补一个线稿占位格
|
||||
decorateCover(it: OutfitItem): { _covers: CoverCell[]; _collageCls: string; _moreCount: number } {
|
||||
const items = it.items || []
|
||||
const covers: CoverCell[] = items.slice(0, 4).map((c) => ({
|
||||
k: c.clothes_uuid,
|
||||
img: c.processed_image || '',
|
||||
}))
|
||||
if (!covers.length) covers.push({ k: 'empty', img: '' })
|
||||
return {
|
||||
_covers: covers,
|
||||
_collageCls: `collage--${covers.length}`,
|
||||
_moreCount: Math.max(items.length - 4, 0),
|
||||
}
|
||||
},
|
||||
|
||||
// 零彩色状态语义:AI 来源 = 黑底白字,已公开 = 描边,仅自己可见 = 浅灰
|
||||
getStatusText(it: OutfitItem): { text: string; cls: string } {
|
||||
if (it.source === OutfitSource.AI) return { text: 'AI 推荐', cls: 'pill--ink' }
|
||||
if (it.is_public === IsPublic.PUBLIC) return { text: '已公开', cls: 'pill--line' }
|
||||
return { text: '仅自己可见', cls: '' }
|
||||
},
|
||||
|
||||
onCreate() {
|
||||
|
||||
@@ -1,84 +1,106 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="穿搭" back="{{false}}" />
|
||||
<navigation-bar title="穿搭" back="{{false}}" scrolled="{{navScrolled}}" />
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-body"
|
||||
class="scroll-body pb-float"
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindscrolltolower="onReachBottom"
|
||||
bindscroll="onNavScroll"
|
||||
>
|
||||
<!-- 分段控件 -->
|
||||
<view class="topbar">
|
||||
<view class="segment">
|
||||
<view class="segment-item {{activeTab === 0 ? 'active' : ''}}" data-index="0" bindtap="onTabChange">我的穿搭</view>
|
||||
<view class="segment-item {{activeTab === 1 ? 'active' : ''}}" data-index="1" bindtap="onTabChange">已收藏</view>
|
||||
</view>
|
||||
<!-- 大标题页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Styling</view>
|
||||
<view class="display">穿搭</view>
|
||||
</view>
|
||||
|
||||
<!-- 入口按钮 -->
|
||||
<view class="entry-row">
|
||||
<view class="entry-btn entry-outline" bindtap="onCreate">+ 创建穿搭</view>
|
||||
<view class="entry-btn entry-primary" bindtap="onAiRecommend">AI 推荐</view>
|
||||
<!-- 下划线式文字分段 -->
|
||||
<view class="segbar">
|
||||
<view class="seg">
|
||||
<view class="seg-item {{activeTab === 0 ? 'is-on' : ''}}" data-index="0" bindtap="onTabChange">我的穿搭</view>
|
||||
<view class="seg-item {{activeTab === 1 ? 'is-on' : ''}}" data-index="1" bindtap="onTabChange">已收藏</view>
|
||||
<view class="seg-item {{activeTab === 2 ? 'is-on' : ''}}" data-index="2" bindtap="onTabChange">社区</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading}}" class="list">
|
||||
<view class="skeleton sk-card" wx:for="{{[1, 2, 3]}}" wx:key="*this"></view>
|
||||
<view wx:if="{{loading}}" class="of-skel">
|
||||
<view class="of-skel-item" wx:for="{{[1, 2]}}" wx:key="*this">
|
||||
<view class="sk sk-img sk-cover"></view>
|
||||
<view class="sk sk-ln" style="width: 44%;"></view>
|
||||
<view class="sk sk-ln" style="width: 72%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<empty-state
|
||||
wx:elif="{{!list.length}}"
|
||||
icon="icon-shirt"
|
||||
title="{{activeTab === 0 ? '还没有穿搭' : '还没有收藏的穿搭'}}"
|
||||
desc="{{activeTab === 0 ? '从衣橱挑几件,搭一套吧' : ''}}"
|
||||
title="{{activeTab === 0 ? '还没有穿搭' : activeTab === 1 ? '还没有收藏的穿搭' : '还没有公开的穿搭'}}"
|
||||
desc="{{activeTab === 0 ? '从衣橱挑几件,搭一套吧' : activeTab === 1 ? '在社区或 AI 推荐中点亮小星星,喜欢的穿搭会出现在这里' : '把喜欢的穿搭公开到社区,让大家看到你的搭配'}}"
|
||||
buttonText="{{activeTab === 0 ? '创建穿搭' : ''}}"
|
||||
bind:action="onCreate"
|
||||
/>
|
||||
|
||||
<!-- 卡片流 -->
|
||||
<view wx:else class="list">
|
||||
<view wx:else class="of-list">
|
||||
<view
|
||||
class="outfit-card card"
|
||||
class="of-card"
|
||||
wx:for="{{list}}"
|
||||
wx:key="outfit_uuid"
|
||||
data-uuid="{{item.outfit_uuid}}"
|
||||
bindtap="onOutfitTap"
|
||||
>
|
||||
<view class="cover">
|
||||
<view
|
||||
class="cover-item cover-{{index}}"
|
||||
wx:for="{{item.items}}"
|
||||
wx:key="clothes_uuid"
|
||||
wx:if="{{index < 5}}"
|
||||
>
|
||||
<image wx:if="{{item.processed_image}}" class="cover-img" src="{{item.processed_image}}" mode="aspectFit" />
|
||||
<view wx:else class="icon icon-hanger cover-ph"></view>
|
||||
<!-- 作者行:仅社区分段 -->
|
||||
<view class="of-author" wx:if="{{activeTab === 2}}">
|
||||
<image wx:if="{{item.owner_avatar}}" class="of-av" src="{{item.owner_avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="of-av">
|
||||
<view class="icon icon-user"></view>
|
||||
</view>
|
||||
<view wx:if="{{!item.items.length}}" class="icon icon-hanger"></view>
|
||||
<text class="cover-count">{{item.items.length}} 件</text>
|
||||
<text class="of-aname">{{item.owner_name || '穿搭达人'}}</text>
|
||||
<text class="of-atime">{{item._time}}</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="title-row">
|
||||
<text class="title">{{item.outfit_name}}</text>
|
||||
<text class="tag tag-{{item._status.type}}">{{item._status.text}}</text>
|
||||
|
||||
<!-- 无缝拼图封面 -->
|
||||
<view class="collage {{item._collageCls}}">
|
||||
<view class="cg" wx:for="{{item._covers}}" wx:for-item="c" wx:key="k">
|
||||
<image wx:if="{{c.img}}" class="pic" src="{{c.img}}" mode="aspectFill" lazy-load />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
<view class="desc" wx:if="{{item.outfit_desc}}">{{item.outfit_desc}}</view>
|
||||
<view class="meta-row">
|
||||
<text class="tag tag-gray" wx:if="{{item.scene}}">{{item.scene}}</text>
|
||||
<text class="meta">{{item.items.length}} 件衣物</text>
|
||||
<view wx:if="{{item._moreCount > 0}}" class="cg-more">+{{item._moreCount}}</view>
|
||||
<text class="cg-badge num">{{item.items.length}} 件</text>
|
||||
</view>
|
||||
|
||||
<view class="of-trow">
|
||||
<text class="of-title">{{item.outfit_name}}</text>
|
||||
<text wx:if="{{activeTab !== 2}}" class="pill {{item._status.cls}}">{{item._status.text}}</text>
|
||||
</view>
|
||||
<view class="of-desc" wx:if="{{item.outfit_desc}}">{{item.outfit_desc}}</view>
|
||||
<view class="of-meta">
|
||||
<text class="pill pill--line" wx:if="{{item.scene}}">{{item.scene}}</text>
|
||||
<text class="of-m">{{item.items.length}} 件衣物</text>
|
||||
<view class="spacer"></view>
|
||||
<view class="meta-icon">
|
||||
<view class="icon icon-heart heart"></view>
|
||||
<text>{{item._likeCount || 0}}</text>
|
||||
<view class="of-m">
|
||||
<view class="icon icon-heart"></view>
|
||||
<text class="num">{{item._likeCount || 0}}</text>
|
||||
</view>
|
||||
<view class="of-m" wx:if="{{activeTab === 2}}">
|
||||
<view class="icon icon-star"></view>
|
||||
<text class="num">{{item._collectCount || 0}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="no-more">没有更多了</view>
|
||||
<view wx:if="{{list.length && !hasMore}}" class="foot-note">没有更多了</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 悬浮玻璃双按钮条(社区为浏览场景,不展示管理入口) -->
|
||||
<view class="float-duo" wx:if="{{activeTab !== 2}}">
|
||||
<view class="btn btn--glass" bindtap="onCreate">创建穿搭</view>
|
||||
<view class="btn btn--ink" bindtap="onAiRecommend">AI 推荐</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 穿搭:下划线分段 + 无缝拼图卡 + 悬浮双按钮条 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,199 +11,137 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
background: #fff;
|
||||
padding: 20rpx 32rpx;
|
||||
/* 分段条:吸顶玻璃 */
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
margin-top: 36rpx;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.segment {
|
||||
.of-list {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
display: flex;
|
||||
background: #F2F2F7;
|
||||
border-radius: 20rpx;
|
||||
padding: 6rpx;
|
||||
flex-direction: column;
|
||||
gap: 56rpx;
|
||||
}
|
||||
|
||||
.segment-item {
|
||||
flex: 1;
|
||||
height: 64rpx;
|
||||
line-height: 64rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #3C3C43;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.segment-item.active {
|
||||
background: #fff;
|
||||
color: #007AFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.entry-btn {
|
||||
flex: 1;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.entry-outline {
|
||||
border: 1rpx solid #007AFF;
|
||||
color: #007AFF;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.entry-primary {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 360rpx;
|
||||
border-radius: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.outfit-card {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.cover {
|
||||
height: 280rpx;
|
||||
background: #F8F8FA;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover .icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.cover-item {
|
||||
position: absolute;
|
||||
width: 150rpx;
|
||||
height: 190rpx;
|
||||
background: #fff;
|
||||
border: 1rpx solid #E5E5EA;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cover-0 { left: 16rpx; top: 40rpx; transform: rotate(-6deg); }
|
||||
.cover-1 { left: 140rpx; top: 82rpx; transform: rotate(3deg); }
|
||||
.cover-2 { left: 268rpx; top: 32rpx; transform: rotate(5deg); }
|
||||
.cover-3 { left: 392rpx; top: 84rpx; transform: rotate(-3deg); }
|
||||
.cover-4 { left: 480rpx; top: 40rpx; transform: rotate(6deg); }
|
||||
|
||||
.cover-img {
|
||||
.of-card {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform .2s var(--ease);
|
||||
}
|
||||
|
||||
.cover-ph {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
color: #C7C7CC;
|
||||
.of-card:active {
|
||||
transform: scale(.985);
|
||||
}
|
||||
|
||||
.cover-count {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #007AFF;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
padding: 4rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
.of-skel {
|
||||
padding: 40rpx var(--pad) 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 56rpx;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
margin-top: 20rpx;
|
||||
.of-skel-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
.sk-cover {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
/* 作者行(仅社区分段) */
|
||||
.of-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
.of-av {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.of-av .icon {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
.of-aname {
|
||||
font-size: 25rpx;
|
||||
letter-spacing: .01em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.of-atime {
|
||||
margin-left: auto;
|
||||
font-size: 22rpx;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.of-trow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.of-title {
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
.of-desc {
|
||||
margin-top: 14rpx;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.62;
|
||||
color: var(--ink-2);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
.of-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
gap: 22rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
display: flex;
|
||||
.of-m {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
color: var(--ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.heart {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
color: #C7C7CC;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #C7C7CC;
|
||||
padding: 24rpx 0 40rpx;
|
||||
.of-m .icon {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { UserInfo, QuotaItem } from '../../types/models'
|
||||
import { QuotaType, STORAGE_KEYS } from '../../utils/config'
|
||||
import { formatDate } from '../../utils/format'
|
||||
import { setTabBarSelected, setTabBarUnread } from '../../utils/tabbar'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -44,8 +45,21 @@ Page({
|
||||
},
|
||||
birthDateStr: '',
|
||||
today: '',
|
||||
// 四栏额度条:上传 / AI 推荐 / 社区分享 / 衣柜分享
|
||||
quotaStats: [] as Array<{ key: number; label: string; remain: number; total: number }>,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// 性别选择:v2 玻璃动作表
|
||||
sexVisible: false,
|
||||
sexItems: [
|
||||
{ text: '男', value: 1 },
|
||||
{ text: '女', value: 2 },
|
||||
],
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onLoad() {
|
||||
this.setToday()
|
||||
},
|
||||
@@ -162,11 +176,17 @@ Page({
|
||||
},
|
||||
|
||||
onSexSelect() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['男', '女'],
|
||||
success: (res) => this.setData({ 'editForm.sex': res.tapIndex + 1 }),
|
||||
fail: (e) => logger.debug(LOG, '取消性别选择, err=', e && e.errMsg),
|
||||
})
|
||||
this.setData({ sexVisible: true })
|
||||
},
|
||||
|
||||
onSexPick(e: WechatMiniprogram.CustomEvent) {
|
||||
const sex = Number(e.detail.value) || 0
|
||||
logger.debug(LOG, '选择性别, sex=', sex)
|
||||
this.setData({ 'editForm.sex': sex, sexVisible: false })
|
||||
},
|
||||
|
||||
onSexCancel() {
|
||||
this.setData({ sexVisible: false })
|
||||
},
|
||||
|
||||
onBirthChange(e: WechatMiniprogram.CustomEvent) {
|
||||
|
||||
@@ -1,97 +1,123 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="我的" back="{{false}}" />
|
||||
<navigation-bar title="我的" back="{{false}}" scrolled="{{navScrolled}}" />
|
||||
|
||||
<scroll-view scroll-y class="scroll-body">
|
||||
<!-- 额度提示条 -->
|
||||
<view wx:if="{{uploadRemain > 0 && uploadRemain <= 2}}" class="quota-banner" bindtap="onTapMall">
|
||||
你的上传额度仅剩 {{uploadRemain}} 次,点击购买额度包 >
|
||||
<scroll-view scroll-y class="scroll-body pb-tab" bindscroll="onNavScroll">
|
||||
<!-- 页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Profile</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人信息 -->
|
||||
<view class="card user-card">
|
||||
<view class="avatar">
|
||||
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user avatar-placeholder"></view>
|
||||
<!-- 资料区:大号头像 + 衬线昵称 + 胶囊标签 -->
|
||||
<view class="pf-hero">
|
||||
<view class="pf-av">
|
||||
<image wx:if="{{userInfo.avatar}}" class="pf-av-img" src="{{userInfo.avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user"></view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="user-name">
|
||||
<text>{{userInfo.username || '微信用户'}}</text>
|
||||
<text wx:if="{{userInfo.sex === 1}}" class="sex-tag">男</text>
|
||||
<text wx:elif="{{userInfo.sex === 2}}" class="sex-tag">女</text>
|
||||
</view>
|
||||
<view class="user-tags">
|
||||
<text wx:if="{{userInfo.vip_type !== 1}}" class="tag tag-blue">月度会员</text>
|
||||
<text wx:if="{{zodiac}}" class="tag tag-gray">{{zodiac}}</text>
|
||||
<text wx:else class="tag tag-gray" bindtap="onEdit">设置生日</text>
|
||||
<view class="pf-info">
|
||||
<view class="pf-name">{{userInfo.username || '微信用户'}}</view>
|
||||
<view class="pf-sub">
|
||||
<text wx:if="{{userInfo.sex === 1}}" class="pill">男</text>
|
||||
<text wx:elif="{{userInfo.sex === 2}}" class="pill">女</text>
|
||||
<text wx:if="{{userInfo.vip_type !== 1}}" class="pill pill--ink">月度会员</text>
|
||||
<text wx:if="{{zodiac}}" class="pill pill--line">{{zodiac}}</text>
|
||||
<text wx:else class="pill pill--line" bindtap="onEdit">设置生日</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="edit-link" bindtap="onEdit">编辑资料<view class="icon icon-chevron-right"></view></view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 1 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-row hairline" bindtap="onTapMall">
|
||||
<view class="icon icon-gauge menu-icon"></view>
|
||||
<text class="menu-label">我的额度</text>
|
||||
<text class="menu-value">上传额度剩 {{uploadRemain}}</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
</view>
|
||||
<view class="menu-row hairline" bindtap="onTapOrder">
|
||||
<view class="icon icon-doc menu-icon"></view>
|
||||
<text class="menu-label">我的订单</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
</view>
|
||||
<view class="menu-row hairline" bindtap="onTapNotification">
|
||||
<view class="icon icon-bell menu-icon"></view>
|
||||
<text class="menu-label">通知</text>
|
||||
<view wx:if="{{unread > 0}}" class="menu-badge">{{unread > 99 ? '99+' : unread}}</view>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
</view>
|
||||
<view class="menu-row hairline" bindtap="onTapCollect">
|
||||
<view class="icon icon-star menu-icon"></view>
|
||||
<text class="menu-label">我的收藏</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
</view>
|
||||
<view class="menu-row" bindtap="onTapFlow">
|
||||
<view class="icon icon-doc menu-icon"></view>
|
||||
<text class="menu-label">额度明细</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
<view class="pf-edit" bindtap="onEdit">
|
||||
编辑资料
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 2 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-row hairline" bindtap="onEdit">
|
||||
<view class="icon icon-pencil menu-icon"></view>
|
||||
<text class="menu-label">编辑资料</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
<!-- 额度提醒 -->
|
||||
<view wx:if="{{uploadRemain > 0 && uploadRemain <= 2}}" class="pf-warn" bindtap="onTapMall">
|
||||
<view class="icon icon-info"></view>
|
||||
<text class="pf-warn-t">你的上传额度仅剩 {{uploadRemain}} 次,点击购买额度包</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="menu-row hairline" bindtap="onEdit">
|
||||
<view class="icon icon-calendar menu-icon"></view>
|
||||
<text class="menu-label">设置生日</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
|
||||
<!-- 我的额度:四栏发丝线数字条 -->
|
||||
<view class="pf-quota" bindtap="onTapMall">
|
||||
<view class="pf-quota-hd">
|
||||
<text class="eyebrow">Quota</text>
|
||||
<text class="qk">我的额度</text>
|
||||
<view class="icon icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="menu-row hairline" bindtap="onTapService">
|
||||
<view class="icon icon-headphones menu-icon"></view>
|
||||
<text class="menu-label">客服与反馈</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
<view class="stats">
|
||||
<view class="stat" wx:for="{{quotaStats}}" wx:key="key">
|
||||
<view class="stat-n num">{{item.remain}}<text class="unit">/{{item.total}}</text></view>
|
||||
<view class="stat-l">{{item.label}}</view>
|
||||
</view>
|
||||
<view class="menu-row" bindtap="onTapAbout">
|
||||
<view class="icon icon-info menu-icon"></view>
|
||||
<text class="menu-label">关于纸条AI衣橱</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="version">v1.0.0</view>
|
||||
<!-- 账户 -->
|
||||
<view class="grp-hd pf-grp"><text class="gh-cn">账户</text><text class="gh-en">Account</text></view>
|
||||
<view class="rows">
|
||||
<view class="row" bindtap="onTapOrder">
|
||||
<view class="icon row-ic icon-doc"></view>
|
||||
<text class="row-lb">我的订单</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onTapNotification">
|
||||
<view class="icon row-ic icon-bell"></view>
|
||||
<text class="row-lb">通知</text>
|
||||
<text wx:if="{{unread > 0}}" class="row-badge">{{unread > 99 ? '99+' : unread}}</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onTapCollect">
|
||||
<view class="icon row-ic icon-star"></view>
|
||||
<text class="row-lb">我的收藏</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onTapFlow">
|
||||
<view class="icon row-ic icon-gauge"></view>
|
||||
<text class="row-lb">额度明细</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 设置 -->
|
||||
<view class="grp-hd pf-grp"><text class="gh-cn">设置</text><text class="gh-en">Settings</text></view>
|
||||
<view class="rows">
|
||||
<view class="row" bindtap="onEdit">
|
||||
<view class="icon row-ic icon-pencil"></view>
|
||||
<text class="row-lb">编辑资料</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onEdit">
|
||||
<view class="icon row-ic icon-calendar"></view>
|
||||
<text class="row-lb">设置生日</text>
|
||||
<text class="row-vl">{{birthDateStr || '未设置'}}</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onTapService">
|
||||
<view class="icon row-ic icon-headphones"></view>
|
||||
<text class="row-lb">客服与反馈</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
<view class="row" bindtap="onTapAbout">
|
||||
<view class="icon row-ic icon-info"></view>
|
||||
<text class="row-lb">关于纸条AI衣橱</text>
|
||||
<view class="icon row-ch icon-chevron-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="pf-ver">v1.0.0</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<view wx:if="{{editVisible}}" class="mask" bindtap="onCloseEdit">
|
||||
<!-- 编辑资料:底部玻璃抽屉 -->
|
||||
<view wx:if="{{editVisible}}" class="scrim" bindtap="onCloseEdit"></view>
|
||||
<view wx:if="{{editVisible}}" class="sheet-wrap">
|
||||
<view class="sheet" catchtap="noop">
|
||||
<view class="sheet-grabber"></view>
|
||||
<view class="sheet-title">编辑资料</view>
|
||||
<view class="sheet-grab"></view>
|
||||
<view class="sheet-hd">
|
||||
<text class="sheet-t">编辑资料</text>
|
||||
<text class="sheet-x" bindtap="onCloseEdit">取消</text>
|
||||
</view>
|
||||
|
||||
<view class="sheet-body">
|
||||
<view class="avatar-wrap">
|
||||
<image wx:if="{{editForm.avatar}}" class="avatar-img" src="{{editForm.avatar}}" mode="aspectFill" />
|
||||
<view wx:else class="icon icon-user avatar-placeholder"></view>
|
||||
@@ -101,26 +127,49 @@
|
||||
|
||||
<view class="form-field">
|
||||
<text class="form-label">昵称</text>
|
||||
<input class="input" type="nickname" value="{{editForm.username}}" bindinput="onNameInput" maxlength="32" placeholder="请输入昵称" />
|
||||
<input
|
||||
class="input form-input"
|
||||
type="nickname"
|
||||
value="{{editForm.username}}"
|
||||
bindinput="onNameInput"
|
||||
maxlength="32"
|
||||
placeholder="请输入昵称"
|
||||
placeholder-class="input-ph"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<view class="form-field" bindtap="onSexSelect">
|
||||
<text class="form-label">性别</text>
|
||||
<view class="form-value" bindtap="onSexSelect">
|
||||
<text class="{{editForm.sex ? '' : 'placeholder'}}">{{editForm.sex === 1 ? '男' : editForm.sex === 2 ? '女' : '请选择'}}</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
<view class="form-value">
|
||||
<text class="{{editForm.sex ? '' : 'input-ph'}}">{{editForm.sex === 1 ? '男' : editForm.sex === 2 ? '女' : '请选择'}}</text>
|
||||
<view class="icon icon-chevron-right form-arrow"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-field">
|
||||
<text class="form-label">生日</text>
|
||||
<picker mode="date" value="{{birthDateStr}}" end="{{today}}" bindchange="onBirthChange">
|
||||
<view class="form-value">
|
||||
<text class="{{birthDateStr ? '' : 'placeholder'}}">{{birthDateStr || '请选择生日'}}</text>
|
||||
<view class="icon icon-chevron-right menu-arrow"></view>
|
||||
<text class="{{birthDateStr ? '' : 'input-ph'}}">{{birthDateStr || '请选择生日'}}</text>
|
||||
<view class="icon icon-chevron-right form-arrow"></view>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="btn btn-primary sheet-btn {{avatarUploading ? 'btn-disabled' : ''}}" bindtap="onSaveProfile">{{avatarUploading ? '上传中…' : '保存'}}</view>
|
||||
<view class="sheet-foot">
|
||||
<view
|
||||
class="btn btn--ink btn--block {{avatarUploading || saving ? 'is-off' : ''}}"
|
||||
bindtap="onSaveProfile"
|
||||
>{{avatarUploading ? '上传中…' : (saving ? '保存中…' : '保存')}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 性别选择:底部动作表 -->
|
||||
<v2-overlay
|
||||
visible="{{sexVisible}}"
|
||||
type="actionsheet"
|
||||
items="{{sexItems}}"
|
||||
bind:select="onSexPick"
|
||||
bind:cancel="onSexCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 我的:大号头像 + 衬线昵称 + 四栏额度条 + 纯发丝线分组列表 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,198 +11,145 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.quota-banner {
|
||||
margin: 24rpx 24rpx 0;
|
||||
background: #FFF3E6;
|
||||
color: #FF9500;
|
||||
font-size: 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.user-card {
|
||||
margin: 24rpx;
|
||||
.pf-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
padding: 48rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
.pf-av {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
background: var(--paper-3);
|
||||
color: var(--ink-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #C7C7CC;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
.pf-av .icon {
|
||||
width: 62rpx;
|
||||
height: 62rpx;
|
||||
}
|
||||
|
||||
.pf-av-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
margin-left: 24rpx;
|
||||
.pf-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.sex-tag {
|
||||
font-size: 20rpx;
|
||||
color: #007AFF;
|
||||
background: rgba(0, 122, 255, 0.1);
|
||||
border-radius: 8rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
margin-left: 12rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.user-tags {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.edit-link .icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.menu-card {
|
||||
margin: 0 24rpx 24rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.menu-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
color: #007AFF;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
font-size: 26rpx;
|
||||
color: #8E8E93;
|
||||
.pf-name {
|
||||
font-family: var(--serif);
|
||||
font-size: 46rpx;
|
||||
font-weight: 500;
|
||||
letter-spacing: -.005em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 10rpx;
|
||||
border-radius: 16rpx;
|
||||
background: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
.pf-sub {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.pf-edit {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rpx;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-3);
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
color: #C7C7CC;
|
||||
margin-left: 12rpx;
|
||||
.pf-edit .icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
|
||||
.version {
|
||||
/* 额度提醒:浅填充提示条,不依赖彩色 */
|
||||
.pf-warn {
|
||||
margin: 44rpx var(--pad) 0;
|
||||
padding: 26rpx 30rpx;
|
||||
border-radius: var(--r-ctrl);
|
||||
background: var(--fill);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
font-size: 25rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.pf-warn .icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
flex-shrink: 0;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
.pf-warn-t {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pf-quota {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.pf-quota-hd {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 0 var(--pad) 26rpx;
|
||||
}
|
||||
|
||||
.pf-quota-hd .qk {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pf-quota-hd .icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
color: var(--ink-4);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pf-grp {
|
||||
margin-top: 68rpx;
|
||||
}
|
||||
|
||||
.pf-ver {
|
||||
padding: 52rpx 0 12rpx;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
padding: 24rpx 0 48rpx;
|
||||
}
|
||||
|
||||
/* 编辑弹窗 */
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
padding: 20rpx 32rpx;
|
||||
padding-bottom: calc(32rpx + env(safe-area-inset-bottom) + 120rpx);
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sheet-grabber {
|
||||
width: 72rpx;
|
||||
height: 10rpx;
|
||||
border-radius: 5rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 auto 24rpx;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
margin-bottom: 24rpx;
|
||||
color: var(--ink-4);
|
||||
letter-spacing: .06em;
|
||||
}
|
||||
|
||||
/* ===== 编辑资料抽屉 ===== */
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
margin: 0 auto;
|
||||
margin: 16rpx auto 0;
|
||||
border-radius: 50%;
|
||||
background: #F2F2F7;
|
||||
background: var(--paper-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
color: #C7C7CC;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
|
||||
.avatar-wrap .avatar-img {
|
||||
@@ -210,8 +158,8 @@
|
||||
}
|
||||
|
||||
.avatar-wrap .avatar-placeholder {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
|
||||
.avatar-btn {
|
||||
@@ -233,28 +181,30 @@
|
||||
.avatar-tip {
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
color: var(--ink-3);
|
||||
margin: 16rpx 0 24rpx;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #F2F2F7;
|
||||
padding: 24rpx var(--pad);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 140rpx;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
color: var(--ink-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-field .input {
|
||||
.form-input {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.form-value {
|
||||
@@ -262,20 +212,11 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 28rpx;
|
||||
color: #000;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.sheet-btn {
|
||||
margin-top: 32rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
.form-arrow {
|
||||
width: 26rpx;
|
||||
height: 26rpx;
|
||||
color: var(--ink-4);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar"
|
||||
"navigation-bar": "/components/navigation-bar/navigation-bar",
|
||||
"v2-overlay": "/components/v2-overlay/index"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { request, uploadFile, NOT_LOGIN_CODE } from '../../utils/request'
|
||||
import { ProcessStatus } from '../../utils/config'
|
||||
import { navPageScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -12,17 +13,42 @@ Page({
|
||||
clothesUuid: '',
|
||||
failReason: '',
|
||||
step: 0,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
// v2 玻璃弹层
|
||||
srcVisible: false,
|
||||
srcItems: [
|
||||
{ text: '拍照', value: 'camera' },
|
||||
{ text: '从相册选择', value: 'album' },
|
||||
],
|
||||
doneVisible: false,
|
||||
doneButtons: [
|
||||
{ text: '回到衣橱', value: 'back' },
|
||||
{ text: '继续上传', value: 'again', type: 'main' },
|
||||
],
|
||||
quotaVisible: false,
|
||||
quotaButtons: [
|
||||
{ text: '取消', value: '' },
|
||||
{ text: '去购买', value: 'ok', type: 'main' },
|
||||
],
|
||||
},
|
||||
|
||||
// 页面级滚动 → 导航收缩为玻璃小标题
|
||||
onPageScroll: navPageScroll,
|
||||
|
||||
onChoose() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['拍照', '从相册选择'],
|
||||
success: (res) => {
|
||||
const sourceType: Array<'camera' | 'album'> = res.tapIndex === 0 ? ['camera'] : ['album']
|
||||
this.chooseImage(sourceType)
|
||||
this.setData({ srcVisible: true })
|
||||
},
|
||||
fail: (e) => logger.debug(LOG, '取消选择图片, err=', e && e.errMsg),
|
||||
})
|
||||
|
||||
onSourceCancel() {
|
||||
this.setData({ srcVisible: false })
|
||||
},
|
||||
|
||||
onSourceSelect(e: WechatMiniprogram.CustomEvent) {
|
||||
const value = e.detail.value as string
|
||||
this.setData({ srcVisible: false })
|
||||
if (value !== 'camera' && value !== 'album') return
|
||||
this.chooseImage([value])
|
||||
},
|
||||
|
||||
chooseImage(sourceType: Array<'camera' | 'album'>) {
|
||||
@@ -66,17 +92,13 @@ Page({
|
||||
// 后台继续轮询,仅用于更新步骤进度(不再自动跳详情)
|
||||
this.pollStatus(this.data.clothesUuid)
|
||||
// 立即弹窗让用户选择下一步,避免干等
|
||||
wx.showModal({
|
||||
title: '衣物已上传',
|
||||
content: 'AI 正在识别中,可在衣橱查看进度',
|
||||
confirmText: '继续上传',
|
||||
cancelText: '回到衣橱',
|
||||
success: (res) => {
|
||||
this.setData({ doneVisible: true })
|
||||
},
|
||||
|
||||
onDoneCancel() {
|
||||
// 回到衣橱
|
||||
this.setData({ doneVisible: false })
|
||||
this.clearTimer()
|
||||
if (res.confirm) {
|
||||
logger.debug(LOG, '选择继续上传,重置状态')
|
||||
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' })
|
||||
} else {
|
||||
wx.navigateBack({
|
||||
delta: 1,
|
||||
fail: (e) => {
|
||||
@@ -87,10 +109,17 @@ Page({
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '上传完成弹窗失败, err=', e && e.errMsg),
|
||||
})
|
||||
|
||||
onDoneConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ doneVisible: false })
|
||||
this.clearTimer()
|
||||
if (e.detail.value === 'again') {
|
||||
logger.debug(LOG, '选择继续上传,重置状态')
|
||||
this.setData({ state: 'idle', imagePath: '', clothesUuid: '', step: 0, failReason: '' })
|
||||
return
|
||||
}
|
||||
this.onDoneCancel()
|
||||
},
|
||||
|
||||
pollStatus(uuid: string) {
|
||||
@@ -139,20 +168,19 @@ Page({
|
||||
},
|
||||
|
||||
showQuotaModal() {
|
||||
wx.showModal({
|
||||
title: '上传额度不足',
|
||||
content: '当前剩余 0 次,购买额度包后可继续上传',
|
||||
confirmText: '去购买',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.setData({ quotaVisible: true })
|
||||
},
|
||||
|
||||
onQuotaCancel() {
|
||||
this.setData({ quotaVisible: false })
|
||||
},
|
||||
|
||||
onQuotaConfirm(e: WechatMiniprogram.CustomEvent) {
|
||||
this.setData({ quotaVisible: false })
|
||||
if (e.detail.value !== 'ok') return
|
||||
wx.navigateTo({
|
||||
url: '/pages/mall/mall',
|
||||
fail: (e) => logger.warn(LOG, 'navigateTo 商城失败, err=', e && e.errMsg),
|
||||
})
|
||||
}
|
||||
},
|
||||
fail: (e) => logger.warn(LOG, '额度弹窗失败, err=', e && e.errMsg),
|
||||
fail: (err) => logger.warn(LOG, 'navigateTo 商城失败, err=', err && err.errMsg),
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -1,48 +1,93 @@
|
||||
<view class="page upload-page">
|
||||
<navigation-bar title="上传衣物" />
|
||||
<view class="page up-page">
|
||||
<navigation-bar title="上传衣物" scrolled="{{navScrolled}}" />
|
||||
|
||||
<!-- 初始态 -->
|
||||
<view wx:if="{{state === 'idle'}}" class="idle">
|
||||
<view class="upload-zone" bindtap="onChoose">
|
||||
<view class="icon icon-camera upload-camera"></view>
|
||||
<view class="upload-title">拍照或从相册选择</view>
|
||||
<view class="upload-desc">支持 JPG / PNG,单张不超过 20MB</view>
|
||||
<view class="upload-desc">上传消耗 1 次上传额度</view>
|
||||
</view>
|
||||
<view class="upload-tip">AI 将自动抠图并识别 15 项属性</view>
|
||||
<!-- 初始态:巨大虚线发丝线方框 + 细描边相机 -->
|
||||
<view wx:if="{{state === 'idle'}}">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Upload</view>
|
||||
<view class="display">上传衣物</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理中 -->
|
||||
<view wx:elif="{{state === 'processing'}}" class="processing">
|
||||
<image class="proc-img" src="{{imagePath}}" mode="aspectFit" />
|
||||
<view class="steps">
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 0 ? 'active' : ''}}"></view>
|
||||
<text class="{{step >= 0 ? 'active-text' : ''}}">排队中</text>
|
||||
</view>
|
||||
<view class="step-line {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 1 ? 'active' : ''}}"></view>
|
||||
<text class="{{step >= 1 ? 'active-text' : ''}}">AI 抠图</text>
|
||||
</view>
|
||||
<view class="step-line {{step >= 2 ? 'active' : ''}}"></view>
|
||||
<view class="step">
|
||||
<view class="step-dot {{step >= 2 ? 'active' : ''}}"></view>
|
||||
<text class="{{step >= 2 ? 'active-text' : ''}}">智能分类</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="proc-text">AI 正在识别你的衣物…</view>
|
||||
<view class="proc-desc">通常需要 10 秒左右,请稍候</view>
|
||||
<view class="up-drop" hover-class="up-drop-press" bindtap="onChoose">
|
||||
<view class="icon icon-camera up-drop-ic"></view>
|
||||
<view class="up-drop-t">拍照或从相册选择</view>
|
||||
<view class="up-drop-d">支持 JPG / PNG,单张不超过 20MB</view>
|
||||
<view class="up-drop-d">上传消耗 1 次上传额度</view>
|
||||
</view>
|
||||
|
||||
<!-- 失败态 -->
|
||||
<view wx:else class="failed">
|
||||
<view class="fail-icon">!</view>
|
||||
<view class="fail-title">识别失败:{{failReason}}</view>
|
||||
<view class="fail-desc">本次上传额度已自动退回</view>
|
||||
<view class="fail-btns">
|
||||
<view class="btn btn-primary" bindtap="onRetry">重新上传</view>
|
||||
<view class="btn btn-outline" bindtap="onManualEdit">手动填写信息</view>
|
||||
<view class="up-note">AI 将自动抠图并识别 15 项属性</view>
|
||||
</view>
|
||||
|
||||
<!-- 处理中:实际图片 + 三段发丝线进度轨(排队中 / AI 抠图 / 智能分类) -->
|
||||
<view wx:elif="{{state === 'processing'}}">
|
||||
<view class="head">
|
||||
<view class="eyebrow">Processing</view>
|
||||
<view class="display">AI 正在识别你的衣物…</view>
|
||||
<view class="up-note-left">通常需要 10 秒左右,请稍候</view>
|
||||
</view>
|
||||
|
||||
<view class="up-preview">
|
||||
<image wx:if="{{imagePath}}" class="pic" src="{{imagePath}}" mode="aspectFit" />
|
||||
<view wx:else class="ph icon icon-hanger"></view>
|
||||
</view>
|
||||
|
||||
<view class="up-steps">
|
||||
<view class="up-step {{step > 0 ? 'is-done' : ''}} {{step === 0 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">排队中</text>
|
||||
</view>
|
||||
<view class="up-step {{step > 1 ? 'is-done' : ''}} {{step === 1 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">AI 抠图</text>
|
||||
</view>
|
||||
<view class="up-step {{step === 2 ? 'is-on' : ''}}">
|
||||
<view class="up-dot"></view>
|
||||
<text class="up-lb">智能分类</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 失败态:极简「!」+ 衬线标题 + 双按钮 -->
|
||||
<view wx:else class="up-failed">
|
||||
<view class="up-bang">!</view>
|
||||
<view class="head">
|
||||
<view class="display">识别失败</view>
|
||||
<view class="up-note-left">{{failReason || '未知原因'}} · 本次上传额度已自动退回</view>
|
||||
</view>
|
||||
<view class="up-duo">
|
||||
<view class="btn btn--ghost" bindtap="onRetry">重新上传</view>
|
||||
<view class="btn btn--ink" bindtap="onManualEdit">手动填写信息</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片来源选择 -->
|
||||
<v2-overlay
|
||||
visible="{{srcVisible}}"
|
||||
type="actionsheet"
|
||||
items="{{srcItems}}"
|
||||
bind:select="onSourceSelect"
|
||||
bind:cancel="onSourceCancel"
|
||||
/>
|
||||
|
||||
<!-- 上传完成 -->
|
||||
<v2-overlay
|
||||
visible="{{doneVisible}}"
|
||||
type="modal"
|
||||
title="衣物已上传"
|
||||
content="AI 正在识别中,可在衣橱查看进度"
|
||||
buttons="{{doneButtons}}"
|
||||
bind:confirm="onDoneConfirm"
|
||||
bind:cancel="onDoneCancel"
|
||||
/>
|
||||
|
||||
<!-- 额度不足 -->
|
||||
<v2-overlay
|
||||
visible="{{quotaVisible}}"
|
||||
type="modal"
|
||||
title="上传额度不足"
|
||||
content="当前剩余 0 次,购买额度包后可继续上传"
|
||||
buttons="{{quotaButtons}}"
|
||||
bind:confirm="onQuotaConfirm"
|
||||
bind:cancel="onQuotaCancel"
|
||||
/>
|
||||
</view>
|
||||
|
||||
@@ -1,156 +1,68 @@
|
||||
.upload-page {
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
/** 上传衣物:虚线投放框 / 处理中三步进度轨 / 失败双按钮 **/
|
||||
.up-page {
|
||||
min-height: 100vh;
|
||||
padding-bottom: calc(60rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 初始态 */
|
||||
.upload-zone {
|
||||
margin-top: 40rpx;
|
||||
height: 520rpx;
|
||||
border: 3rpx dashed #C7C7CC;
|
||||
border-radius: 32rpx;
|
||||
background: #fff;
|
||||
.up-drop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: calc(100% - var(--pad) * 2);
|
||||
margin: 56rpx var(--pad) 0;
|
||||
padding: 108rpx 48rpx;
|
||||
border: 1rpx dashed var(--hairline-2);
|
||||
border-radius: var(--r-img);
|
||||
transition: background .2s var(--ease), border-color .2s var(--ease);
|
||||
}
|
||||
|
||||
.upload-camera {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
color: #007AFF;
|
||||
margin-bottom: 24rpx;
|
||||
.up-drop-press {
|
||||
background: var(--fill);
|
||||
border-color: var(--ink-4);
|
||||
}
|
||||
|
||||
.upload-title {
|
||||
font-size: 30rpx;
|
||||
color: #000;
|
||||
.up-drop-ic {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
color: var(--ink-3);
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
|
||||
.up-drop-t {
|
||||
font-family: var(--serif);
|
||||
font-size: 34rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.upload-desc {
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
.up-drop-d {
|
||||
margin-top: 18rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
.upload-tip {
|
||||
.up-note {
|
||||
margin-top: 44rpx;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 24rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
|
||||
/* 处理中 */
|
||||
.processing {
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.proc-img {
|
||||
width: 480rpx;
|
||||
height: 480rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #F2F2F7;
|
||||
}
|
||||
|
||||
.steps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 56rpx;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 22rpx;
|
||||
color: #C7C7CC;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #E5E5EA;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.step-dot.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.active-text {
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 100rpx;
|
||||
height: 2rpx;
|
||||
background: #E5E5EA;
|
||||
margin: 0 16rpx 34rpx;
|
||||
}
|
||||
|
||||
.step-line.active {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.proc-text {
|
||||
margin-top: 56rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.proc-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 12rpx;
|
||||
.up-note-left {
|
||||
margin-top: 20rpx;
|
||||
font-size: 25rpx;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* 失败态 */
|
||||
.failed {
|
||||
margin-top: 120rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 40rpx;
|
||||
.up-failed {
|
||||
padding-top: 40rpx;
|
||||
}
|
||||
|
||||
.fail-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 72rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.fail-title {
|
||||
margin-top: 40rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fail-desc {
|
||||
font-size: 24rpx;
|
||||
color: #8E8E93;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.fail-btns {
|
||||
width: 100%;
|
||||
margin-top: 56rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
.up-failed .up-bang {
|
||||
margin-left: var(--pad);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PagedResult, ClothesItem, QuotaItem } from '../../types/models'
|
||||
import { QuotaType } from '../../utils/config'
|
||||
import { normalizePaged, mergePaged, hasMorePaged } from '../../utils/paged'
|
||||
import { setTabBarSelected } from '../../utils/tabbar'
|
||||
import { navScroll } from '../../utils/nav'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
// 日志模块名
|
||||
@@ -32,8 +33,13 @@ Page({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
hasMore: true,
|
||||
// 滚动收缩:由 utils/nav.ts 的共享 handler 驱动
|
||||
navScrolled: false,
|
||||
},
|
||||
|
||||
// scroll-view 滚动 → 导航收缩为玻璃小标题
|
||||
onNavScroll: navScroll,
|
||||
|
||||
onShow() {
|
||||
this.syncTabBar()
|
||||
// 未登录直接回登录页(幂等跳转),不再发出必然 401 的请求
|
||||
@@ -93,7 +99,7 @@ Page({
|
||||
const list = raw.map((it) => ({
|
||||
...it,
|
||||
_statusText: !it.category_main && !it.processed_image ? 'AI 处理中' : '',
|
||||
_statusType: 'blue',
|
||||
_statusType: 'plain',
|
||||
}))
|
||||
const merged = mergePaged(this.data.list, list, page === 1)
|
||||
this.setData({
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
<view class="page page-flex">
|
||||
<navigation-bar title="我的衣橱" back="{{false}}" />
|
||||
<navigation-bar title="我的衣橱" back="{{false}}" scrolled="{{navScrolled}}" />
|
||||
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="scroll-body"
|
||||
class="scroll-body pb-tab"
|
||||
refresher-enabled
|
||||
refresher-triggered="{{refreshing}}"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindscrolltolower="onReachBottom"
|
||||
bindscroll="onNavScroll"
|
||||
>
|
||||
<!-- 分类筛选 -->
|
||||
<scroll-view scroll-x class="cat-scroll" show-scrollbar="{{false}}">
|
||||
<view class="cat-row">
|
||||
<!-- 大标题页头 -->
|
||||
<view class="head">
|
||||
<view class="eyebrow">Wardrobe</view>
|
||||
<view class="display">我的衣橱</view>
|
||||
<view class="wd-stat num">共 {{total}} 件 · 上传额度剩余 {{uploadRemain}} 次</view>
|
||||
</view>
|
||||
|
||||
<!-- 下划线式极简分类条 -->
|
||||
<view class="segbar">
|
||||
<scroll-view scroll-x class="seg-scroll" show-scrollbar="{{false}}">
|
||||
<view class="seg">
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="name"
|
||||
class="cat-item {{activeCategory === index ? 'active' : ''}}"
|
||||
class="seg-item {{activeCategory === index ? 'is-on' : ''}}"
|
||||
data-index="{{index}}"
|
||||
bindtap="onCategoryTap"
|
||||
>{{item.name}}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 统计 -->
|
||||
<view class="stat">共 {{total}} 件 · 上传额度剩余 {{uploadRemain}} 次</view>
|
||||
</view>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view wx:if="{{loading}}" class="grid">
|
||||
<view class="grid-item" wx:for="{{[1, 2, 3, 4, 5, 6]}}" wx:key="*this">
|
||||
<view class="skeleton sk-card"></view>
|
||||
<view class="skeleton sk-line"></view>
|
||||
<view wx:if="{{loading}}" class="wd-grid">
|
||||
<view class="wd-skel" wx:for="{{[1, 2, 3, 4, 5, 6]}}" wx:key="*this">
|
||||
<view class="sk sk-img"></view>
|
||||
<view class="sk sk-ln" style="width: 70%;"></view>
|
||||
<view class="sk sk-ln" style="width: 46%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -44,8 +52,8 @@
|
||||
/>
|
||||
|
||||
<!-- 衣物网格 -->
|
||||
<view wx:else class="grid">
|
||||
<view class="grid-item" wx:for="{{list}}" wx:key="clothes_uuid">
|
||||
<view wx:else class="wd-grid">
|
||||
<view class="wd-tile" wx:for="{{list}}" wx:key="clothes_uuid">
|
||||
<clothes-card
|
||||
item="{{item}}"
|
||||
statusText="{{item._statusText}}"
|
||||
@@ -55,7 +63,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{list.length && !hasMore}}" class="no-more">没有更多了</view>
|
||||
<view wx:if="{{list.length && !hasMore}}" class="foot-note">没有更多了</view>
|
||||
<view class="gap-64"></view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="fab" bindtap="onUpload">
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** 衣橱:衬线大标题 + 下划线分类条 + 无容器网格 **/
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
@@ -10,65 +11,44 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.cat-scroll {
|
||||
background: #fff;
|
||||
white-space: nowrap;
|
||||
.wd-stat {
|
||||
margin-top: 22rpx;
|
||||
font-size: 23rpx;
|
||||
color: var(--ink-3);
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
|
||||
.cat-row {
|
||||
display: inline-flex;
|
||||
padding: 20rpx 24rpx;
|
||||
/* 分类条:吸顶玻璃 + 下划线选中态 */
|
||||
.segbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
margin-top: 36rpx;
|
||||
background: var(--glass);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1rpx solid var(--hairline);
|
||||
}
|
||||
|
||||
.cat-item {
|
||||
flex-shrink: 0;
|
||||
padding: 0 32rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: #3C3C43;
|
||||
background: #F2F2F7;
|
||||
margin-right: 16rpx;
|
||||
.wd-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 44rpx 26rpx;
|
||||
padding: 40rpx var(--pad) 0;
|
||||
}
|
||||
|
||||
.cat-item.active {
|
||||
background: #007AFF;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 8rpx 32rpx 0;
|
||||
font-size: 22rpx;
|
||||
color: #8E8E93;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.wd-skel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx;
|
||||
gap: 24rpx;
|
||||
flex-direction: column;
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: calc(50% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
.wd-tile {
|
||||
display: block;
|
||||
width: 100%;
|
||||
transition: transform .2s var(--ease);
|
||||
}
|
||||
|
||||
.sk-card {
|
||||
height: 320rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.sk-line {
|
||||
height: 40rpx;
|
||||
margin-top: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #C7C7CC;
|
||||
padding: 24rpx 0 40rpx;
|
||||
.wd-tile:active {
|
||||
transform: scale(.975);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** SF Symbols 风格线性图标库(CSS mask + currentColor 单色可换色)**/
|
||||
/** 线性图标库(CSS mask + currentColor 单色可换色)· v2 统一 1.5px 细描边 **/
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
@@ -14,68 +14,94 @@
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
.icon-sm { width: 40rpx; height: 40rpx; }
|
||||
.icon-xs { width: 28rpx; height: 28rpx; }
|
||||
.icon-sm { width: 36rpx; height: 36rpx; }
|
||||
.icon-md { width: 56rpx; height: 56rpx; }
|
||||
.icon-lg { width: 72rpx; height: 72rpx; }
|
||||
.icon-xl { width: 104rpx; height: 104rpx; }
|
||||
|
||||
/* 衣橱 / 衣架 */
|
||||
.icon-hanger {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v3'/%3E%3Cpath d='M12 6c-2.5 0-4.5 2-4.5 4.5L4 20h16l-3.5-9.5C16.5 8 14.5 6 12 6z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v3'/%3E%3Cpath d='M12 6c-2.5 0-4.5 2-4.5 4.5L4 20h16l-3.5-9.5C16.5 8 14.5 6 12 6z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v3'/%3E%3Cpath d='M12 6c-2.5 0-4.5 2-4.5 4.5L4 20h16l-3.5-9.5C16.5 8 14.5 6 12 6z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v3'/%3E%3Cpath d='M12 6c-2.5 0-4.5 2-4.5 4.5L4 20h16l-3.5-9.5C16.5 8 14.5 6 12 6z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 穿搭 / T恤 */
|
||||
.icon-shirt {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 3l4 4-2 2-1-1v3l2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6l2-2V8L6 9 4 7l4-4h2a2 2 0 0 0 4 0h2z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 3l4 4-2 2-1-1v3l2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6l2-2V8L6 9 4 7l4-4h2a2 2 0 0 0 4 0h2z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 3l4 4-2 2-1-1v3l2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6l2-2V8L6 9 4 7l4-4h2a2 2 0 0 0 4 0h2z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 3l4 4-2 2-1-1v3l2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6l2-2V8L6 9 4 7l4-4h2a2 2 0 0 0 4 0h2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 运势 / 四角星 */
|
||||
.icon-sparkles {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9L12 3z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 通知 / 铃铛 */
|
||||
.icon-bell {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9'/%3E%3Cpath d='M13.73 21a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 我的 / 人形 */
|
||||
.icon-user {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 加号 */
|
||||
.icon-plus {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14'/%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 减号 */
|
||||
.icon-minus {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 右箭头 */
|
||||
.icon-chevron-right {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 18l6-6-6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 左箭头(返回) */
|
||||
.icon-chevron-left {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 下箭头 */
|
||||
.icon-chevron-down {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 相机 */
|
||||
.icon-camera {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'/%3E%3Ccircle cx='12' cy='13' r='4'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'/%3E%3Ccircle cx='12' cy='13' r='4'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'/%3E%3Ccircle cx='12' cy='13' r='4'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z'/%3E%3Ccircle cx='12' cy='13' r='4'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 图片 */
|
||||
.icon-image {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='M21 15l-5-5L5 21'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.5'/%3E%3Cpath d='M21 15l-5-5L5 21'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 点赞 / 爱心 */
|
||||
.icon-heart {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 收藏 / 星(线框) */
|
||||
.icon-star {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 收藏 / 星(实心) */
|
||||
@@ -86,78 +112,102 @@
|
||||
|
||||
/* 分享 */
|
||||
.icon-share {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cpath d='M8.59 13.51l6.83 3.98'/%3E%3Cpath d='M15.41 6.51l-6.82 3.98'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cpath d='M8.59 13.51l6.83 3.98'/%3E%3Cpath d='M15.41 6.51l-6.82 3.98'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cpath d='M8.59 13.51l6.83 3.98'/%3E%3Cpath d='M15.41 6.51l-6.82 3.98'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'/%3E%3Ccircle cx='6' cy='12' r='3'/%3E%3Ccircle cx='18' cy='19' r='3'/%3E%3Cpath d='M8.59 13.51l6.83 3.98'/%3E%3Cpath d='M15.41 6.51l-6.82 3.98'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 外链 / 右上箭头 */
|
||||
.icon-arrow-up-right {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7'/%3E%3Cpath d='M8 7h9v9'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7'/%3E%3Cpath d='M8 7h9v9'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 删除 / 垃圾桶 */
|
||||
.icon-trash {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18'/%3E%3Cpath d='M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6'/%3E%3Cpath d='M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 编辑 / 铅笔 */
|
||||
.icon-pencil {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 会员 / 皇冠 */
|
||||
.icon-crown {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8l4 4 5-6 5 6 4-4-2 9H5L3 8z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8l4 4 5-6 5 6 4-4-2 9H5L3 8z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8l4 4 5-6 5 6 4-4-2 9H5L3 8z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8l4 4 5-6 5 6 4-4-2 9H5L3 8z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 订单 / 文档 */
|
||||
.icon-doc {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 2v6h6'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 2v6h6'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 2v6h6'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'/%3E%3Cpath d='M14 2v6h6'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 额度 / 仪表 */
|
||||
.icon-gauge {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0'/%3E%3Cpath d='M12 12l5-3'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0'/%3E%3Cpath d='M12 12l5-3'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0'/%3E%3Cpath d='M12 12l5-3'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0-18 0'/%3E%3Cpath d='M12 12l5-3'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 对勾 */
|
||||
.icon-check {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6L9 17l-5-5'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 叉 */
|
||||
.icon-close {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'/%3E%3Cpath d='M6 6l12 12'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'/%3E%3Cpath d='M6 6l12 12'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'/%3E%3Cpath d='M6 6l12 12'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6L6 18'/%3E%3Cpath d='M6 6l12 12'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 更多 / 三点 */
|
||||
.icon-ellipsis {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='12' cy='5' r='2'/%3E%3Ccircle cx='12' cy='12' r='2'/%3E%3Ccircle cx='12' cy='19' r='2'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='12' cy='5' r='2'/%3E%3Ccircle cx='12' cy='12' r='2'/%3E%3Ccircle cx='12' cy='19' r='2'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='12' cy='5' r='1.6'/%3E%3Ccircle cx='12' cy='12' r='1.6'/%3E%3Ccircle cx='12' cy='19' r='1.6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='black'%3E%3Ccircle cx='12' cy='5' r='1.6'/%3E%3Ccircle cx='12' cy='12' r='1.6'/%3E%3Ccircle cx='12' cy='19' r='1.6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 生日 / 日历 */
|
||||
.icon-calendar {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4'/%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4'/%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4'/%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4'/%3E%3Cpath d='M8 2v4'/%3E%3Cpath d='M3 10h18'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 时间 / 时钟 */
|
||||
.icon-clock {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M12 7v5l3 2'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 只读 / 锁 */
|
||||
.icon-lock {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='10' width='16' height='11' rx='2'/%3E%3Cpath d='M8 10V7a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='10' width='16' height='11' rx='2'/%3E%3Cpath d='M8 10V7a4 4 0 0 1 8 0v3'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 刷新 / 重试 */
|
||||
.icon-refresh {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-3-6.7'/%3E%3Cpath d='M21 3v6h-6'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-3-6.7'/%3E%3Cpath d='M21 3v6h-6'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 客服 / 耳机 */
|
||||
.icon-headphones {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 18v-6a9 9 0 0 1 18 0v6'/%3E%3Cpath d='M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 18v-6a9 9 0 0 1 18 0v6'/%3E%3Cpath d='M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 18v-6a9 9 0 0 1 18 0v6'/%3E%3Cpath d='M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 18v-6a9 9 0 0 1 18 0v6'/%3E%3Cpath d='M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 关于 / 信息 */
|
||||
.icon-info {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4'/%3E%3Cpath d='M12 8h.01'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* 设置 / 齿轮 */
|
||||
.icon-setting {
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E");
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@@ -48,6 +48,21 @@ export interface OutfitItem {
|
||||
items: Array<{ clothes_uuid: string; processed_image: string }>
|
||||
}
|
||||
|
||||
// 社区穿搭列表项(/outfit/community 返回)
|
||||
export interface CommunityOutfitItem {
|
||||
outfit_uuid: string
|
||||
outfit_name: string
|
||||
outfit_desc: string
|
||||
scene: string
|
||||
source: number // 1 用户创建 2 AI 推荐
|
||||
created_at: number // 秒级时间戳
|
||||
items: Array<{ clothes_uuid: string; processed_image: string }>
|
||||
owner_name: string
|
||||
owner_avatar: string
|
||||
like_count: number
|
||||
collect_count: number
|
||||
}
|
||||
|
||||
export interface GoodsItem {
|
||||
id: number
|
||||
goods_code: string
|
||||
|
||||
@@ -111,46 +111,98 @@ export const GOODS_TYPE_NAMES: Record<number, string> = {
|
||||
|
||||
// 错误码统一文案(兜底,优先展示服务端 details[0])
|
||||
export const ERROR_MESSAGES: Record<number, string> = {
|
||||
80000: '额度不足',
|
||||
80001: '额度记录不存在',
|
||||
90401: 'AI 推荐次数已达上限',
|
||||
90400: 'AI 推荐失败',
|
||||
90300: '运势生成中,请稍后重试',
|
||||
// 通用
|
||||
10000: '请求参数错误',
|
||||
10001: '资源不存在',
|
||||
10002: '请求方式错误',
|
||||
10003: '操作过于频繁,请稍后重试',
|
||||
// 鉴权 / 用户
|
||||
11000: '请先登录',
|
||||
11001: '登录状态已失效',
|
||||
11002: '登录状态已过期',
|
||||
11003: '登录失败,请重试',
|
||||
11004: '请先登录',
|
||||
12000: '无权限操作',
|
||||
20000: '用户参数错误',
|
||||
21000: '用户名或密码错误',
|
||||
21001: '账号已被禁用',
|
||||
21002: '微信登录失败,请重试',
|
||||
21003: '微信登录凭证无效',
|
||||
21004: '注册暂未开放',
|
||||
22000: '用户不存在',
|
||||
23000: '用户已存在',
|
||||
23001: '邮箱已被注册',
|
||||
// 上传
|
||||
30000: '上传文件为空',
|
||||
30001: '不支持的文件类型',
|
||||
30002: '文件大小超过限制',
|
||||
30004: '文件不存在',
|
||||
30005: '文件不是公开的',
|
||||
30006: '文件权限不足',
|
||||
30007: '文件处理中',
|
||||
39000: '文件保存失败',
|
||||
39001: '图片抠图处理失败',
|
||||
39002: '图片分类处理失败',
|
||||
// 穿搭
|
||||
40000: '穿搭不存在',
|
||||
40001: '创建穿搭失败',
|
||||
40002: '删除穿搭失败',
|
||||
40003: '更新穿搭失败',
|
||||
40004: '无权操作该穿搭',
|
||||
// 衣物
|
||||
50000: '衣物不存在',
|
||||
50001: '该衣物已被搭配使用,无法删除',
|
||||
50002: '创建衣物失败',
|
||||
50003: '删除衣物失败',
|
||||
50004: '更新衣物失败',
|
||||
50005: '无权操作该衣物',
|
||||
40000: '穿搭不存在',
|
||||
40004: '无权操作该穿搭',
|
||||
// 商品
|
||||
60000: '商品不存在',
|
||||
60001: '商品已下架',
|
||||
// 订单 / 退款
|
||||
70000: '订单不存在',
|
||||
70001: '订单状态不正确',
|
||||
70002: '订单已支付',
|
||||
70003: '订单已取消',
|
||||
70004: '订单已过期',
|
||||
70005: '虚拟支付未开通',
|
||||
70006: '商品尚未发布为虚拟支付道具',
|
||||
70007: '虚拟支付订单不支持取消',
|
||||
70008: 'iOS 虚拟支付最低 1 元',
|
||||
70009: '订单真实性校验失败',
|
||||
71000: '退款记录不存在',
|
||||
71001: '当前状态不允许退款',
|
||||
71002: '退款金额不合法',
|
||||
71003: '退款申请失败',
|
||||
71004: 'iOS 订单请通过 App Store 申请退款',
|
||||
79000: '创建订单失败',
|
||||
79001: '取消订单失败',
|
||||
79002: '更新订单失败',
|
||||
79003: '创建虚拟支付订单失败',
|
||||
// 额度 / 会员
|
||||
80000: '额度不足',
|
||||
80001: '额度记录不存在',
|
||||
80002: '广告激励已领取',
|
||||
80003: '无效的激励类型',
|
||||
80004: '广告激励未开启',
|
||||
81000: '会员记录不存在',
|
||||
81001: '会员已过期',
|
||||
81002: '会员开通失败',
|
||||
89000: '额度更新失败',
|
||||
// 通知 / 分享 / 社区 / 星座 / AI
|
||||
90000: '通知不存在',
|
||||
90100: '分享不存在',
|
||||
90101: '分享已过期',
|
||||
90102: '分享已取消',
|
||||
90103: '穿搭分享不存在',
|
||||
90104: '穿搭分享已取消',
|
||||
90200: '目标内容不存在',
|
||||
90201: '点赞操作失败',
|
||||
90202: '收藏操作失败',
|
||||
90300: '运势生成中,请稍后重试',
|
||||
90400: 'AI 推荐失败',
|
||||
90401: 'AI 推荐次数已达上限',
|
||||
// 服务端
|
||||
19000: '服务繁忙,请稍后重试',
|
||||
19001: '服务繁忙,请稍后重试',
|
||||
19002: '服务繁忙,请稍后重试',
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { logger } from './logger'
|
||||
|
||||
/**
|
||||
* 导航滚动收缩共享 handler。
|
||||
*
|
||||
* v2 导航形态:进入页面是内容区的衬线大标题,向下滚动后收缩为居中玻璃小标题。
|
||||
* 页面侧只接入一行(scroll-view 用 bindscroll,页面级滚动用 onPageScroll):
|
||||
* Page({ onNavScroll: navScroll }) // <scroll-view bindscroll="onNavScroll">
|
||||
* Page({ onPageScroll: navPageScroll }) // 页面级滚动
|
||||
*
|
||||
* 性能:只在跨越阈值时 setData 一次,绝不逐帧更新。
|
||||
*/
|
||||
|
||||
// 日志模块名
|
||||
const LOG = 'nav'
|
||||
|
||||
// 滚动超过该距离(px)即切换为玻璃小标题态
|
||||
export const NAV_THRESHOLD = 24
|
||||
|
||||
function apply(ctx: any, scrollTop: number): void {
|
||||
if (!ctx || !ctx.data) return
|
||||
const next = scrollTop > NAV_THRESHOLD
|
||||
if (next === !!ctx.data.navScrolled) return
|
||||
// 仅在首次进入玻璃态时打点,避免滚动过程刷屏
|
||||
if (next && !ctx.__navLogged) {
|
||||
ctx.__navLogged = true
|
||||
logger.debug(LOG, '导航进入玻璃态, scrollTop=', scrollTop)
|
||||
}
|
||||
ctx.setData({ navScrolled: next })
|
||||
}
|
||||
|
||||
/** scroll-view 的 bindscroll 处理器 */
|
||||
export function navScroll(this: any, e: { detail?: { scrollTop?: number } }): void {
|
||||
apply(this, (e && e.detail && e.detail.scrollTop) || 0)
|
||||
}
|
||||
|
||||
/** 页面级滚动的 onPageScroll 处理器 */
|
||||
export function navPageScroll(this: any, e: { scrollTop?: number }): void {
|
||||
apply(this, (e && e.scrollTop) || 0)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user