From a0e246bc6eebc8826a91896dd491982236118a62 Mon Sep 17 00:00:00 2001 From: VeloForm Bot Date: Wed, 17 Jun 2026 03:35:24 +0000 Subject: [PATCH 01/16] =?UTF-8?q?feat(prototype):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E7=B3=BB=E7=BB=9F=E8=A7=84=E8=8C=83=E4=B8=8E?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=BA=93=E8=A7=84=E8=8C=83=E8=87=B3=20v3.8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 设计系统:新增品牌渐变色 (gradient-brand)、gradient-shift 动画 及 Utility Classes 章节(含渐变工具类) - 组件库:新增 gradient 按钮变体(Hero CTA / 品牌强调场景); 新增 shadcn/ui 基础组件来源说明(L1 组件基于 shadcn/ui + Radix UI) - 交互标准:同步版本号至 v3.8.0 - 统一更新文档版本号与更新日期 --- prototype/component-library.md | 16 ++++++--- prototype/design-system-spec.md | 54 +++++++++++++++++++++++++++--- prototype/interaction-standards.md | 8 ++--- 3 files changed, 65 insertions(+), 13 deletions(-) diff --git a/prototype/component-library.md b/prototype/component-library.md index da5b82e..d3741da 100644 --- a/prototype/component-library.md +++ b/prototype/component-library.md @@ -1,8 +1,8 @@ # Veloform 组件库规范 > **路径**: `/prototype/component-library.md` -> **版本**: v3.7.0 -> **更新日期**: 2026-06-14 +> **版本**: v3.8.0 +> **更新日期**: 2026-06-17 --- @@ -21,6 +21,13 @@ > - L3 → `src/components/configurator/` > - L4 → `src/components/sections/` +### 1.1 基础组件来源 + +L1 基础组件(Button / Input / Card / Modal / Badge 等)基于 **shadcn/ui** 生成与扩展: +- 组件源码通过 `shadcn/ui` CLI 初始化,放置于 `src/components/ui/` +- 样式基于 **Tailwind CSS**,与设计系统规范中的 token 映射保持一致 +- 可访问性(键盘、focus ring、ARIA)由底层 Radix UI 提供 + --- ## 2. 基础组件规范 (L1 - Foundation) @@ -37,6 +44,7 @@ | `outline` | 透明背景,`--primary` 描边 | 次要可选操作 | 「了解更多」辅助链接 | | `ghost` | 透明背景,文字操作 | 轻量操作 | 工具栏、行内操作 | | `danger` | 填充 `--error`,白字 | 破坏性操作 | 删除、移除、取消订阅 | +| `gradient` | 渐变背景 `#0071E3 → #34C759 → #AF52DE`,白字,glow shadow | 品牌强调 | Hero CTA、特殊行动号召 | #### 尺寸 (Sizes) @@ -631,5 +639,5 @@ export const ComponentName = forwardRef( --- **文档路径**: `/prototype/component-library.md` -**最后更新**: 2026-06-14 -**版本**: v3.7.0 +**最后更新**: 2026-06-17 +**版本**: v3.8.0 diff --git a/prototype/design-system-spec.md b/prototype/design-system-spec.md index a58ec21..6a6934e 100644 --- a/prototype/design-system-spec.md +++ b/prototype/design-system-spec.md @@ -1,8 +1,8 @@ # Veloform 设计系统规范 > **路径**: `/prototype/design-system-spec.md` -> **版本**: v3.7.0 -> **更新日期**: 2026-06-14 +> **版本**: v3.8.0 +> **更新日期**: 2026-06-17 --- @@ -47,7 +47,14 @@ - **语义色 (Semantic)**:`--success / --warning / --error / --info` 仅用于状态反馈,不可做装饰用途 - **中性色 (Neutral)**:`--surface-*` 系列用于构建视觉层级,避免在单一界面混用多种表面色 -### 1.4 对比度要求 (WCAG 2.1 AA) +### 1.5 品牌渐变色 (Brand Gradient) + +| Token | CSS 值 | 用途 | +|-------|--------|------| +| `--gradient-brand` | `linear-gradient(90deg, #0071E3, #34C759, #AF52DE)` | 品牌渐变 | +| `--gradient-brand-subtle` | `linear-gradient(135deg, rgba(0,113,227,0.05), rgba(52,199,89,0.03))` | 卡片悬停背景 | + +### 1.6 对比度要求 (WCAG 2.1 AA) | 文本类型 | 最小对比度 | 示例组合 | |---------|-----------|---------| @@ -300,6 +307,7 @@ animation: { "pulse": "pulse 2s ease-in-out infinite", // 加载脉冲 "float": "float 8s ease-in-out infinite", // 缓慢浮动 (装饰元素) "glow-pulse": "glowPulse 4s ease-in-out infinite", // 光晕呼吸 + "gradient-shift": "gradientShift 6s ease-in-out infinite", // 渐变呼吸位移动画 } ``` @@ -345,6 +353,11 @@ animation: { 0%, 100% { opacity: 0.5; transform: scale(1); } 50% { opacity: 1; transform: scale(1.1); } } + +@keyframes gradientShift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} ``` ### 5.4 Framer Motion 预设 @@ -403,6 +416,37 @@ const prefersReducedMotion = --- +## 6. Utility Classes + +### 6.1 通用工具类 + +| Class | 说明 | +|-------|------| +| `container` | `max-width: 1280px; margin: 0 auto;` — 页面内容容器 | +| `sr-only` | 视觉隐藏,保留给无障碍读屏器 | +| `truncate` | 单行截断溢出文本 | +| `line-clamp-2` | 最多 2 行截断 | +| `no-scrollbar` | 隐藏滚动条但保留滚动功能 | + +### 6.2 渐变与装饰工具 + +```css +.gradient-brand { + background: linear-gradient(90deg, #0071E3, #34C759, #AF52DE); + color: #ffffff; +} + +.gradient-text { + background: linear-gradient(90deg, #0071E3, #34C759, #AF52DE); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; +} +``` + +--- + ## 相关文档 - [原型说明](./prototype-guide.md) @@ -413,5 +457,5 @@ const prefersReducedMotion = --- **文档路径**: `/prototype/design-system-spec.md` -**最后更新**: 2026-06-14 -**版本**: v3.7.0 +**最后更新**: 2026-06-17 +**版本**: v3.8.0 diff --git a/prototype/interaction-standards.md b/prototype/interaction-standards.md index c3f1b21..63d85c3 100644 --- a/prototype/interaction-standards.md +++ b/prototype/interaction-standards.md @@ -1,8 +1,8 @@ # Veloform 交互标准 > **路径**: `/prototype/interaction-standards.md` -> **版本**: v3.7.0 -> **更新日期**: 2026-06-14 +> **版本**: v3.8.0 +> **更新日期**: 2026-06-17 --- @@ -691,5 +691,5 @@ const prefersReducedMotion = --- **文档路径**: `/prototype/interaction-standards.md` -**最后更新**: 2026-06-14 -**版本**: v3.7.0 +**最后更新**: 2026-06-17 +**版本**: v3.8.0 From ee279f13146ded711dccd3a5c9ce1c7b100a9c22 Mon Sep 17 00:00:00 2001 From: VeloForm Bot Date: Wed, 17 Jun 2026 03:35:24 +0000 Subject: [PATCH 02/16] =?UTF-8?q?feat(miniprogram):=20=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=B8=8E=E6=A0=B8=E5=BF=83=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 核心配置:app.js / app.json / app.wxss / project.config.json / sitemap.json 定义 tabBar、页面路由、全局样式与小程序项目配置 - 工具模块:utils/store.js(配置状态管理 + 本地存储)、 utils/util.js(格式化价格/重量/日期、防抖节流、弹窗提示等)、 utils/constants.js(车型分类等常量) - 数据模块:data/components.js(组件目录数据) - 页面:pages/index(首页 / 车型选择)、pages/configurator(配置器)、 pages/library(配置库)、pages/detail(配置详情) - 每个页面包含 wxml / wxss / js / json 四件套,遵循小程序目录规范 --- miniprogram/app.js | 40 ++ miniprogram/app.json | 46 ++ miniprogram/app.wxss | 492 +++++++++++++++ miniprogram/data/components.js | 473 ++++++++++++++ .../pages/configurator/configurator.js | 292 +++++++++ .../pages/configurator/configurator.json | 8 + .../pages/configurator/configurator.wxml | 205 ++++++ .../pages/configurator/configurator.wxss | 586 ++++++++++++++++++ miniprogram/pages/detail/detail.js | 182 ++++++ miniprogram/pages/detail/detail.json | 8 + miniprogram/pages/detail/detail.wxml | 126 ++++ miniprogram/pages/detail/detail.wxss | 349 +++++++++++ miniprogram/pages/index/index.js | 126 ++++ miniprogram/pages/index/index.json | 8 + miniprogram/pages/index/index.wxml | 113 ++++ miniprogram/pages/index/index.wxss | 316 ++++++++++ miniprogram/pages/library/library.js | 167 +++++ miniprogram/pages/library/library.json | 8 + miniprogram/pages/library/library.wxml | 132 ++++ miniprogram/pages/library/library.wxss | 297 +++++++++ miniprogram/project.config.json | 53 ++ miniprogram/sitemap.json | 9 + miniprogram/utils/constants.js | 64 ++ miniprogram/utils/store.js | 232 +++++++ miniprogram/utils/util.js | 120 ++++ 25 files changed, 4452 insertions(+) create mode 100644 miniprogram/app.js create mode 100644 miniprogram/app.json create mode 100644 miniprogram/app.wxss create mode 100644 miniprogram/data/components.js create mode 100644 miniprogram/pages/configurator/configurator.js create mode 100644 miniprogram/pages/configurator/configurator.json create mode 100644 miniprogram/pages/configurator/configurator.wxml create mode 100644 miniprogram/pages/configurator/configurator.wxss create mode 100644 miniprogram/pages/detail/detail.js create mode 100644 miniprogram/pages/detail/detail.json create mode 100644 miniprogram/pages/detail/detail.wxml create mode 100644 miniprogram/pages/detail/detail.wxss create mode 100644 miniprogram/pages/index/index.js create mode 100644 miniprogram/pages/index/index.json create mode 100644 miniprogram/pages/index/index.wxml create mode 100644 miniprogram/pages/index/index.wxss create mode 100644 miniprogram/pages/library/library.js create mode 100644 miniprogram/pages/library/library.json create mode 100644 miniprogram/pages/library/library.wxml create mode 100644 miniprogram/pages/library/library.wxss create mode 100644 miniprogram/project.config.json create mode 100644 miniprogram/sitemap.json create mode 100644 miniprogram/utils/constants.js create mode 100644 miniprogram/utils/store.js create mode 100644 miniprogram/utils/util.js diff --git a/miniprogram/app.js b/miniprogram/app.js new file mode 100644 index 0000000..b09feb2 --- /dev/null +++ b/miniprogram/app.js @@ -0,0 +1,40 @@ +const configStore = require('./utils/store'); + +App({ + onLaunch() { + // 初始化存储数据 + configStore.init(); + + // 获取系统信息 + const systemInfo = wx.getSystemInfoSync(); + this.globalData.systemInfo = systemInfo; + + // 检查登录状态 + this.checkLoginStatus(); + }, + + onShow() { + // 页面显示时刷新配置数据 + configStore.refresh(); + }, + + globalData: { + systemInfo: null, + isLoggedIn: false, + userInfo: null, + appName: 'Veloform', + version: '3.8.0' + }, + + checkLoginStatus() { + try { + const userInfo = wx.getStorageSync('userInfo'); + if (userInfo) { + this.globalData.isLoggedIn = true; + this.globalData.userInfo = userInfo; + } + } catch (e) { + console.error('检查登录状态失败', e); + } + } +}); diff --git a/miniprogram/app.json b/miniprogram/app.json new file mode 100644 index 0000000..4e37444 --- /dev/null +++ b/miniprogram/app.json @@ -0,0 +1,46 @@ +{ + "pages": [ + "pages/index/index", + "pages/configurator/configurator", + "pages/library/library", + "pages/detail/detail" + ], + "window": { + "backgroundTextStyle": "light", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTitleText": "Veloform", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f7" + }, + "tabBar": { + "color": "#86868b", + "selectedColor": "#0071e3", + "backgroundColor": "#ffffff", + "borderStyle": "white", + "list": [ + { + "pagePath": "pages/index/index", + "text": "首页", + "iconPath": "assets/tab/home.png", + "selectedIconPath": "assets/tab/home-active.png" + }, + { + "pagePath": "pages/configurator/configurator", + "text": "配置器", + "iconPath": "assets/tab/config.png", + "selectedIconPath": "assets/tab/config-active.png" + }, + { + "pagePath": "pages/library/library", + "text": "配置库", + "iconPath": "assets/tab/library.png", + "selectedIconPath": "assets/tab/library-active.png" + } + ] + }, + "style": "v2", + "componentFramework": "glass-easel", + "sitemapLocation": "sitemap.json", + "lazyCodeLoading": "requiredComponents", + "usingComponents": {} +} diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss new file mode 100644 index 0000000..9234185 --- /dev/null +++ b/miniprogram/app.wxss @@ -0,0 +1,492 @@ +/* app.wxss - Veloform 全局样式 */ + +page { + --background: #ffffff; + --foreground: #1d1d1f; + --surface: #f5f5f7; + --surface-secondary: #ffffff; + --surface-tertiary: #e5e5ea; + --border: #d2d2d7; + --border-light: rgba(0, 0, 0, 0.08); + --muted: #6e6e73; + --secondary: #86868b; + --primary: #0071e3; + --primary-hover: #0077ed; + --accent: #34c759; + --warning: #ff9500; + --error: #ff3b30; + --info: #0071e3; + + background: var(--surface); + color: var(--foreground); + font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'SF Pro Display', + 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif; + font-size: 28rpx; + line-height: 1.5; + min-height: 100vh; +} + +/* ========== 布局容器 ========== */ +.container { + padding: 32rpx; + box-sizing: border-box; +} + +.page-container { + min-height: 100vh; + padding-bottom: 180rpx; +} + +.section { + padding: 48rpx 32rpx; +} + +.section-title { + font-size: 36rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 24rpx; +} + +.section-subtitle { + font-size: 28rpx; + color: var(--secondary); + margin-bottom: 32rpx; +} + +/* ========== 按钮系统 ========== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 12rpx; + padding: 28rpx 56rpx; + border-radius: 9999rpx; + font-size: 30rpx; + font-weight: 500; + transition: all 0.25s ease; + border: none; + min-height: 88rpx; +} + +.btn-primary { + background: var(--primary); + color: #ffffff; + box-shadow: 0 4rpx 14rpx rgba(0, 113, 227, 0.25); +} + +.btn-primary:active { + background: var(--primary-hover); + transform: translateY(0); + box-shadow: 0 4rpx 20rpx rgba(0, 113, 227, 0.35); +} + +.btn-gradient { + background: linear-gradient(90deg, #0071e3, #34c759, #af52de); + background-size: 200% 200%; + color: #ffffff; + box-shadow: 0 4rpx 20rpx rgba(0, 113, 227, 0.3); + animation: gradientShift 8s ease infinite; +} + +@keyframes gradientShift { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +.btn-secondary { + background: var(--surface-secondary); + color: var(--primary); + border: 2rpx solid var(--border); +} + +.btn-secondary:active { + background: var(--surface-tertiary); +} + +.btn-outline { + background: transparent; + color: var(--primary); + border: 2rpx solid var(--primary); +} + +.btn-ghost { + background: transparent; + color: var(--primary); +} + +.btn-danger { + background: var(--error); + color: #ffffff; +} + +.btn-block { + width: 100%; +} + +.btn-lg { + padding: 32rpx 64rpx; + font-size: 32rpx; + min-height: 100rpx; +} + +.btn-sm { + padding: 16rpx 32rpx; + font-size: 26rpx; + min-height: 72rpx; +} + +.btn-icon { + width: 88rpx; + height: 88rpx; + padding: 0; + min-width: 88rpx; +} + +/* ========== 卡片系统 ========== */ +.card { + background: var(--surface-secondary); + border-radius: 32rpx; + padding: 40rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); + border: 1rpx solid var(--border-light); +} + +.card-header { + margin-bottom: 32rpx; + padding-bottom: 24rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.card-title { + font-size: 34rpx; + font-weight: 600; + color: var(--foreground); +} + +.card-subtitle { + font-size: 26rpx; + color: var(--secondary); + margin-top: 8rpx; +} + +.card-body { + padding: 16rpx 0; +} + +.card-footer { + margin-top: 32rpx; + padding-top: 24rpx; + border-top: 1rpx solid var(--border-light); + display: flex; + justify-content: flex-end; + gap: 20rpx; +} + +/* ========== 标签系统 ========== */ +.tag { + display: inline-flex; + align-items: center; + padding: 8rpx 24rpx; + border-radius: 9999rpx; + font-size: 24rpx; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.tag-primary { + background: rgba(0, 113, 227, 0.1); + color: var(--primary); +} + +.tag-accent { + background: rgba(52, 199, 89, 0.1); + color: var(--accent); +} + +.tag-warning { + background: rgba(255, 149, 0, 0.1); + color: var(--warning); +} + +.tag-error { + background: rgba(255, 59, 48, 0.1); + color: var(--error); +} + +.tag-muted { + background: var(--surface-tertiary); + color: var(--muted); +} + +/* ========== 文字系统 ========== */ +.text-gradient-brand { + background: linear-gradient(90deg, #0071e3, #34c759, #af52de); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.text-primary { + color: var(--primary); +} + +.text-accent { + color: var(--accent); +} + +.text-muted { + color: var(--muted); +} + +.text-secondary { + color: var(--secondary); +} + +.text-error { + color: var(--error); +} + +.text-warning { + color: var(--warning); +} + +.text-lg { + font-size: 32rpx; +} + +.text-xl { + font-size: 40rpx; +} + +.text-2xl { + font-size: 48rpx; +} + +.text-3xl { + font-size: 64rpx; +} + +.font-bold { + font-weight: 700; +} + +.font-semibold { + font-weight: 600; +} + +.font-medium { + font-weight: 500; +} + +/* ========== 间距工具类 ========== */ +.mt-2 { margin-top: 16rpx; } +.mt-4 { margin-top: 32rpx; } +.mt-6 { margin-top: 48rpx; } +.mb-2 { margin-bottom: 16rpx; } +.mb-4 { margin-bottom: 32rpx; } +.mb-6 { margin-bottom: 48rpx; } +.p-4 { padding: 32rpx; } +.py-4 { padding-top: 32rpx; padding-bottom: 32rpx; } +.px-4 { padding-left: 32rpx; padding-right: 32rpx; } + +/* ========== 网格与弹性布局 ========== */ +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.flex-row { + flex-direction: row; +} + +.items-center { + align-items: center; +} + +.items-start { + align-items: flex-start; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-end { + justify-content: flex-end; +} + +.gap-2 { + gap: 16rpx; +} + +.gap-3 { + gap: 24rpx; +} + +.gap-4 { + gap: 32rpx; +} + +.flex-1 { + flex: 1; +} + +.flex-wrap { + flex-wrap: wrap; +} + +/* ========== 列表与列表项 ========== */ +.list-item { + display: flex; + align-items: center; + gap: 24rpx; + padding: 32rpx; + background: var(--background); + border-radius: 24rpx; + margin-bottom: 16rpx; + border: 1rpx solid var(--border-light); + transition: all 0.25s ease; +} + +.list-item:active { + background: var(--surface); + transform: scale(0.98); +} + +.list-item-icon { + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + background: rgba(0, 113, 227, 0.08); + display: flex; + align-items: center; + justify-content: center; + font-size: 32rpx; + flex-shrink: 0; +} + +.list-item-content { + flex: 1; + min-width: 0; +} + +.list-item-title { + font-size: 30rpx; + font-weight: 500; + color: var(--foreground); +} + +.list-item-desc { + font-size: 26rpx; + color: var(--secondary); + margin-top: 6rpx; +} + +.list-item-price { + font-size: 32rpx; + font-weight: 700; + color: var(--primary); +} + +.list-item-action { + color: var(--primary); + font-size: 28rpx; + flex-shrink: 0; +} + +/* ========== 空状态 ========== */ +.empty-state { + padding: 120rpx 32rpx; + text-align: center; +} + +.empty-state-icon { + font-size: 120rpx; + margin-bottom: 32rpx; + opacity: 0.5; +} + +.empty-state-title { + font-size: 34rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 16rpx; +} + +.empty-state-desc { + font-size: 28rpx; + color: var(--secondary); + margin-bottom: 48rpx; +} + +/* ========== 进度条 ========== */ +.progress-bar { + height: 12rpx; + background: var(--surface-tertiary); + border-radius: 9999rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 9999rpx; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ========== 数字统计卡 ========== */ +.stat-card { + background: var(--background); + border-radius: 24rpx; + padding: 32rpx; + text-align: center; + border: 1rpx solid var(--border-light); +} + +.stat-value { + font-size: 40rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.stat-value.price { + color: var(--primary); +} + +.stat-label { + font-size: 24rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +/* ========== 分隔线 ========== */ +.divider { + height: 1rpx; + background: var(--border-light); + margin: 32rpx 0; +} + +/* ========== 安全区域 ========== */ +.safe-bottom { + padding-bottom: env(safe-area-inset-bottom); +} + +.safe-top { + padding-top: env(safe-area-inset-top); +} diff --git a/miniprogram/data/components.js b/miniprogram/data/components.js new file mode 100644 index 0000000..7a015fc --- /dev/null +++ b/miniprogram/data/components.js @@ -0,0 +1,473 @@ +// data/components.js - 自行车组件数据 + +const ROAD_DEFAULTS = [ + { + id: 'road-drivetrain-1', + category: 'Drivetrain', + bikeType: 'Road', + name: 'Shimano Dura-Ace Di2 R9200', + brand: 'Shimano', + model: 'Dura-Ace Di2 R9200', + price: 4200, + weight: 2430, + specs: { + speeds: 12, + cassetteRange: '11-30T', + chainrings: '52/36T', + shiftSpeed: 'fast' + } + }, + { + id: 'road-wheelset-1', + category: 'Wheelset', + bikeType: 'Road', + name: 'Roval Rapide CLX II', + brand: 'Roval', + model: 'Rapide CLX II', + price: 2800, + weight: 1520, + specs: { + rimDepth: '51mm', + rimWidth: '21mm internal', + material: 'carbon' + } + }, + { + id: 'road-cockpit-1', + category: 'Cockpit', + bikeType: 'Road', + name: 'Roval Rapide Cockpit', + brand: 'Roval', + model: 'Rapide Cockpit', + price: 600, + weight: 310, + specs: { + handlebarWidth: '420mm', + stemLength: '110mm', + dropReach: '75mm' + } + }, + { + id: 'road-tires-1', + category: 'Tires', + bikeType: 'Road', + name: 'Specialized S-Works Turbo 2Bliss Ready 28c', + brand: 'Specialized', + model: 'S-Works Turbo', + price: 180, + weight: 480, + specs: { + size: '700x28c', + compound: 'GRIPTON', + tpi: 120, + tubeless: true + } + } +]; + +const MTB_DEFAULTS = [ + { + id: 'mtb-drivetrain-1', + category: 'Drivetrain', + bikeType: 'MTB', + name: 'Shimano XTR Di2 M9100', + brand: 'Shimano', + model: 'XTR Di2 M9100', + price: 3800, + weight: 1920, + specs: { + speeds: 12, + cassetteRange: '10-51T', + shiftSpeed: 'instant' + } + }, + { + id: 'mtb-suspension-1', + category: 'Suspension', + bikeType: 'MTB', + name: 'Fox 34 Float Factory GRIP2', + brand: 'Fox Racing Shox', + model: '34 Float Factory', + price: 1050, + weight: 1738, + specs: { + travel: '140mm', + damping: 'GRIP2', + adjustability: '4-way adjustable' + } + }, + { + id: 'mtb-wheelset-1', + category: 'Wheelset', + bikeType: 'MTB', + name: 'Roval Traverse SL II 29', + brand: 'Roval', + model: 'Traverse SL II', + price: 1500, + weight: 1720, + specs: { + rimDepth: '25mm', + rimWidth: '30mm internal', + material: 'carbon' + } + }, + { + id: 'mtb-tires-1', + category: 'Tires', + bikeType: 'MTB', + name: 'Maxxis Minion DHR II 3C MaxxTerra', + brand: 'Maxxis', + model: 'Minion DHR II', + price: 140, + weight: 790, + specs: { + size: '29x2.4', + compound: '3C MaxxTerra', + tpi: 60, + tubeless: true + } + } +]; + +const FOLD_DEFAULTS = [ + { + id: 'fold-frame-1', + category: 'Frame', + bikeType: 'Fold', + name: 'Brompton Superlight Main Frame', + brand: 'Brompton', + model: 'Superlight Frame', + price: 1400, + weight: 1800, + specs: { + material: 'steel', + geometry: 'compact', + wheelSize: '16inch' + } + }, + { + id: 'fold-drivetrain-1', + category: 'Drivetrain', + bikeType: 'Fold', + name: 'Shimano Alfine Di2 SG-S7051-11', + brand: 'Shimano', + model: 'Alfine Di2 SG-S7051', + price: 1200, + weight: 1765, + specs: { + speeds: 11, + cassetteRange: 'internal gear hub', + batteryLife: '1000km' + } + }, + { + id: 'fold-wheelset-1', + category: 'Wheelset', + bikeType: 'Fold', + name: 'Brompton Superlight Wheelset', + brand: 'Brompton', + model: 'Superlight Wheelset', + price: 450, + weight: 1240, + specs: { + rimDepth: 'standard', + rimWidth: '19mm', + material: 'aluminum' + } + }, + { + id: 'fold-cockpit-1', + category: 'Cockpit', + bikeType: 'Fold', + name: 'Brompton M-Type Handlebar', + brand: 'Brompton', + model: 'M-Type Handlebar', + price: 120, + weight: 310, + specs: { + handlebarWidth: '540mm', + stemLength: 'integrated' + } + }, + { + id: 'fold-tires-1', + category: 'Tires', + bikeType: 'Fold', + name: 'Schwalbe Marathon Racer 16x1-1/3', + brand: 'Schwalbe', + model: 'Marathon Racer', + price: 65, + weight: 370, + specs: { + size: '16x1-1/3', + compound: 'RaceGuard', + tpi: 67, + tubeless: false + } + } +]; + +const ALTERNATIVES = { + Drivetrain: [ + { + category: 'Drivetrain', + name: 'Shimano Dura-Ace Di2 R9200', + brand: 'Shimano', + price: 4200, + weight: 2430, + description: '顶级电子变速,精准快速' + }, + { + category: 'Drivetrain', + name: 'SRAM Red AXS', + brand: 'SRAM', + price: 4000, + weight: 2380, + description: '12速无线变速系统' + }, + { + category: 'Drivetrain', + name: 'Campagnolo Super Record EPS', + brand: 'Campagnolo', + price: 4500, + weight: 2450, + description: '意大利工艺,极致体验' + }, + { + category: 'Drivetrain', + name: 'Shimano Ultegra Di2 R8100', + brand: 'Shimano', + price: 2800, + weight: 2600, + description: '次顶级电子变速,高性价比' + } + ], + Wheelset: [ + { + category: 'Wheelset', + name: 'Roval Rapide CLX II', + brand: 'Roval', + price: 2800, + weight: 1520, + description: '碳纤轮组,高性能气动轮' + }, + { + category: 'Wheelset', + name: 'Zipp 454 NSW', + brand: 'Zipp', + price: 3200, + weight: 1480, + description: '创新设计,极致气动' + }, + { + category: 'Wheelset', + name: 'Enve SES 4.5', + brand: 'Enve', + price: 2900, + weight: 1550, + description: '综合性能出色的碳轮' + }, + { + category: 'Wheelset', + name: 'DT Swiss ERC 1400', + brand: 'DT Swiss', + price: 2200, + weight: 1650, + description: '瑞士品质,舒适耐用' + } + ], + Cockpit: [ + { + category: 'Cockpit', + name: 'Roval Rapide Cockpit', + brand: 'Roval', + price: 600, + weight: 310, + description: '一体式气动座舱' + }, + { + category: 'Cockpit', + name: 'Enve SES AR', + brand: 'Enve', + price: 550, + weight: 320, + description: '气动弯把,碳纤维材质' + }, + { + category: 'Cockpit', + name: 'Deda SuperZero', + brand: 'Deda', + price: 380, + weight: 280, + description: '轻量化铝合金把组' + } + ], + Tires: [ + { + category: 'Tires', + name: 'Turbo Cotton 28mm', + brand: 'Specialized', + price: 180, + weight: 480, + description: '棉质胎体,极致抓地力' + }, + { + category: 'Tires', + name: 'GP5000 S TR', + brand: 'Continental', + price: 160, + weight: 450, + description: '顶级竞赛胎,低滚阻' + }, + { + category: 'Tires', + name: 'Michelin Power Cup', + brand: 'Michelin', + price: 170, + weight: 465, + description: '法国制造,高性能' + } + ], + Suspension: [ + { + category: 'Suspension', + name: 'Fox 34 Float Factory', + brand: 'Fox', + price: 1050, + weight: 1738, + description: '顶级山地车前叉' + }, + { + category: 'Suspension', + name: 'RockShox SID Ultimate', + brand: 'RockShox', + price: 950, + weight: 1650, + description: '轻量竞赛级前叉' + }, + { + category: 'Suspension', + name: 'Fox 32 Step-Cast Factory', + brand: 'Fox', + price: 1100, + weight: 1580, + description: '轻量级越野前叉' + } + ], + Frame: [ + { + category: 'Frame', + name: 'Titanium Main Frame', + brand: 'Custom', + price: 2100, + weight: 1800, + description: '钛合金车架,轻量化' + }, + { + category: 'Frame', + name: 'Carbon Front Triangle', + brand: 'Custom', + price: 1800, + weight: 1450, + description: '碳纤维主三角,极致轻量' + }, + { + category: 'Frame', + name: 'Steel Classic Frame', + brand: 'Classic', + price: 1200, + weight: 2100, + description: '经典钢制车架,耐用舒适' + } + ] +}; + +const RECOMMENDED_CONFIGS = [ + { + id: 'rec-1', + name: '入门优选', + bikeType: 'Road', + description: '适合新手的平衡配置,性能与价格的完美结合', + totalPrice: 15800, + totalWeight: 8500, + tags: ['热门', '高性价比'], + componentCount: 6 + }, + { + id: 'rec-2', + name: '竞赛级别', + bikeType: 'Road', + description: '专业竞赛配置,极致轻量化,追求极致速度', + totalPrice: 35000, + totalWeight: 6800, + tags: ['进阶'], + componentCount: 6 + }, + { + id: 'rec-3', + name: '越野先锋', + bikeType: 'MTB', + description: '山地越野爱好者的选择,强悍悬挂系统', + totalPrice: 22000, + totalWeight: 11500, + tags: ['越野'], + componentCount: 6 + }, + { + id: 'rec-4', + name: '城市伴侣', + bikeType: 'Fold', + description: '城市通勤首选,便携折叠,灵活便捷', + totalPrice: 8500, + totalWeight: 9800, + tags: ['通勤', '便携'], + componentCount: 5 + } +]; + +function getDefaultsForType(bikeType) { + switch (bikeType) { + case 'Road': + return ROAD_DEFAULTS.map(item => ({ ...item })); + case 'MTB': + return MTB_DEFAULTS.map(item => ({ ...item })); + case 'Fold': + return FOLD_DEFAULTS.map(item => ({ ...item })); + default: + return []; + } +} + +function getAlternativesForCategory(category) { + return (ALTERNATIVES[category] || []).map(item => ({ ...item })); +} + +function getCategoriesForBikeType(bikeType) { + const categories = { + Road: ['Drivetrain', 'Wheelset', 'Cockpit', 'Tires'], + MTB: ['Drivetrain', 'Suspension', 'Wheelset', 'Tires'], + Fold: ['Frame', 'Drivetrain', 'Wheelset', 'Cockpit', 'Tires'] + }; + return categories[bikeType] || []; +} + +function getBaseWeight(bikeType) { + const weights = { + Road: 900, + MTB: 1800, + Fold: 2000 + }; + return weights[bikeType] || 0; +} + +module.exports = { + ROAD_DEFAULTS, + MTB_DEFAULTS, + FOLD_DEFAULTS, + ALTERNATIVES, + RECOMMENDED_CONFIGS, + getDefaultsForType, + getAlternativesForCategory, + getCategoriesForBikeType, + getBaseWeight +}; diff --git a/miniprogram/pages/configurator/configurator.js b/miniprogram/pages/configurator/configurator.js new file mode 100644 index 0000000..cc83dbf --- /dev/null +++ b/miniprogram/pages/configurator/configurator.js @@ -0,0 +1,292 @@ +// pages/configurator/configurator.js - 配置器页面 + +const app = getApp(); +const { BIKE_TYPES, COMPONENT_CATEGORIES } = require('../../utils/constants'); +const { + getDefaultsForType, + getAlternativesForCategory, + getCategoriesForBikeType, + getBaseWeight +} = require('../../data/components'); +const configStore = require('../../utils/store'); +const { + formatPrice, + formatWeight, + formatDate, + showToast, + showModal, + showLoading, + hideLoading, + calculateProgress +} = require('../../utils/util'); + +Page({ + data: { + config: null, + components: [], + categories: [], + alternatives: [], + showAlternativeModal: false, + selectedCategory: '', + bikeTypeName: '', + bikeTypeIcon: '', + totalPrice: '¥0', + totalWeight: '0 kg', + progress: 0, + isEditingName: false, + editedName: '', + componentList: [] + }, + + onLoad(options) { + const configId = options.id; + this.loadConfig(configId); + }, + + onShow() { + if (this.data.config) { + this.loadConfig(this.data.config.id); + } + }, + + loadConfig(configId) { + let config; + if (configId) { + config = configStore.getConfigurations().find(c => c.id === configId); + } + + if (!config) { + config = configStore.getCurrentConfig(); + } + + if (!config) { + const bikeTypes = BIKE_TYPES.map(b => b.type); + config = configStore.createConfiguration(bikeTypes[0]); + } + + this.processConfig(config); + }, + + processConfig(config) { + const bikeTypeInfo = BIKE_TYPES.find(b => b.type === config.bikeType); + const bikeTypeName = bikeTypeInfo ? bikeTypeInfo.name : '自行车'; + const bikeTypeIcon = bikeTypeInfo ? bikeTypeInfo.icon : '🚲'; + + const categories = getCategoriesForBikeType(config.bikeType).map(cat => { + const catInfo = COMPONENT_CATEGORIES.find(c => c.key === cat); + const componentsInCategory = config.components.filter( + comp => comp.category === cat + ); + return { + key: cat, + name: catInfo ? catInfo.name : cat, + icon: catInfo ? catInfo.icon : '🔧', + components: componentsInCategory, + hasComponents: componentsInCategory.length > 0 + }; + }); + + const componentList = config.components.map(comp => { + const catInfo = COMPONENT_CATEGORIES.find(c => c.key === comp.category); + return { + ...comp, + categoryName: catInfo ? catInfo.name : comp.category, + categoryIcon: catInfo ? catInfo.icon : '🔧', + priceFormatted: formatPrice(comp.price), + weightFormatted: formatWeight(comp.weight) + }; + }); + + const totalPrice = config.components.reduce( + (sum, comp) => sum + (comp.price || 0), + 0 + ); + const totalWeight = + config.components.reduce((sum, comp) => sum + (comp.weight || 0), 0) + + getBaseWeight(config.bikeType); + + const progress = calculateProgress( + config.components, + categories.length + ); + + this.setData({ + config: { + ...config, + updatedAtFormatted: formatDate(config.updatedAt) + }, + components: config.components, + categories, + bikeTypeName, + bikeTypeIcon, + totalPrice: formatPrice(totalPrice), + totalWeight: formatWeight(totalWeight), + progress, + editedName: config.name, + componentList + }); + }, + + onEditName() { + this.setData({ isEditingName: true }); + }, + + onNameInput(e) { + this.setData({ editedName: e.detail.value }); + }, + + onSaveName() { + const { editedName, config } = this.data; + if (!editedName || !editedName.trim()) { + showToast('配置名称不能为空'); + return; + } + + const updatedConfig = configStore.updateConfiguration(config.id, { + name: editedName.trim() + }); + + if (updatedConfig) { + this.processConfig(updatedConfig); + showToast('名称已更新', 'success'); + } + + this.setData({ isEditingName: false }); + }, + + onCancelEditName() { + this.setData({ + isEditingName: false, + editedName: this.data.config.name + }); + }, + + onAddComponent(e) { + const { category } = e.currentTarget.dataset; + const alternatives = getAlternativesForCategory(category); + + if (alternatives.length === 0) { + showToast('暂无可用组件'); + return; + } + + const formattedAlternatives = alternatives.map((alt, index) => ({ + ...alt, + index: index + 1, + priceFormatted: formatPrice(alt.price), + weightFormatted: formatWeight(alt.weight) + })); + + this.setData({ + showAlternativeModal: true, + selectedCategory: category, + alternatives: formattedAlternatives + }); + }, + + onSelectAlternative(e) { + const { index } = e.currentTarget.dataset; + const alternatives = getAlternativesForCategory(this.data.selectedCategory); + const selected = alternatives[index]; + + if (selected) { + const updatedConfig = configStore.addComponent(this.data.config.id, selected); + if (updatedConfig) { + this.processConfig(updatedConfig); + showToast('组件已添加', 'success'); + } + } + + this.setData({ showAlternativeModal: false }); + }, + + onCloseModal() { + this.setData({ showAlternativeModal: false }); + }, + + onRemoveComponent(e) { + const { componentId } = e.currentTarget.dataset; + const component = this.data.components.find(c => c.id === componentId); + + showModal('删除组件', `确定要删除「${component.name}」吗?`, { + confirmText: '删除', + cancelText: '取消' + }).then(confirmed => { + if (confirmed) { + const updatedConfig = configStore.removeComponent( + this.data.config.id, + componentId + ); + if (updatedConfig) { + this.processConfig(updatedConfig); + showToast('组件已删除'); + } + } + }); + }, + + onLoadDefaults() { + const { config } = this.data; + + showModal('加载默认组件', '确定要为当前车型加载默认组件吗?这将替换现有组件。', { + confirmText: '加载', + cancelText: '取消' + }).then(confirmed => { + if (confirmed) { + showLoading('加载中...'); + + // 清空现有组件 + const updated = configStore.updateConfiguration(config.id, { + components: [] + }); + + // 添加默认组件 + const defaults = getDefaultsForType(config.bikeType); + defaults.forEach(comp => { + configStore.addComponent(config.id, comp); + }); + + setTimeout(() => { + hideLoading(); + this.loadConfig(config.id); + showToast('默认组件已加载', 'success'); + }, 500); + } + }); + }, + + onShareConfig() { + const { config, totalPrice, totalWeight } = this.data; + + wx.setClipboardData({ + data: `${config.name}\n车型: ${this.data.bikeTypeName}\n组件数: ${config.components.length}\n总价: ${totalPrice}\n总重: ${totalWeight}\n\n- Veloform`, + success() { + showToast('配置信息已复制', 'success'); + } + }); + }, + + onSaveConfig() { + showToast('配置已保存', 'success'); + }, + + onViewDetail() { + wx.navigateTo({ + url: `/pages/detail/detail?id=${this.data.config.id}` + }); + }, + + onBackToHome() { + wx.switchTab({ + url: '/pages/index/index' + }); + }, + + onShareAppMessage() { + const { config, totalPrice, totalWeight } = this.data; + return { + title: `${config.name} - ${totalPrice} / ${totalWeight}`, + path: `/pages/index/index` + }; + } +}); diff --git a/miniprogram/pages/configurator/configurator.json b/miniprogram/pages/configurator/configurator.json new file mode 100644 index 0000000..7855fa8 --- /dev/null +++ b/miniprogram/pages/configurator/configurator.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "配置器", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f7", + "enablePullDownRefresh": false, + "usingComponents": {} +} diff --git a/miniprogram/pages/configurator/configurator.wxml b/miniprogram/pages/configurator/configurator.wxml new file mode 100644 index 0000000..6351041 --- /dev/null +++ b/miniprogram/pages/configurator/configurator.wxml @@ -0,0 +1,205 @@ + + + + + + {{bikeTypeIcon}} + + {{bikeTypeName}} + 更新于 {{config.updatedAtFormatted}} + + + + + {{config.name}} + + ✏️ + + + + + + + 取消 + 保存 + + + + + + + + {{totalPrice}} + 总价格 + + + {{totalWeight}} + 预估重量 + + + {{components.length}} + 已选组件 + + + + + + + 配置进度 + {{progress}}% + + + + + + + + + + + 默认配置 + + + 📋 + 分享配置 + + + 📊 + 详细信息 + + + + + + + 组件管理 + 选择或替换你的自行车组件 + + + + + + + {{item.icon}} + + {{item.name}} + + {{item.hasComponents ? '已选择 ' + item.components.length + ' 个组件' : '未选择组件'}} + + + + + + + + + + + + {{comp.name}} + {{comp.brand}} + + + {{comp.priceFormatted}} + {{comp.weightFormatted}} + + + + + + + + + + + + + + + + 已选组件清单 + 共 {{componentList.length}} 个组件 + + + + + + {{idx + 1}} + + + {{item.categoryIcon}} {{item.categoryName}} + + {{item.name}} + {{item.brand}} · {{item.model}} + + + {{item.priceFormatted}} + {{item.weightFormatted}} + + + + + + + + + 🔧 + 还没有选择任何组件 + 点击上方分类卡片中的「+」按钮开始配置你的自行车,或点击「默认配置」一键加载推荐组件 + + 加载默认配置 + + + + + + + + + + + + + + 选择组件 + + + + + + + + {{item.name}} + {{item.brand}} · {{item.description}} + + + {{item.priceFormatted}} + {{item.weightFormatted}} + + + + + + + diff --git a/miniprogram/pages/configurator/configurator.wxss b/miniprogram/pages/configurator/configurator.wxss new file mode 100644 index 0000000..a7012a5 --- /dev/null +++ b/miniprogram/pages/configurator/configurator.wxss @@ -0,0 +1,586 @@ +/* pages/configurator/configurator.wxss */ + +.configurator-page { + background: var(--surface); + min-height: 100vh; + padding-bottom: 200rpx; +} + +/* ========== 配置头部 ========== */ +.config-header { + padding: 40rpx 32rpx; + background: var(--surface-secondary); + border-bottom: 1rpx solid var(--border-light); +} + +.config-bike-info { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 24rpx; +} + +.bike-icon-lg { + font-size: 64rpx; + width: 96rpx; + height: 96rpx; + border-radius: 24rpx; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.1), rgba(52, 199, 89, 0.1)); + display: flex; + align-items: center; + justify-content: center; +} + +.bike-meta { + flex: 1; + display: flex; + flex-direction: column; + gap: 8rpx; +} + +.bike-type-name { + font-size: 32rpx; + font-weight: 600; + color: var(--foreground); +} + +.config-update-time { + font-size: 24rpx; + color: var(--muted); +} + +.config-name-section { + display: flex; + align-items: center; + gap: 16rpx; + margin-top: 16rpx; +} + +.config-name { + font-size: 42rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.01em; + flex: 1; +} + +.edit-name-btn { + padding: 12rpx 20rpx; + border-radius: 16rpx; + background: var(--surface-tertiary); + font-size: 28rpx; +} + +.config-name-edit { + margin-top: 16rpx; +} + +.name-input { + font-size: 42rpx; + font-weight: 700; + padding: 16rpx 24rpx; + background: var(--surface); + border-radius: 16rpx; + border: 2rpx solid var(--primary); + margin-bottom: 20rpx; +} + +.name-actions { + display: flex; + gap: 16rpx; + justify-content: flex-end; +} + +/* ========== 统计面板 ========== */ +.stats-panel { + display: flex; + gap: 20rpx; + padding: 32rpx; + margin: 24rpx 32rpx; + background: var(--surface-secondary); + border-radius: 32rpx; + border: 1rpx solid var(--border-light); + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); +} + +.stats-panel .stat-card { + flex: 1; + text-align: center; + padding: 20rpx 0; + background: var(--surface); + border-radius: 24rpx; +} + +.stat-value { + font-size: 36rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.stat-value.price { + color: var(--primary); + background: linear-gradient(90deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ========== 进度条 ========== */ +.progress-section { + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.progress-label { + display: flex; + justify-content: space-between; + margin-bottom: 16rpx; + font-size: 26rpx; + color: var(--secondary); +} + +.progress-percent { + color: var(--primary); + font-weight: 600; +} + +.progress-bar { + height: 16rpx; + background: var(--surface-tertiary); + border-radius: 9999rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary), var(--accent)); + border-radius: 9999rpx; + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* ========== 快捷操作栏 ========== */ +.quick-actions { + display: flex; + gap: 16rpx; + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.quick-action-btn { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; + padding: 28rpx 20rpx; + background: var(--surface-secondary); + border-radius: 24rpx; + border: 1rpx solid var(--border-light); + font-size: 26rpx; + color: var(--foreground); + transition: all 0.25s ease; +} + +.quick-action-btn:active { + transform: scale(0.95); + background: var(--surface-tertiary); +} + +.quick-action-btn text:first-child { + font-size: 40rpx; +} + +/* ========== 分类添加区域 ========== */ +.categories-section { + padding: 0 32rpx; + margin-bottom: 32rpx; +} + +.section-title-bar { + margin-bottom: 24rpx; +} + +.section-title-text { + display: block; + font-size: 36rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.01em; +} + +.section-sub-text { + display: block; + font-size: 26rpx; + color: var(--secondary); + margin-top: 8rpx; +} + +.categories-list { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.category-card { + background: var(--surface-secondary); + border-radius: 28rpx; + border: 1rpx solid var(--border-light); + overflow: hidden; +} + +.category-header { + display: flex; + align-items: center; + padding: 28rpx; + gap: 20rpx; +} + +.category-icon { + font-size: 40rpx; + width: 72rpx; + height: 72rpx; + border-radius: 18rpx; + background: rgba(0, 113, 227, 0.08); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.category-info { + flex: 1; + min-width: 0; +} + +.category-name { + display: block; + font-size: 30rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 6rpx; +} + +.category-status { + display: block; + font-size: 24rpx; + color: var(--muted); +} + +.category-action { + width: 64rpx; + height: 64rpx; + border-radius: 16rpx; + background: linear-gradient(135deg, var(--primary), var(--accent)); + color: #ffffff; + display: flex; + align-items: center; + justify-content: center; + font-size: 36rpx; + font-weight: 300; + flex-shrink: 0; + box-shadow: 0 4rpx 12rpx rgba(0, 113, 227, 0.25); +} + +.category-action:active { + transform: scale(0.95); +} + +.category-components { + padding: 0 28rpx 28rpx; + border-top: 1rpx solid var(--border-light); + background: var(--surface); +} + +.component-item { + display: flex; + align-items: center; + padding: 24rpx 20rpx; + margin-top: 16rpx; + background: var(--surface-secondary); + border-radius: 20rpx; + gap: 20rpx; +} + +.component-info { + flex: 1; + min-width: 0; +} + +.component-name { + font-size: 28rpx; + font-weight: 500; + color: var(--foreground); + margin-bottom: 6rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.component-brand { + font-size: 24rpx; + color: var(--muted); +} + +.component-meta { + text-align: right; + flex-shrink: 0; +} + +.component-price { + font-size: 28rpx; + font-weight: 700; + color: var(--primary); + margin-bottom: 4rpx; +} + +.component-weight { + font-size: 22rpx; + color: var(--muted); +} + +.component-remove { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: rgba(255, 59, 48, 0.1); + color: var(--error); + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + flex-shrink: 0; + margin-left: 16rpx; +} + +/* ========== 完整组件列表 ========== */ +.components-section { + padding: 0 32rpx; + margin-bottom: 40rpx; +} + +.components-card { + background: var(--surface-secondary); + border-radius: 28rpx; + border: 1rpx solid var(--border-light); + padding: 12rpx; +} + +.components-list-item { + display: flex; + align-items: center; + padding: 28rpx 20rpx; + gap: 20rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.components-list-item:last-child { + border-bottom: none; +} + +.item-index { + width: 48rpx; + height: 48rpx; + border-radius: 12rpx; + background: rgba(0, 113, 227, 0.08); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + font-size: 26rpx; + font-weight: 600; + flex-shrink: 0; +} + +.item-content { + flex: 1; + min-width: 0; +} + +.item-header { + margin-bottom: 8rpx; +} + +.item-category { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.item-name { + font-size: 30rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 6rpx; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.item-brand { + font-size: 24rpx; + color: var(--secondary); +} + +.item-stats { + text-align: right; + flex-shrink: 0; +} + +.item-price { + font-size: 30rpx; + font-weight: 700; + color: var(--primary); + margin-bottom: 4rpx; +} + +.item-weight { + font-size: 24rpx; + color: var(--muted); +} + +/* ========== 底部操作栏 ========== */ +.bottom-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + gap: 20rpx; + padding: 24rpx 32rpx; + background: var(--surface-secondary); + border-top: 1rpx solid var(--border-light); + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04); + z-index: 100; +} + +.bottom-actions .btn { + flex: 1; +} + +/* ========== 弹窗 ========== */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(10rpx); + display: flex; + align-items: flex-end; + z-index: 1000; + animation: fadeIn 0.25s ease; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal-content { + width: 100%; + max-height: 70vh; + background: var(--surface-secondary); + border-radius: 32rpx 32rpx 0 0; + overflow: hidden; + animation: slideUp 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideUp { + from { transform: translateY(100%); } + to { transform: translateY(0); } +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 32rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.modal-title { + font-size: 34rpx; + font-weight: 600; + color: var(--foreground); +} + +.modal-close { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: var(--surface-tertiary); + color: var(--muted); + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; +} + +.modal-body { + padding: 20rpx 32rpx 48rpx; + max-height: 60vh; + overflow-y: auto; +} + +.alternative-item { + display: flex; + align-items: center; + gap: 20rpx; + padding: 28rpx; + background: var(--surface); + border-radius: 24rpx; + margin-bottom: 16rpx; + border: 1rpx solid var(--border-light); + transition: all 0.2s ease; +} + +.alternative-item-hover { + background: var(--surface-tertiary); + transform: scale(0.98); + border-color: var(--primary); +} + +.alternative-info { + flex: 1; + min-width: 0; +} + +.alternative-name { + font-size: 28rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.alternative-brand { + font-size: 24rpx; + color: var(--secondary); + line-height: 1.5; +} + +.alternative-stats { + text-align: right; + flex-shrink: 0; +} + +.alternative-price { + font-size: 30rpx; + font-weight: 700; + color: var(--primary); + margin-bottom: 4rpx; +} + +.alternative-weight { + font-size: 24rpx; + color: var(--muted); +} diff --git a/miniprogram/pages/detail/detail.js b/miniprogram/pages/detail/detail.js new file mode 100644 index 0000000..99f3bbb --- /dev/null +++ b/miniprogram/pages/detail/detail.js @@ -0,0 +1,182 @@ +// pages/detail/detail.js - 配置详情页 + +const app = getApp(); +const { BIKE_TYPES, COMPONENT_CATEGORIES, APP_INFO } = require('../../utils/constants'); +const { getBaseWeight, RECOMMENDED_CONFIGS } = require('../../data/components'); +const configStore = require('../../utils/store'); +const { + formatPrice, + formatWeight, + formatDate, + showToast, + showModal +} = require('../../utils/util'); + +Page({ + data: { + config: null, + isRecommended: false, + bikeTypeName: '', + bikeTypeIcon: '', + components: [], + totalPrice: '¥0', + totalWeight: '0 kg', + createdTime: '', + updatedTime: '', + componentCount: 0 + }, + + onLoad(options) { + const configId = options.id; + const type = options.type; + + if (type === 'recommended') { + this.loadRecommendedConfig(configId); + } else { + this.loadConfig(configId); + } + }, + + loadConfig(configId) { + let config; + if (configId) { + const configs = configStore.getConfigurations(); + config = configs.find(c => c.id === configId); + } + + if (!config) { + config = configStore.getCurrentConfig(); + } + + if (!config) { + showToast('配置不存在'); + setTimeout(() => { + wx.navigateBack(); + }, 1000); + return; + } + + this.processConfig(config); + }, + + loadRecommendedConfig(configId) { + const recommended = RECOMMENDED_CONFIGS.find(r => r.id === configId); + + if (!recommended) { + showToast('推荐配置不存在'); + setTimeout(() => { + wx.navigateBack(); + }, 1000); + return; + } + + const bikeTypeInfo = BIKE_TYPES.find(b => b.type === recommended.bikeType); + const bikeTypeName = bikeTypeInfo ? bikeTypeInfo.name : '自行车'; + const bikeTypeIcon = bikeTypeInfo ? bikeTypeInfo.icon : '🚲'; + + this.setData({ + config: { + ...recommended, + isRecommended: true, + description: recommended.description + }, + isRecommended: true, + bikeTypeName, + bikeTypeIcon, + componentCount: recommended.componentCount, + totalPrice: formatPrice(recommended.totalPrice), + totalWeight: formatWeight(recommended.totalWeight), + components: [] + }); + }, + + processConfig(config) { + const bikeTypeInfo = BIKE_TYPES.find(b => b.type === config.bikeType); + const bikeTypeName = bikeTypeInfo ? bikeTypeInfo.name : '自行车'; + const bikeTypeIcon = bikeTypeInfo ? bikeTypeInfo.icon : '🚲'; + + const processedComponents = config.components.map(comp => { + const catInfo = COMPONENT_CATEGORIES.find(c => c.key === comp.category); + return { + ...comp, + categoryName: catInfo ? catInfo.name : comp.category, + categoryIcon: catInfo ? catInfo.icon : '🔧', + priceFormatted: formatPrice(comp.price), + weightFormatted: formatWeight(comp.weight) + }; + }); + + const totalPrice = config.components.reduce( + (sum, comp) => sum + (comp.price || 0), + 0 + ); + const totalWeight = + config.components.reduce((sum, comp) => sum + (comp.weight || 0), 0) + + getBaseWeight(config.bikeType); + + this.setData({ + config: { + ...config, + componentCount: config.components.length + }, + bikeTypeName, + bikeTypeIcon, + components: processedComponents, + componentCount: config.components.length, + totalPrice: formatPrice(totalPrice), + totalWeight: formatWeight(totalWeight), + createdTime: formatDate(config.createdAt), + updatedTime: formatDate(config.updatedAt) + }); + }, + + onEdit() { + if (this.data.isRecommended) { + showModal('使用此配置', '是否基于此推荐配置创建一个新的配置?', { + confirmText: '创建配置', + cancelText: '取消' + }).then(confirmed => { + if (confirmed) { + const newConfig = configStore.createConfiguration( + this.data.config.bikeType, + this.data.config.name + ); + showToast('配置已创建', 'success'); + setTimeout(() => { + wx.redirectTo({ + url: `/pages/configurator/configurator?id=${newConfig.id}` + }); + }, 800); + } + }); + } else { + wx.redirectTo({ + url: `/pages/configurator/configurator?id=${this.data.config.id}` + }); + } + }, + + onShare() { + const { config, totalPrice, totalWeight, bikeTypeName } = this.data; + const shareText = `${config.name}\n车型: ${bikeTypeName}\n组件数: ${config.componentCount}\n总价: ${totalPrice}\n总重: ${totalWeight}\n\n- ${APP_INFO.name}`; + + wx.setClipboardData({ + data: shareText, + success() { + showToast('配置信息已复制', 'success'); + } + }); + }, + + onBack() { + wx.navigateBack(); + }, + + onShareAppMessage() { + const { config, totalPrice } = this.data; + return { + title: `${config.name} - ${totalPrice}`, + path: '/pages/index/index' + }; + } +}); diff --git a/miniprogram/pages/detail/detail.json b/miniprogram/pages/detail/detail.json new file mode 100644 index 0000000..6ef71d4 --- /dev/null +++ b/miniprogram/pages/detail/detail.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "配置详情", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f7", + "enablePullDownRefresh": false, + "usingComponents": {} +} diff --git a/miniprogram/pages/detail/detail.wxml b/miniprogram/pages/detail/detail.wxml new file mode 100644 index 0000000..ed53fd4 --- /dev/null +++ b/miniprogram/pages/detail/detail.wxml @@ -0,0 +1,126 @@ + + + + + + {{bikeTypeIcon}} + {{bikeTypeName}} + + + {{config.name}} + + + {{config.description}} + + + + + + {{totalPrice}} + 总价格 + + + {{totalWeight}} + 预估重量 + + + {{componentCount}} + 组件数量 + + + + + + + + 创建时间 + {{createdTime}} + + + + 更新时间 + {{updatedTime}} + + + + + + + 推荐配置 + 基于专业建议的平衡配置 + + + + + + 组件清单 + 共 {{componentCount}} 个组件 + + + + + + {{idx + 1}} + + + + {{item.categoryIcon}} {{item.categoryName}} + + {{item.name}} + {{item.brand}} · {{item.model}} + {{item.description}} + + + + + 价格 + {{item.priceFormatted}} + + + 重量 + {{item.weightFormatted}} + + + + + + + + + + ⚙️ + 暂无组件 + 此配置还没有选择任何组件,点击下方按钮开始配置 + + + + + 配置总结 + + + 总价格 + {{totalPrice}} + + + 总重量 + {{totalWeight}} + + + 平均组件价格 + {{'¥' + (config.totalPrice ? (config.totalPrice / config.componentCount) : 0)}} + + + + + + + + 📋 + 分享配置 + + + 使用此配置 + 编辑配置 + + + diff --git a/miniprogram/pages/detail/detail.wxss b/miniprogram/pages/detail/detail.wxss new file mode 100644 index 0000000..06259c2 --- /dev/null +++ b/miniprogram/pages/detail/detail.wxss @@ -0,0 +1,349 @@ +/* pages/detail/detail.wxss */ + +.detail-page { + background: var(--surface); + min-height: 100vh; + padding-bottom: 200rpx; +} + +/* ========== 头部信息 ========== */ +.detail-header { + padding: 40rpx 32rpx; + background: var(--surface-secondary); + border-bottom: 1rpx solid var(--border-light); +} + +.header-top { + display: flex; + align-items: center; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.bike-icon { + font-size: 52rpx; + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.1), rgba(52, 199, 89, 0.1)); + display: flex; + align-items: center; + justify-content: center; +} + +.bike-type-tag { + padding: 10rpx 24rpx; + background: var(--surface-tertiary); + border-radius: 9999rpx; + font-size: 24rpx; + color: var(--secondary); + font-weight: 500; +} + +.config-name { + font-size: 52rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.02em; + margin-bottom: 20rpx; +} + +.config-description { + font-size: 28rpx; + color: var(--secondary); + line-height: 1.6; + margin-bottom: 32rpx; +} + +.stats-grid { + display: flex; + gap: 16rpx; + margin-top: 32rpx; +} + +.detail-stat-card { + flex: 1; + background: var(--surface); + border-radius: 24rpx; + padding: 28rpx 20rpx; + text-align: center; + border: 1rpx solid var(--border-light); +} + +.stat-value { + font-size: 34rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.stat-value.price { + color: var(--primary); + background: linear-gradient(90deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.stat-label { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ========== 时间信息卡片 ========== */ +.time-info-card { + display: flex; + margin: 32rpx; + padding: 32rpx; + background: var(--surface-secondary); + border-radius: 24rpx; + border: 1rpx solid var(--border-light); +} + +.time-item { + flex: 1; + text-align: center; +} + +.time-label { + display: block; + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 8rpx; +} + +.time-value { + display: block; + font-size: 28rpx; + color: var(--foreground); + font-weight: 500; +} + +.time-item-divider { + width: 1rpx; + background: var(--border-light); + margin: 0 24rpx; +} + +/* ========== 推荐配置标识 ========== */ +.recommended-badge { + display: flex; + flex-direction: column; + align-items: center; + gap: 12rpx; + padding: 40rpx 32rpx; + margin: 0 32rpx 32rpx; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.05), rgba(52, 199, 89, 0.05)); + border-radius: 28rpx; + border: 1rpx solid rgba(0, 113, 227, 0.2); +} + +.badge-icon { + font-size: 64rpx; +} + +.badge-text { + font-size: 32rpx; + font-weight: 600; + color: var(--foreground); +} + +.badge-desc { + font-size: 26rpx; + color: var(--secondary); +} + +/* ========== 组件详细列表 ========== */ +.components-section { + padding: 0 32rpx; +} + +.section-title-bar { + margin-bottom: 24rpx; +} + +.section-title { + display: block; + font-size: 36rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.01em; +} + +.section-sub { + display: block; + font-size: 26rpx; + color: var(--secondary); + margin-top: 8rpx; +} + +.components-card { + background: var(--surface-secondary); + border-radius: 28rpx; + border: 1rpx solid var(--border-light); + padding: 12rpx; +} + +.component-detail-item { + display: flex; + align-items: flex-start; + gap: 20rpx; + padding: 32rpx 20rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.component-detail-item:last-child { + border-bottom: none; +} + +.item-index { + width: 56rpx; + height: 56rpx; + border-radius: 16rpx; + background: rgba(0, 113, 227, 0.08); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + font-size: 28rpx; + font-weight: 600; + flex-shrink: 0; +} + +.item-content { + flex: 1; + min-width: 0; +} + +.item-header-info { + margin-bottom: 12rpx; +} + +.item-category-tag { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.item-name-lg { + font-size: 32rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.item-brand-line { + font-size: 26rpx; + color: var(--secondary); + margin-bottom: 8rpx; +} + +.item-description { + font-size: 26rpx; + color: var(--muted); + line-height: 1.5; + margin-top: 12rpx; +} + +.item-stats-block { + flex-shrink: 0; + text-align: right; +} + +.stat-row { + margin-bottom: 12rpx; +} + +.stat-row:last-child { + margin-bottom: 0; +} + +.stat-key { + font-size: 22rpx; + color: var(--muted); + display: block; + margin-bottom: 4rpx; +} + +.stat-value-sm { + font-size: 28rpx; + font-weight: 600; + color: var(--foreground); +} + +.stat-value-sm.price { + color: var(--primary); +} + +/* ========== 总结卡片 ========== */ +.summary-card { + margin: 32rpx; + padding: 36rpx; + background: var(--surface-secondary); + border-radius: 28rpx; + border: 1rpx solid var(--border-light); +} + +.summary-title { + font-size: 32rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 24rpx; + padding-bottom: 20rpx; + border-bottom: 1rpx solid var(--border-light); +} + +.summary-grid { + display: flex; + flex-direction: column; + gap: 20rpx; +} + +.summary-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.summary-label { + font-size: 28rpx; + color: var(--secondary); +} + +.summary-value { + font-size: 32rpx; + font-weight: 700; + color: var(--foreground); +} + +.summary-value.price { + color: var(--primary); +} + +/* ========== 底部操作栏 ========== */ +.bottom-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + gap: 20rpx; + padding: 24rpx 32rpx; + background: var(--surface-secondary); + border-top: 1rpx solid var(--border-light); + box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.04); + z-index: 100; +} + +.bottom-actions .btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; +} diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js new file mode 100644 index 0000000..594e6cc --- /dev/null +++ b/miniprogram/pages/index/index.js @@ -0,0 +1,126 @@ +// pages/index/index.js - 首页(车型选择页) + +const app = getApp(); +const { BIKE_TYPES, APP_INFO, COMPONENT_CATEGORIES } = require('../../utils/constants'); +const { getDefaultsForType, RECOMMENDED_CONFIGS } = require('../../data/components'); +const configStore = require('../../utils/store'); +const { formatPrice, formatWeight, showToast, showModal } = require('../../utils/util'); + +Page({ + data: { + appInfo: APP_INFO, + bikeTypes: BIKE_TYPES, + selectedBikeType: 'Road', + activeBikeIndex: 0, + recommendedConfigs: [], + categories: COMPONENT_CATEGORIES, + version: APP_INFO.version + }, + + onLoad() { + this.loadRecommendedConfigs(); + }, + + onShow() { + const selectedType = configStore.getSelectedBikeType(); + const index = BIKE_TYPES.findIndex(b => b.type === selectedType); + this.setData({ + selectedBikeType: selectedType, + activeBikeIndex: index >= 0 ? index : 0 + }); + }, + + loadRecommendedConfigs() { + const configs = RECOMMENDED_CONFIGS.map(config => ({ + ...config, + totalPriceFormatted: formatPrice(config.totalPrice), + totalWeightFormatted: formatWeight(config.totalWeight), + bikeTypeName: this.getBikeTypeName(config.bikeType), + bikeTypeIcon: this.getBikeTypeIcon(config.bikeType) + })); + this.setData({ recommendedConfigs: configs }); + }, + + getBikeTypeName(bikeType) { + const names = { Road: '公路车', MTB: '山地车', Fold: '折叠车' }; + return names[bikeType] || '自行车'; + }, + + getBikeTypeIcon(bikeType) { + const icons = { Road: '🚴', MTB: '🚵', Fold: '🚲' }; + return icons[bikeType] || '🚲'; + }, + + onSelectBike(e) { + const { type, index } = e.currentTarget.dataset; + this.setData({ + selectedBikeType: type, + activeBikeIndex: index + }); + configStore.setSelectedBikeType(type); + }, + + onStartConfig() { + const { selectedBikeType } = this.data; + const bikeTypeName = this.getBikeTypeName(selectedBikeType); + + showModal('创建新配置', `开始创建你的${bikeTypeName}配置?`, { + confirmText: '开始配置', + cancelText: '取消' + }).then(confirmed => { + if (confirmed) { + const config = configStore.createConfiguration(selectedBikeType); + wx.navigateTo({ + url: `/pages/configurator/configurator?id=${config.id}` + }); + } + }); + }, + + onViewRecommended(e) { + const { id } = e.currentTarget.dataset; + const rec = RECOMMENDED_CONFIGS.find(r => r.id === id); + + if (rec) { + showModal(rec.name, `加载推荐配置「${rec.name}」?\n将使用默认组件创建该配置。`, { + confirmText: '使用此配置', + cancelText: '查看详情' + }).then(confirmed => { + if (confirmed) { + const config = configStore.createConfiguration(rec.bikeType, rec.name); + const defaults = getDefaultsForType(rec.bikeType); + defaults.forEach(comp => { + configStore.addComponent(config.id, comp); + }); + showToast('配置已创建'); + wx.navigateTo({ + url: `/pages/configurator/configurator?id=${config.id}` + }); + } else { + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}&type=recommended` + }); + } + }); + } + }, + + onViewLibrary() { + wx.switchTab({ + url: '/pages/library/library' + }); + }, + + onShareAppMessage() { + return { + title: `${APP_INFO.name} - 打造你的梦想自行车`, + path: '/pages/index/index' + }; + }, + + onShareTimeline() { + return { + title: `${APP_INFO.name} - 自行车配置器` + }; + } +}); diff --git a/miniprogram/pages/index/index.json b/miniprogram/pages/index/index.json new file mode 100644 index 0000000..046153b --- /dev/null +++ b/miniprogram/pages/index/index.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "Veloform · 打造你的梦想自行车", + "navigationBarBackgroundColor": "#f5f5f7", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f7", + "enablePullDownRefresh": false, + "usingComponents": {} +} diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000..588e333 --- /dev/null +++ b/miniprogram/pages/index/index.wxml @@ -0,0 +1,113 @@ + + + + + + + 全新设计体验 v{{version}} + + + 打造你的 + 梦想自行车 + + {{appInfo.description}} + + + + + + + + + + + + 选择你的车型 + 三种精心设计的车型,满足不同骑行需求 + + + + + + {{item.icon}} + {{item.name}} + {{item.desc}} + + + 已选择 + + + + + + + + + + + + 推荐配置 + 精选的配置方案,一键使用 + + + + + + + {{item.bikeTypeIcon}} + + + {{tag}} + + + + + {{item.name}} + {{item.description}} + + + + {{item.totalPriceFormatted}} + 总价格 + + + {{item.totalWeightFormatted}} + 预估重量 + + + + + 查看配置 → + + + + + + + + + + {{appInfo.name}} + Version {{appInfo.version}} + {{appInfo.tagline}} + + + diff --git a/miniprogram/pages/index/index.wxss b/miniprogram/pages/index/index.wxss new file mode 100644 index 0000000..bc7e68d --- /dev/null +++ b/miniprogram/pages/index/index.wxss @@ -0,0 +1,316 @@ +/* pages/index/index.wxss */ + +.hero { + background: linear-gradient(180deg, rgba(229, 229, 234, 0.6) 0%, var(--surface) 100%); + padding: 100rpx 40rpx 80rpx; + text-align: center; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 12rpx; + padding: 16rpx 32rpx; + background: rgba(0, 113, 227, 0.1); + border-radius: 9999rpx; + font-size: 24rpx; + color: var(--primary); + font-weight: 500; + margin-bottom: 32rpx; +} + +.hero-badge-dot { + width: 12rpx; + height: 12rpx; + border-radius: 50%; + background: var(--primary); + animation: pulse 2s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +.hero-title { + font-size: 64rpx; + font-weight: 700; + color: var(--foreground); + line-height: 1.2; + letter-spacing: -0.02em; + margin-bottom: 24rpx; +} + +.hero-title text { + display: inline; +} + +.hero-subtitle { + font-size: 30rpx; + color: var(--secondary); + line-height: 1.6; + max-width: 600rpx; + margin: 0 auto; + margin-bottom: 48rpx; +} + +.hero-bike-icon { + margin: 48rpx 0; + position: relative; + display: flex; + justify-content: center; + align-items: center; +} + +.bike-circle { + width: 300rpx; + height: 300rpx; + border-radius: 50%; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.1), rgba(52, 199, 89, 0.1)); + position: absolute; + animation: float 6s ease-in-out infinite; +} + +@keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-20rpx); } +} + +.bike-emoji { + font-size: 160rpx; + position: relative; + z-index: 1; +} + +.hero-actions { + margin-top: 32rpx; +} + +/* ========== 车型选择区域 ========== */ +.bike-selector-section { + padding: 64rpx 32rpx; + background: var(--surface); +} + +.section-header { + margin-bottom: 48rpx; +} + +.section-title { + display: block; + font-size: 42rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.01em; + margin-bottom: 12rpx; +} + +.section-subtitle { + display: block; + font-size: 28rpx; + color: var(--secondary); +} + +.bike-grid { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.bike-card { + background: var(--surface-secondary); + border-radius: 32rpx; + padding: 40rpx; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + border: 3rpx solid transparent; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); +} + +.bike-card-hover, +.bike-card.active { + border-color: var(--primary); + background: var(--surface-secondary); + box-shadow: 0 8rpx 30rpx rgba(0, 113, 227, 0.15); +} + +.bike-icon { + font-size: 64rpx; + width: 100rpx; + height: 100rpx; + border-radius: 24rpx; + background: rgba(0, 113, 227, 0.08); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 24rpx; + transition: all 0.35s ease; +} + +.bike-card.active .bike-icon { + background: linear-gradient(135deg, var(--primary), var(--accent)); + transform: scale(1.1); +} + +.bike-name { + font-size: 36rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 12rpx; + letter-spacing: -0.01em; +} + +.bike-desc { + font-size: 28rpx; + color: var(--secondary); + line-height: 1.6; +} + +.bike-active-indicator { + display: flex; + align-items: center; + gap: 12rpx; + margin-top: 24rpx; + padding-top: 24rpx; + border-top: 1rpx solid var(--border-light); +} + +.indicator-dot { + width: 16rpx; + height: 16rpx; + border-radius: 50%; + background: var(--primary); +} + +.bike-active-indicator text { + font-size: 26rpx; + color: var(--primary); + font-weight: 500; +} + +/* ========== 推荐配置区域 ========== */ +.recommended-section { + padding: 64rpx 32rpx; + background: var(--surface); + border-top: 1rpx solid var(--border-light); +} + +.recommended-grid { + display: flex; + flex-direction: column; + gap: 24rpx; +} + +.recommended-card { + background: var(--surface-secondary); + border-radius: 32rpx; + padding: 40rpx; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); + border: 1rpx solid var(--border-light); +} + +.recommended-card-hover { + transform: translateY(-4rpx); + box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.08); +} + +.recommended-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; +} + +.recommended-icon { + font-size: 48rpx; + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + background: rgba(52, 199, 89, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +.recommended-tags { + display: flex; + gap: 12rpx; +} + +.recommended-name { + font-size: 34rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 12rpx; + letter-spacing: -0.01em; +} + +.recommended-desc { + font-size: 28rpx; + color: var(--secondary); + line-height: 1.6; + margin-bottom: 32rpx; +} + +.recommended-meta { + display: flex; + gap: 32rpx; + margin-bottom: 32rpx; + padding-top: 32rpx; + border-top: 1rpx solid var(--border-light); +} + +.meta-item { + flex: 1; +} + +.meta-value { + font-size: 36rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.meta-value.price { + color: var(--primary); +} + +.meta-label { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.recommended-action { + font-size: 28rpx; + color: var(--primary); + font-weight: 500; +} + +/* ========== 底部说明区域 ========== */ +.footer-section { + padding: 64rpx 32rpx 120rpx; + text-align: center; + background: var(--surface); +} + +.footer-info { + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.footer-brand { + font-size: 40rpx; + letter-spacing: -0.02em; +} + +.footer-version { + font-size: 24rpx; + color: var(--muted); +} + +.footer-tagline { + font-size: 26rpx; + color: var(--secondary); +} diff --git a/miniprogram/pages/library/library.js b/miniprogram/pages/library/library.js new file mode 100644 index 0000000..adab4bd --- /dev/null +++ b/miniprogram/pages/library/library.js @@ -0,0 +1,167 @@ +// pages/library/library.js - 配置库页面 + +const app = getApp(); +const { BIKE_TYPES, APP_INFO, COMPONENT_CATEGORIES } = require('../../utils/constants'); +const { getBaseWeight } = require('../../data/components'); +const configStore = require('../../utils/store'); +const { + formatPrice, + formatWeight, + formatDate, + showToast, + showModal +} = require('../../utils/util'); + +Page({ + data: { + appInfo: APP_INFO, + configurations: [], + totalConfigs: 0, + totalPrice: '¥0', + isEmpty: true, + filter: 'all' + }, + + onLoad() { + this.loadConfigurations(); + }, + + onShow() { + this.loadConfigurations(); + }, + + loadConfigurations() { + const configs = configStore.getConfigurations(); + + if (!configs || configs.length === 0) { + this.setData({ + configurations: [], + totalConfigs: 0, + totalPrice: '¥0', + isEmpty: true + }); + return; + } + + const processedConfigs = configs.map(config => { + const bikeTypeInfo = BIKE_TYPES.find(b => b.type === config.bikeType); + const bikeTypeName = bikeTypeInfo ? bikeTypeInfo.name : '自行车'; + const bikeTypeIcon = bikeTypeInfo ? bikeTypeInfo.icon : '🚲'; + + const totalPrice = config.components.reduce( + (sum, comp) => sum + (comp.price || 0), + 0 + ); + const totalWeight = + config.components.reduce( + (sum, comp) => sum + (comp.weight || 0), + 0 + ) + getBaseWeight(config.bikeType); + + const categoryStats = {}; + config.components.forEach(comp => { + const category = comp.category; + if (!categoryStats[category]) { + const catInfo = COMPONENT_CATEGORIES.find(c => c.key === category); + categoryStats[category] = { + count: 0, + name: catInfo ? catInfo.name : category, + icon: catInfo ? catInfo.icon : '🔧' + }; + } + categoryStats[category].count += 1; + }); + + return { + ...config, + bikeTypeName, + bikeTypeIcon, + componentCount: config.components.length, + totalPriceFormatted: formatPrice(totalPrice), + totalWeightFormatted: formatWeight(totalWeight), + updatedAtFormatted: formatDate(config.updatedAt), + categories: Object.values(categoryStats) + }; + }); + + const totalConfigPrice = processedConfigs.reduce( + (sum, config) => { + const price = config.components.reduce( + (s, c) => s + (c.price || 0), + 0 + ); + return sum + price; + }, + 0 + ); + + this.setData({ + configurations: processedConfigs, + totalConfigs: processedConfigs.length, + totalPrice: formatPrice(totalConfigPrice), + isEmpty: false + }); + }, + + onViewConfig(e) { + const { id } = e.currentTarget.dataset; + configStore.setCurrentConfig(id); + wx.navigateTo({ + url: `/pages/detail/detail?id=${id}` + }); + }, + + onEditConfig(e) { + const { id } = e.currentTarget.dataset; + configStore.setCurrentConfig(id); + wx.navigateTo({ + url: `/pages/configurator/configurator?id=${id}` + }); + }, + + onDeleteConfig(e) { + const { id, name } = e.currentTarget.dataset; + + showModal('删除配置', `确定要删除「${name}」吗?此操作不可撤销。`, { + confirmText: '删除', + cancelText: '取消' + }).then(confirmed => { + if (confirmed) { + const success = configStore.deleteConfiguration(id); + if (success) { + showToast('已删除'); + this.loadConfigurations(); + } else { + showToast('删除失败'); + } + } + }); + }, + + onCreateNew() { + wx.switchTab({ + url: '/pages/index/index' + }); + }, + + onShareConfig(e) { + const { id } = e.currentTarget.dataset; + const config = this.data.configurations.find(c => c.id === id); + + if (config) { + wx.setClipboardData({ + data: `${config.name}\n车型: ${config.bikeTypeName}\n组件数: ${config.componentCount}\n总价: ${config.totalPriceFormatted}\n总重: ${config.totalWeightFormatted}\n\n- ${this.data.appInfo.name}`, + success() { + showToast('配置信息已复制', 'success'); + } + }); + } + }, + + onShareAppMessage() { + return { + title: `${this.data.appInfo.name} - 我的自行车配置`, + path: '/pages/index/index' + }; + } +}); diff --git a/miniprogram/pages/library/library.json b/miniprogram/pages/library/library.json new file mode 100644 index 0000000..aec74b0 --- /dev/null +++ b/miniprogram/pages/library/library.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "配置库", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#f5f5f7", + "enablePullDownRefresh": true, + "usingComponents": {} +} diff --git a/miniprogram/pages/library/library.wxml b/miniprogram/pages/library/library.wxml new file mode 100644 index 0000000..e9f98c1 --- /dev/null +++ b/miniprogram/pages/library/library.wxml @@ -0,0 +1,132 @@ + + + + + 我的配置库 + 管理你的所有自行车配置 + + + + {{totalConfigs}} + 配置总数 + + + + {{totalPrice}} + 总价值 + + + + + + + 📋 + 还没有配置 + 开始创建你的第一个自行车配置,打造专属于你的骑行装备 + + + + + + + + + + + + {{item.bikeTypeIcon}} + + {{item.name}} + {{item.bikeTypeName}} + + + {{item.componentCount}} 组件 + + + + + + + {{item.totalPriceFormatted}} + 总价格 + + + {{item.totalWeightFormatted}} + 预估重量 + + + {{item.componentCount}} + 已选组件 + + + + + + + + {{cat.icon}} + {{cat.name}} + {{cat.count}} + + + + + + 更新于 {{item.updatedAtFormatted}} + + + + + 📋 + 分享 + + + 🗑️ + 删除 + + + ✏️ + 编辑 + + + 👁️ + 详情 + + + + + + + + + 创建新配置 + + + + + + {{appInfo.name}} + Version {{appInfo.version}} + + diff --git a/miniprogram/pages/library/library.wxss b/miniprogram/pages/library/library.wxss new file mode 100644 index 0000000..4831aeb --- /dev/null +++ b/miniprogram/pages/library/library.wxss @@ -0,0 +1,297 @@ +/* pages/library/library.wxss */ + +.library-page { + background: var(--surface); + min-height: 100vh; + padding-bottom: 120rpx; +} + +/* ========== 头部概览 ========== */ +.header { + padding: 60rpx 32rpx 40rpx; + background: var(--surface-secondary); + border-bottom: 1rpx solid var(--border-light); +} + +.header-title { + font-size: 48rpx; + font-weight: 700; + color: var(--foreground); + letter-spacing: -0.02em; + margin-bottom: 12rpx; +} + +.header-subtitle { + font-size: 28rpx; + color: var(--secondary); + margin-bottom: 40rpx; +} + +.overview-card { + background: var(--surface); + border-radius: 32rpx; + padding: 32rpx; + display: flex; + align-items: center; + border: 1rpx solid var(--border-light); +} + +.overview-item { + flex: 1; + text-align: center; +} + +.overview-value { + font-size: 48rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; + letter-spacing: -0.02em; +} + +.overview-value.price { + color: var(--primary); + background: linear-gradient(90deg, var(--primary), var(--accent)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.overview-label { + font-size: 24rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.overview-divider { + width: 2rpx; + height: 80rpx; + background: var(--border-light); + margin: 0 24rpx; +} + +/* ========== 配置列表 ========== */ +.config-list { + padding: 32rpx; +} + +.config-card { + background: var(--surface-secondary); + border-radius: 32rpx; + padding: 36rpx; + margin-bottom: 24rpx; + border: 1rpx solid var(--border-light); + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04); + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); +} + +.config-card-hover { + transform: translateY(-4rpx); + box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.08); +} + +/* 卡片头部 */ +.config-card-header { + display: flex; + align-items: center; + gap: 20rpx; + margin-bottom: 28rpx; +} + +.config-bike-icon { + font-size: 48rpx; + width: 80rpx; + height: 80rpx; + border-radius: 20rpx; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.1), rgba(52, 199, 89, 0.1)); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.config-info { + flex: 1; + min-width: 0; +} + +.config-name { + font-size: 34rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 8rpx; + letter-spacing: -0.01em; +} + +.config-type { + font-size: 26rpx; + color: var(--secondary); +} + +.config-status { + flex-shrink: 0; +} + +/* 统计信息 */ +.config-stats { + display: flex; + gap: 20rpx; + padding: 24rpx 0; + border-top: 1rpx solid var(--border-light); + border-bottom: 1rpx solid var(--border-light); + margin-bottom: 24rpx; +} + +.stat-block { + flex: 1; + text-align: center; +} + +.stat-value { + font-size: 32rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; +} + +.stat-value.price { + color: var(--primary); +} + +.stat-label { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* 分类标签 */ +.config-categories { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + margin-bottom: 20rpx; +} + +.category-pill { + display: inline-flex; + align-items: center; + gap: 8rpx; + padding: 12rpx 20rpx; + background: var(--surface-tertiary); + border-radius: 9999rpx; + font-size: 24rpx; +} + +.category-pill-icon { + font-size: 24rpx; +} + +.category-pill-name { + color: var(--secondary); + font-weight: 500; +} + +.category-pill-count { + color: var(--primary); + font-weight: 600; +} + +/* 更新时间 */ +.config-update-time { + font-size: 24rpx; + color: var(--muted); + margin-bottom: 24rpx; +} + +/* 操作按钮 */ +.config-actions { + display: flex; + gap: 12rpx; +} + +.action-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 20rpx 16rpx; + border-radius: 16rpx; + font-size: 26rpx; + font-weight: 500; + transition: all 0.25s ease; +} + +.action-btn:active { + transform: scale(0.95); +} + +.action-secondary { + background: var(--surface-tertiary); + color: var(--foreground); +} + +.action-primary { + background: rgba(0, 113, 227, 0.1); + color: var(--primary); +} + +.action-gradient { + background: linear-gradient(90deg, #0071e3, #34c759); + color: #ffffff; + box-shadow: 0 4rpx 12rpx rgba(0, 113, 227, 0.25); +} + +/* 创建新配置按钮 */ +.create-new-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 12rpx; + padding: 48rpx 32rpx; + background: var(--surface-secondary); + border-radius: 32rpx; + border: 2rpx dashed var(--border); + margin-top: 16rpx; + transition: all 0.25s ease; +} + +.create-new-btn:active { + background: var(--surface-tertiary); + border-color: var(--primary); + transform: scale(0.98); +} + +.plus-icon { + font-size: 48rpx; + color: var(--primary); + font-weight: 300; +} + +.btn-text { + font-size: 28rpx; + color: var(--foreground); + font-weight: 500; +} + +/* 底部信息 */ +.footer-info { + text-align: center; + padding: 48rpx 32rpx; + display: flex; + flex-direction: column; + gap: 12rpx; +} + +.footer-brand { + font-size: 32rpx; + letter-spacing: -0.02em; +} + +.footer-version { + font-size: 22rpx; + color: var(--muted); +} diff --git a/miniprogram/project.config.json b/miniprogram/project.config.json new file mode 100644 index 0000000..15ca2aa --- /dev/null +++ b/miniprogram/project.config.json @@ -0,0 +1,53 @@ +{ + "description": "项目配置文件", + "packOptions": { + "ignore": [], + "include": [] + }, + "setting": { + "bundle": false, + "userConfirmedBundleSwitch": false, + "urlCheck": true, + "scopeDataCheck": false, + "coverView": true, + "es6": true, + "postcss": true, + "compileHotReLoad": false, + "lazyloadPlaceholderEnable": false, + "preloadBackgroundData": false, + "minified": true, + "autoAudits": false, + "newFeature": false, + "uglifyFileName": false, + "uploadWithSourceMap": true, + "useIsolateContext": true, + "nodeModules": false, + "enhance": true, + "useMultiFrameRuntime": true, + "useApiHook": true, + "useApiHostProcess": true, + "showShadowRootInWxmlPanel": true, + "packNpmManually": false, + "enableEngineNative": false, + "packNpmRelationList": [], + "minifyWXSS": true, + "minifyWXML": true, + "showES6CompileOption": false, + "babelSetting": { + "ignore": [], + "disablePlugins": [], + "outputPath": "" + }, + "ignoreUploadUnusedFiles": true + }, + "compileType": "miniprogram", + "libVersion": "3.5.2", + "appid": "touristappid", + "projectname": "veloform-miniprogram", + "condition": {}, + "editorSetting": { + "tabIndent": "insertSpaces", + "tabSize": 2 + }, + "miniprogramRoot": "./" +} diff --git a/miniprogram/sitemap.json b/miniprogram/sitemap.json new file mode 100644 index 0000000..9230ad8 --- /dev/null +++ b/miniprogram/sitemap.json @@ -0,0 +1,9 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [ + { + "action": "allow", + "page": "*" + } + ] +} diff --git a/miniprogram/utils/constants.js b/miniprogram/utils/constants.js new file mode 100644 index 0000000..147e50d --- /dev/null +++ b/miniprogram/utils/constants.js @@ -0,0 +1,64 @@ +// utils/constants.js - 项目常量与配置 + +const APP_INFO = { + name: 'Veloform', + version: '3.8.0', + tagline: 'Bike Configurator', + description: '打造你的梦想自行车,从车型选择到组件配置,每一个细节由你掌控' +}; + +const BIKE_TYPES = [ + { + type: 'Road', + name: '公路车', + desc: '速度与激情,专为竞速打造,轻量化设计让你风驰电掣', + icon: '🚴', + baseWeight: 900, + gradientFrom: '#0071e3', + gradientTo: '#af52de' + }, + { + type: 'MTB', + name: '山地车', + desc: '征服山野,强悍的悬挂系统应对各种复杂地形', + icon: '🚵', + baseWeight: 1800, + gradientFrom: '#34c759', + gradientTo: '#0071e3' + }, + { + type: 'Fold', + name: '折叠车', + desc: '灵活便携,轻松收纳,城市通勤的最佳伴侣', + icon: '🚲', + baseWeight: 2000, + gradientFrom: '#ff9500', + gradientTo: '#34c759' + } +]; + +const COMPONENT_CATEGORIES = [ + { key: 'Frame', name: '车架', icon: '🏗️' }, + { key: 'Drivetrain', name: '传动系统', icon: '⚙️' }, + { key: 'Wheelset', name: '轮组', icon: '🔘' }, + { key: 'Suspension', name: '避震系统', icon: '🔧' }, + { key: 'Cockpit', name: '操控系统', icon: '🎯' }, + { key: 'Tires', name: '轮胎', icon: '⚪' } +]; + +const CURRENCY = '¥'; + +const STORAGE_KEYS = { + CONFIGURATIONS: 'veloform_configurations', + CURRENT_CONFIG: 'veloform_current_config', + USER_INFO: 'veloform_user_info', + SETTINGS: 'veloform_settings' +}; + +module.exports = { + APP_INFO, + BIKE_TYPES, + COMPONENT_CATEGORIES, + CURRENCY, + STORAGE_KEYS +}; diff --git a/miniprogram/utils/store.js b/miniprogram/utils/store.js new file mode 100644 index 0000000..3316d12 --- /dev/null +++ b/miniprogram/utils/store.js @@ -0,0 +1,232 @@ +// utils/store.js - 配置状态管理与本地存储 + +const { STORAGE_KEYS } = require('./constants'); +const { generateId, deepClone, showToast, formatDate } = require('./util'); + +let globalState = { + configurations: [], + currentConfig: null, + selectedBikeType: 'Road', + isLoaded: false +}; + +function init() { + try { + const configs = wx.getStorageSync(STORAGE_KEYS.CONFIGURATIONS) || []; + const current = wx.getStorageSync(STORAGE_KEYS.CURRENT_CONFIG) || null; + + globalState.configurations = configs; + globalState.currentConfig = current; + globalState.isLoaded = true; + + // 如果没有保存的配置,创建默认配置 + if (!current && configs.length === 0) { + createDefaultConfig(); + } + } catch (e) { + console.error('初始化存储失败', e); + globalState.isLoaded = true; + } +} + +function refresh() { + try { + globalState.configurations = wx.getStorageSync(STORAGE_KEYS.CONFIGURATIONS) || []; + globalState.currentConfig = wx.getStorageSync(STORAGE_KEYS.CURRENT_CONFIG) || null; + } catch (e) { + console.error('刷新存储失败', e); + } +} + +function createDefaultConfig() { + const defaultConfig = { + id: generateId('config'), + name: '我的公路车配置', + bikeType: 'Road', + components: [], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + globalState.configurations = [defaultConfig]; + globalState.currentConfig = defaultConfig; + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('保存默认配置失败', e); + } +} + +function getConfigurations() { + return deepClone(globalState.configurations); +} + +function getCurrentConfig() { + return deepClone(globalState.currentConfig); +} + +function setCurrentConfig(configId) { + const config = globalState.configurations.find(c => c.id === configId); + if (config) { + globalState.currentConfig = config; + try { + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, config); + } catch (e) { + console.error('设置当前配置失败', e); + } + return true; + } + return false; +} + +function createConfiguration(bikeType, name) { + const newConfig = { + id: generateId('config'), + name: name || `我的${getBikeTypeName(bikeType)}配置`, + bikeType: bikeType, + components: [], + createdAt: Date.now(), + updatedAt: Date.now() + }; + + globalState.configurations.unshift(newConfig); + globalState.currentConfig = newConfig; + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('创建配置失败', e); + } + + return deepClone(newConfig); +} + +function updateConfiguration(configId, updates) { + const index = globalState.configurations.findIndex(c => c.id === configId); + if (index === -1) return null; + + globalState.configurations[index] = { + ...globalState.configurations[index], + ...updates, + updatedAt: Date.now() + }; + + if (globalState.currentConfig && globalState.currentConfig.id === configId) { + globalState.currentConfig = globalState.configurations[index]; + } + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('更新配置失败', e); + } + + return deepClone(globalState.configurations[index]); +} + +function deleteConfiguration(configId) { + const index = globalState.configurations.findIndex(c => c.id === configId); + if (index === -1) return false; + + globalState.configurations.splice(index, 1); + + if (globalState.currentConfig && globalState.currentConfig.id === configId) { + globalState.currentConfig = globalState.configurations[0] || null; + } + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('删除配置失败', e); + } + + return true; +} + +function addComponent(configId, component) { + const config = globalState.configurations.find(c => c.id === configId); + if (!config) return null; + + const newComponent = { + ...component, + id: generateId('comp'), + addedAt: Date.now() + }; + + config.components.push(newComponent); + config.updatedAt = Date.now(); + + if (globalState.currentConfig && globalState.currentConfig.id === configId) { + globalState.currentConfig = config; + } + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('添加组件失败', e); + } + + return deepClone(config); +} + +function removeComponent(configId, componentId) { + const config = globalState.configurations.find(c => c.id === configId); + if (!config) return null; + + const compIndex = config.components.findIndex(c => c.id === componentId); + if (compIndex !== -1) { + config.components.splice(compIndex, 1); + config.updatedAt = Date.now(); + } + + if (globalState.currentConfig && globalState.currentConfig.id === configId) { + globalState.currentConfig = config; + } + + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + } catch (e) { + console.error('删除组件失败', e); + } + + return deepClone(config); +} + +function getBikeTypeName(bikeType) { + const names = { + Road: '公路车', + MTB: '山地车', + Fold: '折叠车' + }; + return names[bikeType] || '自行车'; +} + +function setSelectedBikeType(bikeType) { + globalState.selectedBikeType = bikeType; +} + +function getSelectedBikeType() { + return globalState.selectedBikeType; +} + +module.exports = { + init, + refresh, + getConfigurations, + getCurrentConfig, + setCurrentConfig, + createConfiguration, + updateConfiguration, + deleteConfiguration, + addComponent, + removeComponent, + setSelectedBikeType, + getSelectedBikeType +}; diff --git a/miniprogram/utils/util.js b/miniprogram/utils/util.js new file mode 100644 index 0000000..6ebf8e1 --- /dev/null +++ b/miniprogram/utils/util.js @@ -0,0 +1,120 @@ +// utils/util.js - 通用工具函数 + +function formatPrice(price, currency = '¥') { + return `${currency}${Number(price).toLocaleString('zh-CN')}`; +} + +function formatWeight(grams) { + if (grams >= 1000) { + return `${(grams / 1000).toFixed(2)} kg`; + } + return `${grams} g`; +} + +function formatDate(date) { + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hour = String(d.getHours()).padStart(2, '0'); + const minute = String(d.getMinutes()).padStart(2, '0'); + return `${year}-${month}-${day} ${hour}:${minute}`; +} + +function generateId(prefix = 'cfg') { + return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +} + +function debounce(fn, delay = 300) { + let timer = null; + return function (...args) { + if (timer) clearTimeout(timer); + timer = setTimeout(() => fn.apply(this, args), delay); + }; +} + +function throttle(fn, delay = 300) { + let last = 0; + return function (...args) { + const now = Date.now(); + if (now - last >= delay) { + last = now; + fn.apply(this, args); + } + }; +} + +function deepClone(obj) { + if (obj === null || typeof obj !== 'object') return obj; + if (Array.isArray(obj)) return obj.map(item => deepClone(item)); + const cloned = {}; + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + cloned[key] = deepClone(obj[key]); + } + } + return cloned; +} + +function showToast(title, icon = 'none', duration = 2000) { + wx.showToast({ + title, + icon, + duration + }); +} + +function showLoading(title = '加载中...') { + wx.showLoading({ title, mask: true }); +} + +function hideLoading() { + wx.hideLoading(); +} + +function showModal(title, content, options = {}) { + return new Promise(resolve => { + wx.showModal({ + title, + content, + confirmColor: '#0071e3', + cancelText: options.cancelText || '取消', + confirmText: options.confirmText || '确定', + success: res => { + resolve(res.confirm); + } + }); + }); +} + +function calculateTotalPrice(components) { + if (!components || !components.length) return 0; + return components.reduce((sum, comp) => sum + (comp.price || 0), 0); +} + +function calculateTotalWeight(components, baseWeight = 0) { + if (!components || !components.length) return baseWeight; + return components.reduce((sum, comp) => sum + (comp.weight || 0), baseWeight); +} + +function calculateProgress(components, expectedCount = 6) { + if (!components || !components.length) return 0; + return Math.round((components.length / expectedCount) * 100); +} + +module.exports = { + formatPrice, + formatWeight, + formatDate, + generateId, + debounce, + throttle, + deepClone, + showToast, + showLoading, + hideLoading, + showModal, + calculateTotalPrice, + calculateTotalWeight, + calculateProgress +}; From 92d602526c4e69b037d498a0a1d3e931aeed140e Mon Sep 17 00:00:00 2001 From: AjaCHN Date: Wed, 17 Jun 2026 08:49:13 +0000 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=E5=88=86=E6=9E=90=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=A1=B5=E9=9D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- prototype/INDEX.md | 230 +++++++++++++++------------ prototype/README.md | 190 ++++++++++------------- prototype/component-library.md | 241 ++++++++++++++++++++++++++++- prototype/design-system-spec.md | 215 +++++++++++++++++++++---- prototype/interaction-standards.md | 84 ++++++++++ 5 files changed, 729 insertions(+), 231 deletions(-) diff --git a/prototype/INDEX.md b/prototype/INDEX.md index 6adc74b..b4ed41b 100644 --- a/prototype/INDEX.md +++ b/prototype/INDEX.md @@ -1,119 +1,128 @@ -# Veloform 原型设计文档 +# Veloform 原型设计文档索引 -> **路径**: `/prototype/INDEX.md` -> **版本**: v2.0 -> **更新日期**: 2026-06-05 - -## 概述 - -本目录包含 Veloform 自行车配置器的核心原型设计资源。所有原型均以 **高保真原型** 为规范标准。 +> **路径**: `/prototype/INDEX.md` +> **版本**: v3.8.0 +> **更新日期**: 2026-06-17 --- -## 目录结构 +## 概述 -``` -prototype/ -├── prototype-high-fidelity.html # ⭐ 高保真原型(规范标准) -├── prototype-guide.md # 原型使用指南 -├── design-improvements.md # UI/UX 优化建议 -└── INDEX.md # 本文档 -``` +本目录包含 Veloform 自行车配置器的核心原型设计资源与规范文档。所有规范均以 **高保真原型** 为标准,采用 **shadcn/ui + Radix UI + Tailwind CSS** 技术栈。 --- -## 核心文件 +## 规范体系 -### prototype-high-fidelity.html ⭐ +Veloform 设计系统采用三层规范结构,确保设计与开发的一致性: -**高保真原型 - 规范标准** +| 层级 | 文档 | 作用 | 面向对象 | +|-----|------|------|---------| +| **L1 - 设计系统** | design-system-spec.md | 色彩、字体、间距、图标、动效基础规范 | 设计师 + 开发者 | +| **L2 - 组件库** | component-library.md | 组件分类、变体、状态、使用规则 | 开发者 | +| **L3 - 交互标准** | interaction-standards.md | 交互模式、反馈、错误处理、空状态 | 设计师 + 开发者 | -这是项目的核心原型文件,包含了完整的设计系统、交互逻辑和视觉效果。所有设计和开发均应以此文件为准。 +--- -**特性**: -- 完整的响应式设计(桌面端、平板、移动端) -- 精致的微交互动画和过渡效果 -- 无障碍访问支持(ARIA 属性、键盘导航、prefers-reduced-motion) -- SEO 优化元数据 -- 深色主题 + 玻璃态效果 -- 实时价格计算和性能评分 -- 组件选择器模态框 -- Toast 通知系统 -- 移动端底部导航栏 +## 核心规范文档 -**技术栈**: -- HTML5 + Tailwind CSS CDN -- Lucide Icons -- Vanilla JavaScript -- CSS 变量设计系统 -- Framer Motion 风格动画 +### 1. 设计系统规范 ⭐ +> `design-system-spec.md` · v3.8.0 -**预览方式**: -```bash -# 使用 Python 本地服务器 -cd prototype && python3 -m http.server 8080 +**核心内容**: +- 色彩系统(Core Tokens、主色阶、渐变色) +- 字体系统(SF Pro Display/Text、字号层级、字重) +- 间距系统(4px 网格、组件内边距、圆角) +- 图标规范(Lucide React、尺寸、语义) +- 动效系统(预设动画、Keyframes、Framer Motion 预设) +- Utility Classes(.gradient-brand、.gradient-text) -# 或直接在浏览器中打开 -open prototype-high-fidelity.html -``` +**设计原则**: +- 即时响应(≤100ms 反馈) +- 自然过渡(cubic-bezier缓动) +- 适度克制(≤400ms 动画) ---- +### 2. 组件库规范 ⭐ +> `component-library.md` · v3.8.0 -## 支持文档 +**组件层级**: +- **L1 基础组件**:Button、Input、Card、Modal、Badge、Toast +- **L2 复合组件**:Form、DataTable、Pagination、Dropdown +- **L3 业务组件**:BikeTypeSelector、ComponentSelector、SummaryPanel +- **L4 页面 Section**:Hero、Features、Pricing -### prototype-guide.md +**技术来源**:shadcn/ui CLI + Radix UI + Tailwind CSS -原型使用指南,包含详细的功能说明、组件映射和交互流程。 +### 3. 交互标准 ⭐ +> `interaction-standards.md` · v3.8.0 -### design-improvements.md - -顶级 UI/UX 设计师视角的优化建议,包含 16 项专业改进方向。 +**核心内容**: +- 交互模式库(导航、选择、列表、搜索) +- 反馈规范(悬停、按下、焦点、加载、Toast) +- 错误处理(输入错误、网络错误、错误边界) +- 空状态设计(首次使用、搜索无结果、权限不足) +- 键盘快捷键(全局、表单) +- 无障碍规范(WCAG 2.1 AA) --- -## 设计系统 - -原型中使用的核心设计 tokens: +## 目录结构 -### 色彩系统 +``` +prototype/ +├── INDEX.md # 文档索引(本文档) +├── README.md # 设计系统总览 +│ +├── design-system-spec.md # 设计系统规范 ⭐ +├── component-library.md # 组件库规范 ⭐ +├── interaction-standards.md # 交互标准 ⭐ +│ +├── prototype.html # HTML 原型文件 +├── prototype-guide.md # 原型使用指南 +├── design-critique.md # 设计审视报告 +│ +├── design/ # 辅助设计文档 +│ ├── ui-recommendations.md +│ ├── responsive-guidelines.md +│ ├── accessibility-guidelines.md +│ └── design-optimization-report.md +│ +└── documentation/ # 参考文档 + ├── design-system-summary.md + └── component-mapping.md +``` -| 用途 | 变量 | 颜色 | -|------|------|------| -| 主背景 | `--bg-primary` | `#050505` | -| 次背景 | `--bg-secondary` | `#0f0f11` | -| 卡片背景 | `--bg-tertiary` | `#1a1a1f` | -| 主强调 | `--accent-primary` | `#10b981` (翡翠绿) | -| 次强调 | `--accent-secondary` | `#34d399` | -| 金色强调 | `--accent-gold` | `#fbbf24` | -| 主文本 | `--text-primary` | `#f8fafc` | -| 次文本 | `--text-secondary` | `#94a3b8` | +--- -### 圆角系统 +## 技术栈 -| 名称 | 值 | -|------|-----| -| `--radius-sm` | 8px | -| `--radius-md` | 12px | -| `--radius-lg` | 16px | -| `--radius-xl` | 20px | -| `--radius-2xl` | 28px | +### 前端技术 +- **React 18** + TypeScript +- **Next.js** (App Router) +- **Tailwind CSS v3** +- **shadcn/ui** + Radix UI +- **Framer Motion** +- **Lucide React** +- **Sonner** (Toast) -### 字体系统 +### 设计工具 +- **Figma** - 设计与协作 +- **CSS Variables** - Design Tokens +- **WCAG 2.1 AA** - 无障碍标准 -- **标题字体**: DM Serif Display -- **正文字体**: Plus Jakarta Sans -- **字重**: 300 (Light), 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold) +--- -### 动画系统 +## 设计系统版本 -| 动画名称 | 用途 | 时长 | -|---------|------|------| -| `fadeIn` | 淡入效果 | 0.5s | -| `slideUp` | 向上滑入 | 0.6s | -| `slideInLeft` | 向左滑入 | 0.6s | -| `slideInRight` | 向右滑入 | 0.6s | -| `scaleIn` | 缩放进入 | 0.5s | -| `float1/2/3` | 背景漂浮 | 20-30s | +| 版本 | 日期 | 关键变更 | +|-----|------|---------| +| v3.8.0 | 2026-06-17 | 新增品牌渐变、gradient 按钮变体、Utility Classes | +| v3.7.0 | 2026-06-14 | 完善交互标准、空状态设计 | +| v3.6.0 | 2026-06-08 | shadcn/ui 集成、组件库重构 | +| v3.5.0 | 2026-06-06 | 动效规范完善 | +| v3.0.0 | 2026-06-01 | 设计系统 v3 重构 | +| v2.0 | 2026-06-05 | 高保真原型规范化 | +| v1.0 | 2026-06-01 | 初始原型创建 | --- @@ -124,7 +133,8 @@ open prototype-high-fidelity.html - ✅ 所有交互元素支持键盘导航 - ✅ 使用 ARIA 属性增强语义化 - ✅ 支持 `prefers-reduced-motion` 用户偏好 -- ✅ 足够的色彩对比度(文本至少 4.5:1) +- ✅ 色彩对比度:正文 ≥ 4.5:1,大文本 ≥ 3:1 +- ✅ 触控目标最小 44×44px - ✅ 使用语义化 HTML 标签 --- @@ -132,29 +142,51 @@ open prototype-high-fidelity.html ## 响应式断点 | 断点 | 宽度 | 布局策略 | -|------|------|----------| -| Mobile | < 768px | 单列,底部导航栏 | -| Tablet | 768px - 1023px | 双列,侧边栏折叠 | -| Desktop | 1024px+ | 三列,完整功能 | +|-----|------|----------| +| base | < 640px | 单列,垂直布局 | +| sm | ≥ 640px | 双列网格基础 | +| md | ≥ 768px | 双列,侧边栏可展开 | +| lg | ≥ 1024px | 三列,完整导航 | +| xl | ≥ 1280px | 四列,最大内容宽度限制 | --- -## 版本历史 +## 快速开始 + +### 预览规范文档 + +1. 查看 [设计系统规范](./design-system-spec.md) - 基础设计规范 +2. 查看 [组件库规范](./component-library.md) - 组件使用指南 +3. 查看 [交互标准](./interaction-standards.md) - 交互设计指南 -| 版本 | 日期 | 说明 | -|------|------|------| -| v2.0 | 2026-06-05 | 规范化单一原型,移除冗余文件,增强无障碍和微交互 | -| v1.0 | 2026-06-01 | 初始高保真原型创建 | +### 运行应用 + +```bash +# 从项目根目录 +npm install +npm run dev +# 访问 http://localhost:3000 +``` + +### 预览 HTML 原型 + +```bash +cd prototype +python3 -m http.server 8080 +# 访问 http://localhost:8080/prototype.html +``` --- ## 相关资源 -- [项目 README](../README.md) - 项目概述和快速开始 -- [规范文档](../openspec/README.md) - 完整技术规范 +- [项目 README](../README.md) - 项目概述 +- [openspec 规范](../openspec/README.md) - 完整技术规范 +- [UI 设计系统](../openspec/design/ui-design-system.md) - 官方设计系统 - [变更日志](../CHANGELOG.md) - 版本历史 --- -**最后更新**: 2026-06-05 -**版本**: v2.0 +**文档路径**: `/prototype/INDEX.md` +**最后更新**: 2026-06-17 +**版本**: v3.8.0 diff --git a/prototype/README.md b/prototype/README.md index a85e40c..0a50cb2 100644 --- a/prototype/README.md +++ b/prototype/README.md @@ -1,142 +1,122 @@ -# Veloform 原型设计 +# Veloform 原型设计系统 -> **路径**: `/prototype/README.md` -> **版本**: v4.0.0 -> **更新日期**: 2024-06-08 +> **路径**: `/prototype/README.md` +> **版本**: v3.8.0 +> **更新日期**: 2026-06-17 -## 📁 目录结构 +## 设计理念 + +Veloform 设计系统是一套面向高端自行车配置场景的设计规范体系,融合了 **Apple 极简美学** 与 **现代科技感**。整体风格追求精致、克制、功能导向,同时通过精心设计的动效与渐变传递品牌的专业与创新精神。 + +### 核心美学方向 + +| 维度 | 选择 | 说明 | +|-----|------|------| +| **视觉基调** | Apple 极简 + 科技渐变 | 大量留白、精确间距、微妙阴影、品牌渐变点缀 | +| **色彩策略** | 中性灰底 + 蓝绿主色 | 营造高端、专业的工具型产品氛围 | +| **字体系统** | SF Pro 系列 + 等宽数字 | Apple 平台原生体验,数字清晰易读 | +| **动效哲学** | 即时响应 + 自然过渡 | 快速反馈、自然缓动、克制使用 | +| **组件风格** | shadcn/ui + Radix UI | 可访问性优先、样式与逻辑分离 | + +## 目录结构 ``` prototype/ -├── README.md # 原型设计总览(本文件) -├── prototype.html # ✨ 独立原型文件(可直接打开) -├── prototype-guide.md # 原型与设计系统说明 +├── README.md # 设计系统总览(本文档) +├── design-system-spec.md # 设计系统规范 v3.8.0 ⭐ +├── component-library.md # 组件库规范 v3.8.0 ⭐ +├── interaction-standards.md # 交互标准 v3.8.0 ⭐ +├── prototype-guide.md # 原型使用指南 ├── design-critique.md # 设计审视报告 │ -├── design/ # 设计文档 +├── design/ # 辅助设计文档 │ ├── ui-recommendations.md # UI/UX 优化建议 │ ├── responsive-guidelines.md # 响应式设计指南 -│ ├── accessibility-guidelines.md # 可访问性指南 -│ └── design-optimization-report.md # 世界级优化报告(新) +│ ├── accessibility-guidelines.md # 可访问性指南 +│ └── design-optimization-report.md # 世界级优化报告 │ -├── documentation/ # 文档资料 -│ ├── design-system-summary.md # 设计系统摘要 -│ └── component-mapping.md # 组件映射表 +├── documentation/ # 设计文档 +│ ├── design-system-summary.md # 设计系统摘要 +│ └── component-mapping.md # 组件映射表 │ -└── archive/ # 已废弃的原型文件 +└── archive/ # 已归档文件 └── README.md ``` -## 🎯 概述 - -本目录包含 Veloform 项目的原型设计和 UI/UX 优化建议。我们提供两种原型方式: - -### 1. **HTML 原型** (`prototype.html`) - - ✅ 独立完整的原型文件 - - ✅ 直接在浏览器打开即可体验 - - ✅ 包含所有交互功能 - - ✅ 响应式设计,适配桌面/移动端 - -### 2. **Next.js 应用** (`/src/`) - - ✅ 实际的生产级应用 - - ✅ 完整的功能实现 - - ✅ 位于项目根目录的 `src/` 文件夹 - -## 🚀 快速开始 +## 规范文档 -### 查看 HTML 原型 +### 核心规范(v3.8.0) -1. 直接在浏览器中打开 `prototype/prototype.html` 文件 -2. 或者使用本地服务器: - ```bash - cd prototype - python3 -m http.server 8000 - # 然后访问 http://localhost:8000/prototype.html - ``` +| 文档 | 内容 | 状态 | +|-----|------|------| +| [设计系统规范](./design-system-spec.md) | 色彩、字体、间距、图标、动效、Utility Classes | ✅ v3.8.0 | +| [组件库规范](./component-library.md) | 基础/复合/业务组件、使用规则、shadcn/ui 来源 | ✅ v3.8.0 | +| [交互标准](./interaction-standards.md) | 交互模式库、反馈规范、错误处理、空状态设计 | ✅ v3.8.0 | -### 查看 Next.js 应用 +### 快速导航 -```bash -# 从项目根目录 -npm install -npm run dev -# 访问 http://localhost:3000 -``` - -## 📚 文档导航 - -### 原型相关 -- [原型说明](./prototype-guide.md) - 详细了解原型设计理念 -- [设计系统摘要](./documentation/design-system-summary.md) - 设计规范和令牌 -- [组件映射表](./documentation/component-mapping.md) - 原型与实际代码对照 - -### UI/UX 优化 -- [世界级优化报告](./design/design-optimization-report.md) - 国际顶级设计师视角的优化建议 -- [UI/UX 优化建议](./design/ui-recommendations.md) - 详细的改进建议 +- [原型说明](./prototype-guide.md) - 原型设计理念与使用方法 +- [UI/UX 优化建议](./design/ui-recommendations.md) - 详细改进方向 - [响应式设计指南](./design/responsive-guidelines.md) - 多设备适配策略 - [可访问性指南](./design/accessibility-guidelines.md) - WCAG 标准实现 +- [设计系统摘要](./documentation/design-system-summary.md) - 规范速查 +- [组件映射表](./documentation/component-mapping.md) - 原型与代码对照 -### 项目规范 -- [项目规范](../openspec/README.md) - 完整的项目规范文档 -- [UI 设计系统](../openspec/design/ui-design-system.md) - 官方设计系统 -- [设计审查](../openspec/design/design-review.md) - 设计质量评估 - -## ✨ 新特性 (v4.0) +## 设计系统版本 -1. **世界级设计标准** - - 采用 Awwwards 级别的视觉效果 - - 精致的微交互动画 - - 现代渐变和视觉层次 +| 版本 | 日期 | 变更内容 | +|-----|------|---------| +| **v3.8.0** | 2026-06-17 | 新增品牌渐变 token、gradient-shift 动画、gradient 按钮变体、Utility Classes | +| **v3.7.0** | 2026-06-14 | 完善交互标准、空状态设计、键盘快捷键规范 | +| **v3.6.0** | 2026-06-08 | 组件库规范重构、shadcn/ui 集成 | +| **v3.5.0** | 2026-06-06 | 动效规范完善、Framer Motion 预设 | +| **v3.0.0** | 2026-06-01 | 设计系统 v3 重构 | -2. **完整功能实现** - - 车型选择器集成 - - 组件配置系统 - - 配置对比面板 - - 分享和导出功能 +## 技术栈 -3. **完美的响应式体验** - - 桌面端优化布局 - - 移动端友好触控 - - 平板适配 +### 前端框架 +- **React 18** + TypeScript +- **Next.js** (App Router) +- **Tailwind CSS v3** - 样式系统 +- **shadcn/ui** + Radix UI - 组件库 +- **Framer Motion** - 动画库 +- **Lucide React** - 图标库 -4. **用户体验优化** - - 流畅的过渡动画 - - 直观的操作反馈 - - 键盘快捷键支持 +### 设计工具 +- **Figma** - 设计与协作 +- **CSS Variables** - 设计 token +- **WCAG 2.1 AA** - 无障碍标准 -## 🎨 设计理念 +## 设计原则 -### 视觉风格 -- **主题**: 深色模式 + 紫色渐变 -- **字体**: Poppins(标题)+ Inter(正文) -- **风格**: 现代、精致、科技感 -- **层次**: 清晰的视觉层级和空间感 +### 1. 即时响应 (Responsive) +用户操作后 **100ms** 内给予视觉反馈,让用户感知系统的灵敏与可控。 -### 交互原则 -- **即时反馈**: 每个操作都有明确的视觉响应 -- **渐进式引导**: 让用户自然地学习和使用 -- **容错设计**: 提供简单的撤销和重做机制 +### 2. 自然过渡 (Natural) +使用 `cubic-bezier(0.4, 0, 0.2, 1)` 缓动曲线,模拟物理运动规律,避免突兀的线性动画。 -## 🔗 快速链接 +### 3. 适度克制 (Restrained) +动画时长 **≤ 400ms**,避免分散用户注意力。装饰性动画在 `prefers-reduced-motion` 下禁用。 -- [查看原型文件](./prototype.html) - 立即体验 HTML 原型 -- [设计优化报告](./design/design-optimization-report.md) - 世界级优化建议 -- [原型与实际代码映射](./documentation/component-mapping.md) - 开发参考 +## 无障碍承诺 -## 📋 更新日志 +所有组件遵循 WCAG 2.1 AA 标准: +- ✅ 色彩对比度:正文 ≥ 4.5:1,大文本 ≥ 3:1 +- ✅ 键盘导航:所有交互元素可 Tab 聚焦 +- ✅ ARIA 属性:语义化标签与屏幕阅读器支持 +- ✅ 触控目标:最小 44×44px +- ✅ 减少动效:尊重用户 `prefers-reduced-motion` 设置 -### v4.0.0 (2024-06-08) -- ✨ 全新创建的世界级设计原型 -- 🎨 优化的视觉效果和动画 -- 📱 完善的移动端体验 -- 📚 添加国际顶级设计师视角的优化报告 +## 相关资源 -### v3.6.0 (2026-06-06) -- 原版本的更新内容... +- [项目 README](../README.md) - 项目概述 +- [openspec 规范](../openspec/README.md) - 完整技术规范 +- [UI 设计系统](../openspec/design/ui-design-system.md) - 官方设计系统 +- [变更日志](../CHANGELOG.md) - 版本历史 --- -**文档路径**: `/prototype/README.md` -**最后更新**: 2024-06-08 -**版本**: v4.0.0 +**文档路径**: `/prototype/README.md` +**最后更新**: 2026-06-17 +**版本**: v3.8.0 diff --git a/prototype/component-library.md b/prototype/component-library.md index d3741da..4e43a79 100644 --- a/prototype/component-library.md +++ b/prototype/component-library.md @@ -6,6 +6,21 @@ --- +## 设计理念 + +组件库采用 **shadcn/ui** 哲学——组件作为**源代码**而非依赖包被添加到项目中,允许开发者完全控制样式和逻辑。这确保了设计与实现的一致性,同时保留了最大程度的定制灵活性。 + +### 组件设计原则 + +| 原则 | 描述 | 实践 | +|-----|------|------| +| **可访问性优先** | 每个组件内置 ARIA 支持 | Radix UI 底层、语义化标签 | +| **变体驱动** | 通过 variant/size 控制样式 | Tailwind CSS 工具类 | +| **组合优于继承** | 组件可嵌套组合 | Props 传递children | +| **样式逻辑分离** | 样式与行为解耦 | cn() 工具类合并 | + +--- + ## 1. 组件分类体系 | 层级 | 名称 | 定义 | 典型组件 | @@ -34,6 +49,8 @@ L1 基础组件(Button / Input / Card / Modal / Badge 等)基于 **shadcn/ui ### 2.1 按钮 (Button) +> **设计说明**:按钮是用户执行操作的主要方式。通过变体区分操作优先级,通过尺寸适应不同容器。 + #### 变体 (Variants) | 变体 | 视觉特征 | 语义 | 使用场景 | @@ -46,6 +63,21 @@ L1 基础组件(Button / Input / Card / Modal / Badge 等)基于 **shadcn/ui | `danger` | 填充 `--error`,白字 | 破坏性操作 | 删除、移除、取消订阅 | | `gradient` | 渐变背景 `#0071E3 → #34C759 → #AF52DE`,白字,glow shadow | 品牌强调 | Hero CTA、特殊行动号召 | +**变体可视化**: + +``` +Button Variants +═══════════════════════════════════════════════════════════════════ +primary ████████████████████████ 品牌蓝填充,白字 +secondary ░░░░░░░░░░░░░░░░░░░░░░░ 灰色填充,深色字 +accent ████████████████████████ 绿色填充,白字 +outline ░░░░░░░░░░░░░░░░░░░░░░░ 透明底,品牌蓝边框 +ghost ░░░░░░░░░░░░░░░░░░░░░░░ 完全透明,仅文字 +danger ████████████████████████ 红色填充,白字 +gradient ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 蓝→绿→紫渐变,白字 +═══════════════════════════════════════════════════════════════════ +``` + #### 尺寸 (Sizes) | 尺寸 | Padding | 最小高度 | 字号 | 图标尺寸 | @@ -66,11 +98,25 @@ L1 基础组件(Button / Input / Card / Modal / Badge 等)基于 **shadcn/ui | Disabled | `opacity-40`,`cursor-not-allowed`,禁 hover | — | | Loading | 显示 spinner,按钮文字隐藏 | — | +**状态可视化**: + +``` +Button States +═══════════════════════════════════════════════════════════ +Default ████████████████████████ +Hover ████████████████████████ ↑y=-1 阴影加深 +Pressed ████████░░░░░░░░░░░░░░░░ scale=0.97 +Focus ████████████████████████ ◯ ring +Disabled ████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒ opacity=40% +Loading ████████████████████████ ⟳ spinner +═══════════════════════════════════════════════════════════ +``` + #### 代码示例 ```tsx import { Button } from '@/components/ui/Button'; -import { Bike, Loader2 } from 'lucide-react'; +import { Bike, Loader2, Plus, X } from 'lucide-react'; // 主操作按钮 +// 渐变按钮 (Hero CTA) + + +// 次要操作按钮 + + +// 危险操作按钮 + + // 加载状态 + + + @@ -202,4 +212,59 @@ + + + + + + + {{selectedSpecData.categoryIcon}} + + {{selectedSpecData.categoryName}} + {{selectedSpecData.name}} + {{selectedSpecData.brand}} · {{selectedSpecData.model}} + + {{selectedSpecData.description}} + + + + + + + + {{selectedSpecData.priceFormatted}} + 价格 + + + {{selectedSpecData.weightFormatted}} + 重量 + + + + + + 详细规格 + + + + {{item.key}} + {{item.value}} + + + + + + + + 关闭 + + 删除组件 + + + + diff --git a/miniprogram/pages/configurator/configurator.wxss b/miniprogram/pages/configurator/configurator.wxss index a7012a5..156714e 100644 --- a/miniprogram/pages/configurator/configurator.wxss +++ b/miniprogram/pages/configurator/configurator.wxss @@ -360,6 +360,20 @@ margin-left: 16rpx; } +.component-view-spec { + width: 48rpx; + height: 48rpx; + border-radius: 50%; + background: rgba(0, 113, 227, 0.1); + color: var(--primary); + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; + flex-shrink: 0; + margin-left: 8rpx; +} + /* ========== 完整组件列表 ========== */ .components-section { padding: 0 32rpx; @@ -584,3 +598,147 @@ font-size: 24rpx; color: var(--muted); } + +/* ========== 组件规格详情 ========== */ +.component-spec-modal .modal-content { + max-height: 80vh; +} + +.spec-header { + display: flex; + align-items: flex-start; + gap: 24rpx; + padding: 32rpx; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.08), rgba(52, 199, 89, 0.05)); + border-bottom: 1rpx solid var(--border-light); +} + +.spec-icon { + font-size: 80rpx; + width: 120rpx; + height: 120rpx; + border-radius: 28rpx; + background: var(--surface-secondary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.spec-header-info { + flex: 1; + min-width: 0; +} + +.spec-category { + display: inline-block; + padding: 8rpx 16rpx; + background: rgba(0, 113, 227, 0.1); + color: var(--primary); + border-radius: 12rpx; + font-size: 22rpx; + font-weight: 500; + margin-bottom: 12rpx; +} + +.spec-name { + font-size: 36rpx; + font-weight: 700; + color: var(--foreground); + margin-bottom: 8rpx; + letter-spacing: -0.01em; +} + +.spec-brand { + font-size: 26rpx; + color: var(--secondary); +} + +.spec-description { + font-size: 28rpx; + color: var(--muted); + line-height: 1.6; + margin-top: 12rpx; +} + +.spec-stats-row { + display: flex; + gap: 32rpx; + padding: 28rpx 32rpx; + background: var(--surface-secondary); + border-bottom: 1rpx solid var(--border-light); +} + +.spec-stat-item { + flex: 1; + text-align: center; +} + +.spec-stat-value { + font-size: 38rpx; + font-weight: 700; + color: var(--primary); + margin-bottom: 8rpx; +} + +.spec-stat-value.price { + color: var(--primary); +} + +.spec-stat-value.weight { + color: var(--secondary); +} + +.spec-stat-label { + font-size: 22rpx; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.spec-details-section { + padding: 24rpx 32rpx; +} + +.spec-section-title { + font-size: 28rpx; + font-weight: 600; + color: var(--foreground); + margin-bottom: 20rpx; +} + +.spec-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16rpx; +} + +.spec-item { + background: var(--surface); + border-radius: 16rpx; + padding: 20rpx; + border: 1rpx solid var(--border-light); +} + +.spec-item-key { + font-size: 22rpx; + color: var(--muted); + margin-bottom: 8rpx; +} + +.spec-item-value { + font-size: 28rpx; + font-weight: 600; + color: var(--foreground); +} + +.spec-actions { + padding: 24rpx 32rpx; + display: flex; + gap: 16rpx; + border-top: 1rpx solid var(--border-light); +} + +.spec-actions .btn { + flex: 1; +} diff --git a/miniprogram/pages/detail/detail.js b/miniprogram/pages/detail/detail.js index 99f3bbb..c554d87 100644 --- a/miniprogram/pages/detail/detail.js +++ b/miniprogram/pages/detail/detail.js @@ -29,9 +29,10 @@ Page({ onLoad(options) { const configId = options.id; const type = options.type; + const data = options.data; if (type === 'recommended') { - this.loadRecommendedConfig(configId); + this.loadRecommendedConfig(configId, data); } else { this.loadConfig(configId); } @@ -59,8 +60,20 @@ Page({ this.processConfig(config); }, - loadRecommendedConfig(configId) { - const recommended = RECOMMENDED_CONFIGS.find(r => r.id === configId); + loadRecommendedConfig(configId, data) { + let recommended; + + // 如果有传递的完整数据,直接使用 + if (data) { + try { + recommended = JSON.parse(decodeURIComponent(data)); + } catch (e) { + // 解析失败,从数据中查找 + recommended = RECOMMENDED_CONFIGS.find(r => r.id === configId); + } + } else { + recommended = RECOMMENDED_CONFIGS.find(r => r.id === configId); + } if (!recommended) { showToast('推荐配置不存在'); @@ -74,6 +87,16 @@ Page({ const bikeTypeName = bikeTypeInfo ? bikeTypeInfo.name : '自行车'; const bikeTypeIcon = bikeTypeInfo ? bikeTypeInfo.icon : '🚲'; + // 处理组件数据 + let processedComponents = []; + if (recommended.components && recommended.components.length > 0) { + processedComponents = recommended.components.map(comp => ({ + ...comp, + priceFormatted: formatPrice(comp.price), + weightFormatted: formatWeight(comp.weight) + })); + } + this.setData({ config: { ...recommended, @@ -83,10 +106,10 @@ Page({ isRecommended: true, bikeTypeName, bikeTypeIcon, - componentCount: recommended.componentCount, + componentCount: recommended.componentCount || (recommended.components ? recommended.components.length : 0), totalPrice: formatPrice(recommended.totalPrice), totalWeight: formatWeight(recommended.totalWeight), - components: [] + components: processedComponents }); }, @@ -137,10 +160,30 @@ Page({ cancelText: '取消' }).then(confirmed => { if (confirmed) { + const { config, components } = this.data; const newConfig = configStore.createConfiguration( - this.data.config.bikeType, - this.data.config.name + config.bikeType, + config.name ); + + // 如果有组件数据,添加到配置中 + if (components && components.length > 0) { + components.forEach(comp => { + configStore.addComponent(newConfig.id, { + id: comp.id, + category: comp.category, + categoryName: comp.categoryName, + categoryIcon: comp.categoryIcon, + name: comp.name, + brand: comp.brand, + model: comp.model, + price: comp.price, + weight: comp.weight, + description: comp.description + }); + }); + } + showToast('配置已创建', 'success'); setTimeout(() => { wx.redirectTo({ diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index 594e6cc..751e99a 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -82,23 +82,44 @@ Page({ const rec = RECOMMENDED_CONFIGS.find(r => r.id === id); if (rec) { - showModal(rec.name, `加载推荐配置「${rec.name}」?\n将使用默认组件创建该配置。`, { + showModal(rec.name, `加载推荐配置「${rec.name}」?\n${rec.description}`, { confirmText: '使用此配置', cancelText: '查看详情' }).then(confirmed => { if (confirmed) { + // 使用推荐配置的组件 const config = configStore.createConfiguration(rec.bikeType, rec.name); - const defaults = getDefaultsForType(rec.bikeType); - defaults.forEach(comp => { - configStore.addComponent(config.id, comp); - }); + if (rec.components && rec.components.length > 0) { + rec.components.forEach(comp => { + configStore.addComponent(config.id, { + id: comp.id, + category: comp.category, + categoryName: comp.categoryName, + categoryIcon: comp.categoryIcon, + name: comp.name, + brand: comp.brand, + model: comp.model, + price: comp.price, + weight: comp.weight, + description: comp.description + }); + }); + } showToast('配置已创建'); wx.navigateTo({ url: `/pages/configurator/configurator?id=${config.id}` }); } else { + // 导航到详情页,传递完整推荐配置信息 + const recData = { + ...rec, + bikeTypeName: this.getBikeTypeName(rec.bikeType), + bikeTypeIcon: this.getBikeTypeIcon(rec.bikeType), + totalPriceFormatted: formatPrice(rec.totalPrice), + totalWeightFormatted: formatWeight(rec.totalWeight) + }; wx.navigateTo({ - url: `/pages/detail/detail?id=${id}&type=recommended` + url: `/pages/detail/detail?id=${id}&type=recommended&data=${encodeURIComponent(JSON.stringify(recData))}` }); } }); diff --git a/miniprogram/pages/index/index.json b/miniprogram/pages/index/index.json index 046153b..49135d3 100644 --- a/miniprogram/pages/index/index.json +++ b/miniprogram/pages/index/index.json @@ -4,5 +4,7 @@ "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f7", "enablePullDownRefresh": false, - "usingComponents": {} + "usingComponents": { + "tabbar": "/components/tabbar/tabbar" + } } diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index 588e333..b87e152 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -110,4 +110,7 @@ {{appInfo.tagline}} + + + diff --git a/miniprogram/pages/library/library.js b/miniprogram/pages/library/library.js index adab4bd..e0a7e6d 100644 --- a/miniprogram/pages/library/library.js +++ b/miniprogram/pages/library/library.js @@ -15,11 +15,14 @@ const { Page({ data: { appInfo: APP_INFO, + bikeTypes: BIKE_TYPES, configurations: [], + filteredConfigurations: [], totalConfigs: 0, totalPrice: '¥0', isEmpty: true, - filter: 'all' + filter: 'all', + selectedFilter: 'all' }, onLoad() { @@ -36,6 +39,7 @@ Page({ if (!configs || configs.length === 0) { this.setData({ configurations: [], + filteredConfigurations: [], totalConfigs: 0, totalPrice: '¥0', isEmpty: true @@ -95,14 +99,30 @@ Page({ 0 ); + // 应用筛选 + const { selectedFilter } = this.data; + const filtered = selectedFilter === 'all' + ? processedConfigs + : processedConfigs.filter(c => c.bikeType === selectedFilter); + this.setData({ configurations: processedConfigs, - totalConfigs: processedConfigs.length, - totalPrice: formatPrice(totalConfigPrice), - isEmpty: false + filteredConfigurations: filtered, + totalConfigs: filtered.length, + totalPrice: formatPrice(filtered.reduce((sum, c) => { + const price = c.components.reduce((s, comp) => s + (comp.price || 0), 0); + return sum + price; + }, 0)), + isEmpty: filtered.length === 0 }); }, + onFilterChange(e) { + const filter = e.currentTarget.dataset.filter; + this.setData({ selectedFilter: filter }); + this.loadConfigurations(); + }, + onViewConfig(e) { const { id } = e.currentTarget.dataset; configStore.setCurrentConfig(id); diff --git a/miniprogram/pages/library/library.json b/miniprogram/pages/library/library.json index aec74b0..75c60ff 100644 --- a/miniprogram/pages/library/library.json +++ b/miniprogram/pages/library/library.json @@ -4,5 +4,7 @@ "navigationBarTextStyle": "black", "backgroundColor": "#f5f5f7", "enablePullDownRefresh": true, - "usingComponents": {} + "usingComponents": { + "tabbar": "/components/tabbar/tabbar" + } } diff --git a/miniprogram/pages/library/library.wxml b/miniprogram/pages/library/library.wxml index e9f98c1..10d559f 100644 --- a/miniprogram/pages/library/library.wxml +++ b/miniprogram/pages/library/library.wxml @@ -16,6 +16,28 @@ 总价值 + + + + + + 全部 + + + + {{item.icon}} {{item.name}} + + + + @@ -30,7 +52,7 @@ - + {{appInfo.name}} Version {{appInfo.version}} + + + diff --git a/miniprogram/pages/library/library.wxss b/miniprogram/pages/library/library.wxss index 4831aeb..9ca7014 100644 --- a/miniprogram/pages/library/library.wxss +++ b/miniprogram/pages/library/library.wxss @@ -71,6 +71,46 @@ margin: 0 24rpx; } +/* ========== 筛选器 ========== */ +.filter-section { + margin-top: 32rpx; +} + +.filter-tabs { + display: flex; + gap: 12rpx; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + padding-bottom: 8rpx; +} + +.filter-tabs::-webkit-scrollbar { + display: none; +} + +.filter-tab { + flex-shrink: 0; + padding: 16rpx 28rpx; + background: var(--surface); + border-radius: 9999rpx; + border: 1rpx solid var(--border-light); + font-size: 26rpx; + color: var(--secondary); + transition: all 0.25s ease; +} + +.filter-tab:active { + transform: scale(0.95); +} + +.filter-tab.active { + background: linear-gradient(90deg, #0071e3, #34c759); + color: #ffffff; + border-color: transparent; + box-shadow: 0 4rpx 12rpx rgba(0, 113, 227, 0.25); +} + /* ========== 配置列表 ========== */ .config-list { padding: 32rpx; From d52ad77cb7d647a70592fc0a784e838827926639 Mon Sep 17 00:00:00 2001 From: AjaCHN Date: Thu, 18 Jun 2026 07:08:55 +0000 Subject: [PATCH 05/16] =?UTF-8?q?feat:=20=E5=88=86=E6=9E=90=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=A1=B5=E9=9D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- miniprogram/data/components.js | 111 ++++++++++++++ miniprogram/utils/constants.js | 159 +++++++++++++++++-- miniprogram/utils/store.js | 273 ++++++++++++++++++++++++++++----- miniprogram/utils/util.js | 146 +++++++++++++++++- 4 files changed, 640 insertions(+), 49 deletions(-) diff --git a/miniprogram/data/components.js b/miniprogram/data/components.js index 0dc0cb1..3b3895a 100644 --- a/miniprogram/data/components.js +++ b/miniprogram/data/components.js @@ -1,6 +1,23 @@ // data/components.js - 自行车组件数据 const ROAD_DEFAULTS = [ + { + id: 'road-frame-1', + category: 'Frame', + bikeType: 'Road', + name: 'Specialized Tarmac SL8 Comp', + brand: 'Specialized', + model: 'Tarmac SL8', + price: 8500, + weight: 800, + description: '全新Tarmac SL8,极致轻量气动设计', + specs: { + material: 'FACT 10r Carbon', + geometry: 'Race', + wheelSize: '700c', + brakeType: 'Disc' + } + }, { id: 'road-drivetrain-1', category: 'Drivetrain', @@ -10,6 +27,7 @@ const ROAD_DEFAULTS = [ model: 'Dura-Ace Di2 R9200', price: 4200, weight: 2430, + description: '顶级电子变速,精准快速', specs: { speeds: 12, cassetteRange: '11-30T', @@ -26,6 +44,7 @@ const ROAD_DEFAULTS = [ model: 'Rapide CLX II', price: 2800, weight: 1520, + description: '碳纤轮组,高性能气动轮', specs: { rimDepth: '51mm', rimWidth: '21mm internal', @@ -41,6 +60,7 @@ const ROAD_DEFAULTS = [ model: 'Rapide Cockpit', price: 600, weight: 310, + description: '一体式气动座舱', specs: { handlebarWidth: '420mm', stemLength: '110mm', @@ -56,16 +76,50 @@ const ROAD_DEFAULTS = [ model: 'S-Works Turbo', price: 180, weight: 480, + description: '棉质胎体,极致抓地力', specs: { size: '700x28c', compound: 'GRIPTON', tpi: 120, tubeless: true } + }, + { + id: 'road-saddle-1', + category: 'Saddle', + bikeType: 'Road', + name: 'Specialized Power Expert', + brand: 'Specialized', + model: 'Power Expert', + price: 220, + weight: 195, + description: '碳轨座垫,轻量舒适', + specs: { + width: '143mm', + rails: 'Carbon', + padding: 'Body Geometry' + } } ]; const MTB_DEFAULTS = [ + { + id: 'mtb-frame-1', + category: 'Frame', + bikeType: 'MTB', + name: 'Specialized Stumpjumper EVO Comp', + brand: 'Specialized', + model: 'Stumpjumper EVO', + price: 6500, + weight: 2100, + description: '150mm行程全地形越野车架', + specs: { + material: 'M5 Aluminum', + geometry: 'Trail', + wheelSize: '29er', + travel: '150mm' + } + }, { id: 'mtb-drivetrain-1', category: 'Drivetrain', @@ -75,6 +129,7 @@ const MTB_DEFAULTS = [ model: 'XTR Di2 M9100', price: 3800, weight: 1920, + description: '顶级山地电子变速,即时响应', specs: { speeds: 12, cassetteRange: '10-51T', @@ -90,6 +145,7 @@ const MTB_DEFAULTS = [ model: '34 Float Factory', price: 1050, weight: 1738, + description: '140mm行程,四向可调阻尼', specs: { travel: '140mm', damping: 'GRIP2', @@ -105,12 +161,29 @@ const MTB_DEFAULTS = [ model: 'Traverse SL II', price: 1500, weight: 1720, + description: '29寸碳纤轮组,耐冲击设计', specs: { rimDepth: '25mm', rimWidth: '30mm internal', material: 'carbon' } }, + { + id: 'mtb-cockpit-1', + category: 'Cockpit', + bikeType: 'MTB', + name: 'Roval Traverse Cockpit', + brand: 'Roval', + model: 'Traverse Cockpit', + price: 450, + weight: 380, + description: '山地专用铝合金座舱', + specs: { + handlebarWidth: '780mm', + stemLength: '50mm', + rise: '20mm' + } + }, { id: 'mtb-tires-1', category: 'Tires', @@ -120,12 +193,29 @@ const MTB_DEFAULTS = [ model: 'Minion DHR II', price: 140, weight: 790, + description: '29x2.4寸,山地越野专用', specs: { size: '29x2.4', compound: '3C MaxxTerra', tpi: 60, tubeless: true } + }, + { + id: 'mtb-saddle-1', + category: 'Saddle', + bikeType: 'MTB', + name: 'Specialized Bridge Comp', + brand: 'Specialized', + model: 'Bridge Comp', + price: 200, + weight: 340, + description: '山地专用座垫,耐磨耐用', + specs: { + width: '143mm', + rails: 'Cr-Mo', + padding: 'Body Geometry' + } } ]; @@ -139,6 +229,7 @@ const FOLD_DEFAULTS = [ model: 'Superlight Frame', price: 1400, weight: 1800, + description: '轻量化钢架,三步折叠设计', specs: { material: 'steel', geometry: 'compact', @@ -154,6 +245,7 @@ const FOLD_DEFAULTS = [ model: 'Alfine Di2 SG-S7051', price: 1200, weight: 1765, + description: '11速内变速,低维护保养', specs: { speeds: 11, cassetteRange: 'internal gear hub', @@ -169,6 +261,7 @@ const FOLD_DEFAULTS = [ model: 'Superlight Wheelset', price: 450, weight: 1240, + description: '16寸轻量轮组,铝合金材质', specs: { rimDepth: 'standard', rimWidth: '19mm', @@ -184,6 +277,7 @@ const FOLD_DEFAULTS = [ model: 'M-Type Handlebar', price: 120, weight: 310, + description: '经典M把,人体工学设计', specs: { handlebarWidth: '540mm', stemLength: 'integrated' @@ -198,12 +292,29 @@ const FOLD_DEFAULTS = [ model: 'Marathon Racer', price: 65, weight: 370, + description: '轻量通勤胎,低滚阻设计', specs: { size: '16x1-1/3', compound: 'RaceGuard', tpi: 67, tubeless: false } + }, + { + id: 'fold-saddle-1', + category: 'Saddle', + bikeType: 'Fold', + name: 'Brompton Aerolite Saddle', + brand: 'Brompton', + model: 'Aerolite', + price: 280, + weight: 280, + description: '轻量座垫,适合城市骑行', + specs: { + width: '140mm', + rails: 'Titanium', + padding: 'Minimal' + } } ]; diff --git a/miniprogram/utils/constants.js b/miniprogram/utils/constants.js index 147e50d..a17c5cd 100644 --- a/miniprogram/utils/constants.js +++ b/miniprogram/utils/constants.js @@ -4,7 +4,9 @@ const APP_INFO = { name: 'Veloform', version: '3.8.0', tagline: 'Bike Configurator', - description: '打造你的梦想自行车,从车型选择到组件配置,每一个细节由你掌控' + description: '打造你的梦想自行车,从车型选择到组件配置,每一个细节由你掌控', + author: 'Veloform Team', + supportEmail: 'support@veloform.com' }; const BIKE_TYPES = [ @@ -15,7 +17,9 @@ const BIKE_TYPES = [ icon: '🚴', baseWeight: 900, gradientFrom: '#0071e3', - gradientTo: '#af52de' + gradientTo: '#af52de', + recommendedBudget: { min: 8000, max: 50000 }, + suitableFor: ['竞速', '长途骑行', '健身训练'] }, { type: 'MTB', @@ -24,7 +28,9 @@ const BIKE_TYPES = [ icon: '🚵', baseWeight: 1800, gradientFrom: '#34c759', - gradientTo: '#0071e3' + gradientTo: '#0071e3', + recommendedBudget: { min: 10000, max: 40000 }, + suitableFor: ['山地越野', '林道骑行', '户外探险'] }, { type: 'Fold', @@ -33,26 +39,142 @@ const BIKE_TYPES = [ icon: '🚲', baseWeight: 2000, gradientFrom: '#ff9500', - gradientTo: '#34c759' + gradientTo: '#34c759', + recommendedBudget: { min: 3000, max: 20000 }, + suitableFor: ['城市通勤', '地铁出行', '便携旅行'] } ]; const COMPONENT_CATEGORIES = [ - { key: 'Frame', name: '车架', icon: '🏗️' }, - { key: 'Drivetrain', name: '传动系统', icon: '⚙️' }, - { key: 'Wheelset', name: '轮组', icon: '🔘' }, - { key: 'Suspension', name: '避震系统', icon: '🔧' }, - { key: 'Cockpit', name: '操控系统', icon: '🎯' }, - { key: 'Tires', name: '轮胎', icon: '⚪' } + { key: 'Frame', name: '车架', icon: '🏗️', required: true }, + { key: 'Drivetrain', name: '传动系统', icon: '⚙️', required: true }, + { key: 'Wheelset', name: '轮组', icon: '🔘', required: true }, + { key: 'Suspension', name: '避震系统', icon: '🔧', required: false }, + { key: 'Cockpit', name: '操控系统', icon: '🎯', required: true }, + { key: 'Tires', name: '轮胎', icon: '⚪', required: true }, + { key: 'Saddle', name: '座垫', icon: '🪑', required: false }, + { key: 'Handlebar', name: '车把', icon: '🔧', required: false }, + { key: 'Stem', name: '把立', icon: '🔩', required: false } ]; const CURRENCY = '¥'; +const CURRENCY_CODE = 'CNY'; +const WEIGHT_UNIT = 'g'; +const WEIGHT_UNIT_FULL = '克'; const STORAGE_KEYS = { CONFIGURATIONS: 'veloform_configurations', CURRENT_CONFIG: 'veloform_current_config', USER_INFO: 'veloform_user_info', - SETTINGS: 'veloform_settings' + SETTINGS: 'veloform_settings', + COMPARE_LIST: 'veloform_compare_list', + LAST_VISITED: 'veloform_last_visited' +}; + +const COLORS = { + primary: '#0071e3', + primaryDark: '#0077ed', + primaryLight: '#338aff', + secondary: '#14b8a6', + success: '#34c759', + warning: '#ff9500', + error: '#ff3b30', + info: '#5856d6', + background: '#ffffff', + backgroundSecondary: '#f5f5f7', + foreground: '#1d1d1f', + foregroundSecondary: '#86868b', + border: '#e5e5ea', + text: '#1d1d1f', + textSecondary: '#86868b', + textTertiary: '#c7c7cc', + card: '#ffffff', + cardHover: '#f5f5f7' +}; + +const SPACING = { + xs: '8rpx', + sm: '16rpx', + md: '24rpx', + lg: '32rpx', + xl: '48rpx', + '2xl': '64rpx', + '3xl': '96rpx' +}; + +const FONT_SIZES = { + xs: '22rpx', + sm: '24rpx', + base: '28rpx', + md: '32rpx', + lg: '36rpx', + xl: '40rpx', + '2xl': '48rpx', + '3xl': '56rpx', + '4xl': '64rpx' +}; + +const FONT_WEIGHTS = { + normal: '400', + medium: '500', + semibold: '600', + bold: '700' +}; + +const BORDERS = { + radius: { + sm: '8rpx', + md: '12rpx', + lg: '16rpx', + xl: '24rpx', + full: '9999rpx' + }, + width: { + none: '0', + thin: '1rpx', + normal: '2rpx', + thick: '4rpx' + } +}; + +const SHADOWS = { + none: 'none', + sm: '0 2rpx 4rpx rgba(0, 0, 0, 0.05)', + md: '0 4rpx 12rpx rgba(0, 0, 0, 0.08)', + lg: '0 8rpx 24rpx rgba(0, 0, 0, 0.12)', + xl: '0 16rpx 48rpx rgba(0, 0, 0, 0.16)' +}; + +const ANIMATIONS = { + duration: { + fast: '150ms', + normal: '200ms', + slow: '300ms', + slowest: '500ms' + }, + easing: { + ease: 'ease', + easeIn: 'ease-in', + easeOut: 'ease-out', + easeInOut: 'ease-in-out', + bounce: 'cubic-bezier(0.34, 1.56, 0.64, 1)' + } +}; + +const COMPARE_LIMIT = 3; + +const PRICE_RANGES = [ + { label: '入门', min: 0, max: 10000, color: '#34c759' }, + { label: '进阶', min: 10000, max: 25000, color: '#0071e3' }, + { label: '高端', min: 25000, max: 40000, color: '#af52de' }, + { label: '顶级', min: 40000, max: Infinity, color: '#ff9500' } +]; + +const TAGS = { + hot: { label: '热门', color: '#ff3b30', bgColor: '#fff0f0' }, + recommended: { label: '推荐', color: '#0071e3', bgColor: '#e8f4ff' }, + premium: { label: '高端', color: '#af52de', bgColor: '#f3e8ff' }, + budget: { label: '高性价比', color: '#34c759', bgColor: '#f0fff4' } }; module.exports = { @@ -60,5 +182,18 @@ module.exports = { BIKE_TYPES, COMPONENT_CATEGORIES, CURRENCY, - STORAGE_KEYS + CURRENCY_CODE, + WEIGHT_UNIT, + WEIGHT_UNIT_FULL, + STORAGE_KEYS, + COLORS, + SPACING, + FONT_SIZES, + FONT_WEIGHTS, + BORDERS, + SHADOWS, + ANIMATIONS, + COMPARE_LIMIT, + PRICE_RANGES, + TAGS }; diff --git a/miniprogram/utils/store.js b/miniprogram/utils/store.js index 3316d12..021fb2c 100644 --- a/miniprogram/utils/store.js +++ b/miniprogram/utils/store.js @@ -1,26 +1,37 @@ // utils/store.js - 配置状态管理与本地存储 const { STORAGE_KEYS } = require('./constants'); -const { generateId, deepClone, showToast, formatDate } = require('./util'); +const { + generateId, + deepClone, + showToast, + formatDate, + validateConfig, + validateComponent, + ERROR_CODES, + createError +} = require('./util'); let globalState = { configurations: [], currentConfig: null, selectedBikeType: 'Road', - isLoaded: false + isLoaded: false, + compareList: [] }; function init() { try { const configs = wx.getStorageSync(STORAGE_KEYS.CONFIGURATIONS) || []; const current = wx.getStorageSync(STORAGE_KEYS.CURRENT_CONFIG) || null; + const compareList = wx.getStorageSync(STORAGE_KEYS.COMPARE_LIST) || []; - globalState.configurations = configs; - globalState.currentConfig = current; + globalState.configurations = validateConfigurations(configs); + globalState.currentConfig = current ? validateAndFixConfig(current) : null; + globalState.compareList = compareList; globalState.isLoaded = true; - // 如果没有保存的配置,创建默认配置 - if (!current && configs.length === 0) { + if (!globalState.currentConfig && globalState.configurations.length === 0) { createDefaultConfig(); } } catch (e) { @@ -29,15 +40,59 @@ function init() { } } +function validateConfigurations(configs) { + if (!Array.isArray(configs)) return []; + + return configs.filter(config => { + const validation = validateConfig(config); + if (!validation.valid) { + console.warn('过滤无效配置:', validation.errors); + return false; + } + return true; + }); +} + +function validateAndFixConfig(config) { + const validation = validateConfig(config); + if (!validation.valid) { + console.warn('配置验证失败,尝试修复:', validation.errors); + if (!config.id) config.id = generateId('config'); + if (!config.name) config.name = '未命名配置'; + if (!config.bikeType) config.bikeType = 'Road'; + if (!config.components) config.components = []; + if (!config.createdAt) config.createdAt = Date.now(); + if (!config.updatedAt) config.updatedAt = Date.now(); + } + return config; +} + function refresh() { try { - globalState.configurations = wx.getStorageSync(STORAGE_KEYS.CONFIGURATIONS) || []; - globalState.currentConfig = wx.getStorageSync(STORAGE_KEYS.CURRENT_CONFIG) || null; + globalState.configurations = validateConfigurations( + wx.getStorageSync(STORAGE_KEYS.CONFIGURATIONS) || [] + ); + globalState.currentConfig = validateAndFixConfig( + wx.getStorageSync(STORAGE_KEYS.CURRENT_CONFIG) || null + ); + globalState.compareList = wx.getStorageSync(STORAGE_KEYS.COMPARE_LIST) || []; } catch (e) { console.error('刷新存储失败', e); } } +function saveToStorage() { + try { + wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); + wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); + wx.setStorageSync(STORAGE_KEYS.COMPARE_LIST, globalState.compareList); + return true; + } catch (e) { + console.error('保存到存储失败', e); + return false; + } +} + function createDefaultConfig() { const defaultConfig = { id: generateId('config'), @@ -82,6 +137,11 @@ function setCurrentConfig(configId) { } function createConfiguration(bikeType, name) { + if (!bikeType) { + console.error('创建配置失败:车型不能为空'); + return null; + } + const newConfig = { id: generateId('config'), name: name || `我的${getBikeTypeName(bikeType)}配置`, @@ -91,14 +151,17 @@ function createConfiguration(bikeType, name) { updatedAt: Date.now() }; + const validation = validateConfig(newConfig); + if (!validation.valid) { + console.error('配置验证失败:', validation.errors); + return null; + } + globalState.configurations.unshift(newConfig); globalState.currentConfig = newConfig; - try { - wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); - wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); - } catch (e) { - console.error('创建配置失败', e); + if (!saveToStorage()) { + showToast({ title: '保存失败,请检查存储空间', icon: 'error' }); } return deepClone(newConfig); @@ -106,31 +169,42 @@ function createConfiguration(bikeType, name) { function updateConfiguration(configId, updates) { const index = globalState.configurations.findIndex(c => c.id === configId); - if (index === -1) return null; + if (index === -1) { + console.error('更新配置失败:未找到配置'); + return null; + } - globalState.configurations[index] = { + const updatedConfig = { ...globalState.configurations[index], ...updates, updatedAt: Date.now() }; + const validation = validateConfig(updatedConfig); + if (!validation.valid) { + console.error('配置更新验证失败:', validation.errors); + return null; + } + + globalState.configurations[index] = updatedConfig; + if (globalState.currentConfig && globalState.currentConfig.id === configId) { - globalState.currentConfig = globalState.configurations[index]; + globalState.currentConfig = updatedConfig; } - try { - wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); - wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); - } catch (e) { - console.error('更新配置失败', e); + if (!saveToStorage()) { + showToast({ title: '保存失败,请检查存储空间', icon: 'error' }); } - return deepClone(globalState.configurations[index]); + return deepClone(updatedConfig); } function deleteConfiguration(configId) { const index = globalState.configurations.findIndex(c => c.id === configId); - if (index === -1) return false; + if (index === -1) { + console.error('删除配置失败:未找到配置'); + return false; + } globalState.configurations.splice(index, 1); @@ -138,11 +212,11 @@ function deleteConfiguration(configId) { globalState.currentConfig = globalState.configurations[0] || null; } - try { - wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); - wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); - } catch (e) { - console.error('删除配置失败', e); + globalState.compareList = globalState.compareList.filter(id => id !== configId); + + if (!saveToStorage()) { + showToast({ title: '删除失败,请重试', icon: 'error' }); + return false; } return true; @@ -150,7 +224,24 @@ function deleteConfiguration(configId) { function addComponent(configId, component) { const config = globalState.configurations.find(c => c.id === configId); - if (!config) return null; + if (!config) { + console.error('添加组件失败:未找到配置'); + return null; + } + + const validation = validateComponent(component); + if (!validation.valid) { + console.error('组件验证失败:', validation.errors); + return null; + } + + const existingComponent = config.components.find( + c => c.category === component.category && c.name === component.name + ); + if (existingComponent) { + showToast({ title: '该组件已存在', icon: 'none' }); + return deepClone(config); + } const newComponent = { ...component, @@ -165,11 +256,8 @@ function addComponent(configId, component) { globalState.currentConfig = config; } - try { - wx.setStorageSync(STORAGE_KEYS.CONFIGURATIONS, globalState.configurations); - wx.setStorageSync(STORAGE_KEYS.CURRENT_CONFIG, globalState.currentConfig); - } catch (e) { - console.error('添加组件失败', e); + if (!saveToStorage()) { + showToast({ title: '保存失败,请检查存储空间', icon: 'error' }); } return deepClone(config); @@ -216,6 +304,114 @@ function getSelectedBikeType() { return globalState.selectedBikeType; } +function addToCompare(configId) { + if (!configId) { + console.error('添加对比失败:配置ID为空'); + return false; + } + + if (globalState.compareList.includes(configId)) { + showToast({ title: '该配置已在对比列表中', icon: 'none' }); + return false; + } + + if (globalState.compareList.length >= 3) { + showToast({ title: '最多只能对比3个配置', icon: 'none' }); + return false; + } + + globalState.compareList.push(configId); + + if (!saveToStorage()) { + showToast({ title: '保存失败', icon: 'error' }); + return false; + } + + showToast({ title: '已添加到对比', icon: 'success' }); + return true; +} + +function removeFromCompare(configId) { + const index = globalState.compareList.indexOf(configId); + if (index === -1) { + console.error('移除对比失败:未找到配置'); + return false; + } + + globalState.compareList.splice(index, 1); + + if (!saveToStorage()) { + showToast({ title: '保存失败', icon: 'error' }); + return false; + } + + return true; +} + +function getCompareList() { + return deepClone(globalState.compareList); +} + +function getCompareConfigurations() { + return globalState.compareList + .map(id => globalState.configurations.find(c => c.id === id)) + .filter(Boolean); +} + +function clearCompareList() { + globalState.compareList = []; + + if (!saveToStorage()) { + showToast({ title: '保存失败', icon: 'error' }); + return false; + } + + return true; +} + +function exportData() { + try { + const data = { + configurations: globalState.configurations, + currentConfig: globalState.currentConfig, + compareList: globalState.compareList, + selectedBikeType: globalState.selectedBikeType, + exportTime: Date.now(), + version: '3.8.0' + }; + return JSON.stringify(data, null, 2); + } catch (e) { + console.error('导出数据失败', e); + return null; + } +} + +function importData(jsonString) { + try { + const data = JSON.parse(jsonString); + + if (!data.configurations || !Array.isArray(data.configurations)) { + throw createError(ERROR_CODES.VALIDATION_ERROR, '导入数据格式不正确'); + } + + globalState.configurations = validateConfigurations(data.configurations); + globalState.currentConfig = data.currentConfig ? validateAndFixConfig(data.currentConfig) : null; + globalState.compareList = data.compareList || []; + globalState.selectedBikeType = data.selectedBikeType || 'Road'; + + if (!saveToStorage()) { + throw createError(ERROR_CODES.STORAGE_ERROR, '保存失败'); + } + + showToast({ title: '导入成功', icon: 'success' }); + return true; + } catch (e) { + console.error('导入数据失败', e); + showToast({ title: e.message || '导入失败', icon: 'error' }); + return false; + } +} + module.exports = { init, refresh, @@ -228,5 +424,12 @@ module.exports = { addComponent, removeComponent, setSelectedBikeType, - getSelectedBikeType + getSelectedBikeType, + addToCompare, + removeFromCompare, + getCompareList, + getCompareConfigurations, + clearCompareList, + exportData, + importData }; diff --git a/miniprogram/utils/util.js b/miniprogram/utils/util.js index 6ebf8e1..1f9e54d 100644 --- a/miniprogram/utils/util.js +++ b/miniprogram/utils/util.js @@ -1,10 +1,30 @@ // utils/util.js - 通用工具函数 +const ERROR_CODES = { + UNKNOWN_ERROR: 'E000', + STORAGE_ERROR: 'E001', + VALIDATION_ERROR: 'E002', + NETWORK_ERROR: 'E003', + AUTH_ERROR: 'E004', + NOT_FOUND: 'E005' +}; + +const ERROR_MESSAGES = { + [ERROR_CODES.UNKNOWN_ERROR]: '发生未知错误,请重试', + [ERROR_CODES.STORAGE_ERROR]: '存储操作失败,请检查存储空间', + [ERROR_CODES.VALIDATION_ERROR]: '数据验证失败,请检查输入', + [ERROR_CODES.NETWORK_ERROR]: '网络连接异常,请检查网络', + [ERROR_CODES.AUTH_ERROR]: '登录状态异常,请重新登录', + [ERROR_CODES.NOT_FOUND]: '未找到相关数据' +}; + function formatPrice(price, currency = '¥') { + if (!price && price !== 0) return '¥0'; return `${currency}${Number(price).toLocaleString('zh-CN')}`; } function formatWeight(grams) { + if (!grams && grams !== 0) return '0 g'; if (grams >= 1000) { return `${(grams / 1000).toFixed(2)} kg`; } @@ -12,6 +32,7 @@ function formatWeight(grams) { } function formatDate(date) { + if (!date) return '-'; const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); @@ -56,12 +77,127 @@ function deepClone(obj) { return cloned; } -function showToast(title, icon = 'none', duration = 2000) { +function showToast(options) { + let title = ''; + let icon = 'none'; + let duration = 2000; + let action = null; + + if (typeof options === 'string') { + title = options; + } else if (options && typeof options === 'object') { + title = options.title || ''; + icon = options.icon || 'none'; + duration = options.duration || 2000; + action = options.action; + } + wx.showToast({ title, icon, duration }); + + if (action && action.text && action.handler) { + setTimeout(() => { + wx.showModal({ + title: '', + content: title, + showCancel: false, + confirmText: action.text, + confirmColor: '#0071e3', + success: (res) => { + if (res.confirm && typeof action.handler === 'function') { + action.handler(); + } + } + }); + }, duration - 500); + } +} + +function createError(code, message, details = null) { + const err = new Error(message || ERROR_MESSAGES[code] || '未知错误'); + err.code = code; + err.details = details; + return err; +} + +function handleError(error, showNotification = true) { + const code = error.code || ERROR_CODES.UNKNOWN_ERROR; + const message = error.message || ERROR_MESSAGES[code]; + + console.error(`[${code}] ${message}`, error.details); + + if (showNotification) { + showToast({ + title: message, + icon: 'error', + duration: 3000 + }); + } + + return { code, message, details: error.details }; +} + +function validateConfig(config) { + const errors = []; + + if (!config) { + errors.push('配置不能为空'); + return { valid: false, errors }; + } + + if (!config.id) { + errors.push('配置ID不能为空'); + } + + if (!config.name || !config.name.trim()) { + errors.push('配置名称不能为空'); + } + + if (!config.bikeType) { + errors.push('车型不能为空'); + } + + if (!Array.isArray(config.components)) { + errors.push('组件列表必须是数组'); + } + + return { + valid: errors.length === 0, + errors + }; +} + +function validateComponent(component) { + const errors = []; + + if (!component) { + errors.push('组件不能为空'); + return { valid: false, errors }; + } + + if (!component.name || !component.name.trim()) { + errors.push('组件名称不能为空'); + } + + if (!component.category) { + errors.push('组件类别不能为空'); + } + + if (component.price !== undefined && typeof component.price !== 'number') { + errors.push('价格必须是数字'); + } + + if (component.weight !== undefined && typeof component.weight !== 'number') { + errors.push('重量必须是数字'); + } + + return { + valid: errors.length === 0, + errors + }; } function showLoading(title = '加载中...') { @@ -116,5 +252,11 @@ module.exports = { showModal, calculateTotalPrice, calculateTotalWeight, - calculateProgress + calculateProgress, + ERROR_CODES, + ERROR_MESSAGES, + createError, + handleError, + validateConfig, + validateComponent }; From c8b0744f5bacdbb23f58cd9bb61268c172bd3c42 Mon Sep 17 00:00:00 2001 From: AjaCHN Date: Thu, 18 Jun 2026 11:55:20 +0000 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20=E5=88=86=E6=9E=90=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E9=A1=B5=E9=9D=A2=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: traeagent --- next.config.mjs | 8 ++ package-lock.json | 27 +++++- package.json | 2 + src/app/library/page.tsx | 24 ++++- src/app/page.tsx | 48 ++++++---- src/lib/i18n/index.ts | 185 +++++++++++++++++++++++++++++++----- src/lib/shareable-config.ts | 143 ++++++++++++++++++++++++++++ src/middleware.ts | 41 +++++++- 8 files changed, 428 insertions(+), 50 deletions(-) create mode 100644 src/lib/shareable-config.ts diff --git a/next.config.mjs b/next.config.mjs index b853bff..b4236c4 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -3,6 +3,14 @@ import withPWA from 'next-pwa'; /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + images: { + // Allow images from Firebase Storage and any HTTPS source + remotePatterns: [ + { protocol: 'https', hostname: 'firebasestorage.googleapis.com' }, + { protocol: 'https', hostname: '**.firebaseapp.com' }, + { protocol: 'https', hostname: '**' }, + ], + }, webpack: (config, { isServer }) => { if (isServer) { config.externals = [...(config.externals || []), 'firebase', 'firebase-admin']; diff --git a/package-lock.json b/package-lock.json index 6a922cc..625f841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,11 +23,13 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", "zustand": "^4.5.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^14.2.0", + "@types/btoa": "^1.2.5", "@types/node": "^24", "@types/react": "^18", "@types/react-dom": "^18", @@ -5017,6 +5019,16 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/btoa": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/btoa/-/btoa-1.2.5.tgz", + "integrity": "sha512-BItINdjZRlcGdI2efwK4bwxY5vEAT0SnIVfMOZVT18wp4900F1Lurqk/9PNdF9hMP1zgFmWbjVEtAsQKVcbqxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -14273,6 +14285,15 @@ "node": ">= 10.0.0" } }, + "node_modules/shadcn/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/sharp": { "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", @@ -18337,9 +18358,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index df69351..8ea17c8 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,13 @@ "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", "zustand": "^4.5.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.4.0", "@testing-library/react": "^14.2.0", + "@types/btoa": "^1.2.5", "@types/node": "^24", "@types/react": "^18", "@types/react-dom": "^18", diff --git a/src/app/library/page.tsx b/src/app/library/page.tsx index b52b026..5b225a0 100644 --- a/src/app/library/page.tsx +++ b/src/app/library/page.tsx @@ -1,8 +1,10 @@ 'use client'; +import { Suspense } from 'react'; import { Navbar } from '@/components/layout/Navbar'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; import { useMyConfigs, useComparingConfigIds, useCompareStore, useConfigStore } from '@/lib/stores'; import { formatCurrency, formatWeight } from '@/lib/utils'; import { useTranslation } from '@/lib/i18n'; @@ -46,7 +48,24 @@ export default function LibraryPage() { ) : ( -
+ + {[1, 2, 3].map((i) => ( + + + +
+ + +
+ +
+ ))} +
+ } + > +
{myConfigs.map((config, index) => ( ))}
- )} + + )} diff --git a/src/app/page.tsx b/src/app/page.tsx index b70c544..1d29d1a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import { Suspense } from 'react'; import { Navbar } from '@/components/layout/Navbar'; import { Footer } from '@/components/layout/Footer'; import { Hero } from '@/components/sections/Hero'; @@ -10,6 +11,7 @@ import { SummaryPanel } from '@/components/configurator/SummaryPanel'; import { ComponentSelector } from '@/components/configurator/ComponentSelector'; import { RecommendedConfigs } from '@/components/configurator/RecommendedConfigs'; import { ErrorBoundary } from '@/components/ui/error-boundary'; +import { LoadingScreen } from '@/components/ui/LoadingScreen'; import { motion } from 'framer-motion'; import { uiLogger } from '@/lib/logger'; @@ -48,34 +50,38 @@ export default function Home() { {/* Main Configurator */} + }> + +
+
+ +
+
+ +
+
+
+
+
+ + {/* Recommended Configurations */} + }> -
-
- -
-
- -
-
+
- - - {/* Recommended Configurations */} - - - +