diff --git a/src/components/AddCarCrad/index.vue b/src/components/AddCarCrad/index.vue new file mode 100644 index 0000000..0039f8a --- /dev/null +++ b/src/components/AddCarCrad/index.vue @@ -0,0 +1,29 @@ + + + + 请绑定真实有效的车牌号,否则将无法正常使用车牌付费等功能 + + + 长按二维码识别 + + + + + + diff --git a/src/components/AddCarCrad/styles/index.scss b/src/components/AddCarCrad/styles/index.scss new file mode 100644 index 0000000..89f33fe --- /dev/null +++ b/src/components/AddCarCrad/styles/index.scss @@ -0,0 +1,19 @@ +.add-car-card { + display: flex; + flex-direction: column; + align-items: center; + + .code-img { + margin-top: 12px; + width: 250px; + height: 250px; + border-radius: 4px; + } + + .tips { + padding: 12px; + font-size: 36rpx; + font-weight: 600; + color: #666; + } +} diff --git a/src/components/AiTabSwitch/images/L_01.png b/src/components/AiTabSwitch/images/L_01.png new file mode 100644 index 0000000..4fd32e9 Binary files /dev/null and b/src/components/AiTabSwitch/images/L_01.png differ diff --git a/src/components/AiTabSwitch/images/L_02.png b/src/components/AiTabSwitch/images/L_02.png new file mode 100644 index 0000000..3124cbe Binary files /dev/null and b/src/components/AiTabSwitch/images/L_02.png differ diff --git a/src/components/AiTabSwitch/images/R_01.png b/src/components/AiTabSwitch/images/R_01.png new file mode 100644 index 0000000..87e3bb3 Binary files /dev/null and b/src/components/AiTabSwitch/images/R_01.png differ diff --git a/src/components/AiTabSwitch/images/R_02.png b/src/components/AiTabSwitch/images/R_02.png new file mode 100644 index 0000000..97a7f0a Binary files /dev/null and b/src/components/AiTabSwitch/images/R_02.png differ diff --git a/src/components/AiTabSwitch/index.vue b/src/components/AiTabSwitch/index.vue new file mode 100644 index 0000000..89c97ed --- /dev/null +++ b/src/components/AiTabSwitch/index.vue @@ -0,0 +1,80 @@ + + + + + + + + 探索发现 + + + + + + + + + AI伴游 + + + + + + + + + + + diff --git a/src/components/AiTabSwitch/styles/index.scss b/src/components/AiTabSwitch/styles/index.scss new file mode 100644 index 0000000..6b1b96d --- /dev/null +++ b/src/components/AiTabSwitch/styles/index.scss @@ -0,0 +1,103 @@ +.ai-tab-wrapper { + width: 100%; +} + +.tab-container { + position: relative; + width: 100%; + height: 50px; + display: flex; + overflow: hidden; +} + +.tab-item { + position: relative; + flex: 1; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; +} + +.tab-item.active { + z-index: 10; +} + +.tab-content { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + line-height: 0; + transition: background 0.3s; + overflow: hidden; +} + +.tab-text { + font-size: 18px; + font-weight: 500; + color: rgba(255, 255, 255, 0.65); +} + +.tab-image { + position: absolute; + left: 0; + right: 0; + top: auto; + bottom: 0; + width: 100%; + height: 48px; + display: block; + vertical-align: bottom; + z-index: 5; +} + +.tab-label { + display: flex; + align-items: center; + justify-content: center; + position: relative; + z-index: 20; +} + +.tab-item.active .tab-text { + color: #2e312f; + font-weight: bold; +} + +.is-left { + margin-right: -24px; +} + +.is-left .tab-label { + transform: translateX(-20px); +} + +.is-right { + margin-left: -24px; +} + +.is-right .tab-label { + transform: translateX(20px); +} + +.status-dot { + display: inline-block; + margin-left: 6px; + width: 6px; + height: 6px; + border-radius: 50%; + z-index: 22; + transform: translateY(-1px); +} + +.status-dot--active { + background-color: #26d46c; +} + +.status-dot--inactive { + background-color: rgba(255, 255, 255, 0.65); + border: 1px solid rgba(0, 0, 0, 0.08); +} diff --git a/src/components/Calender/README.md b/src/components/Calender/README.md new file mode 100644 index 0000000..6da457f --- /dev/null +++ b/src/components/Calender/README.md @@ -0,0 +1,586 @@ +# 日历组件 (Calendar Component) + +## 功能需求分析 + +基于设计图片分析,该日历组件是一个酒店预订场景的低价日历弹窗,支持通过日期图标点击触发显示,需要实现以下细分功能: + +## 交互使用方式 + +### 基础用法 + +通过点击日期图标打开日历组件: + +```vue + + + + 选择日期 + {{ selectedDate }} + 请点击日期图标选择 + + + + + + + + + + + +``` + +### 演示文件 + +- `year-demo.vue` - 全年日历演示(推荐) +- `demo.vue` - 完整的交互演示页面 +- `example.vue` - 详细的使用示例,包含单选和范围选择 + +### 跨年日历 + +组件支持显示从当前月份到明年同月份的13个月日历,用户可以连续选择入住和离店日期: + +1. 点击第一个日期设置入住日期 +2. 点击第二个日期设置离店日期 +3. 自动显示"入住"和"离店"标签 +4. 支持滚动浏览跨年日期范围 + +### 1. 弹窗容器结构 + +#### 1.1 弹窗基础框架 + +- [ ] 实现遮罩层背景(半透明黑色) +- [ ] 弹窗居中显示,支持垂直居中 +- [ ] 弹窗圆角设计(建议12px) +- [ ] 弹窗阴影效果 +- [ ] 弹窗动画效果(淡入淡出) + +#### 1.2 弹窗头部区域 + +- [ ] 标题文字:"低价日历" +- [ ] 副标题文字:"以下价格为单晚参考价" +- [ ] 右上角关闭按钮(X图标) +- [ ] 关闭按钮点击交互 +- [ ] 头部区域背景色和分割线 + +### 2. 日历主体结构 + +#### 2.1 周标题行 + +- [ ] 显示周几标题:一、二、三、四、五、六、日 +- [ ] 周标题居中对齐 +- [ ] 周标题字体样式(灰色、小字号) + +#### 2.2 月份导航 + +- [ ] 月份标题显示:"2024年5月"、"2024年6月" +- [ ] 月份标题居中显示 +- [ ] 月份标题字体加粗 +- [ ] 支持上下滑动切换月份(可选) + +### 3. 日期网格系统 + +#### 3.1 日期格子基础 + +- [ ] 7列网格布局(对应周一到周日) +- [ ] 每个格子固定宽高比 +- [ ] 格子间距设计 +- [ ] 格子圆角设计 + +#### 3.2 日期内容显示 + +- [ ] 日期数字显示(居中对齐) +- [ ] 价格信息显示(¥449格式) +- [ ] 日期数字字体大小和颜色 +- [ ] 价格字体大小和颜色(较小、灰色) + +#### 3.3 日期状态管理 + +- [ ] 普通可选日期(白色背景) +- [ ] 当前选中日期(蓝色背景,白色文字) +- [ ] 入住日期标记("入住"标签) +- [ ] 离店日期标记("离店"标签) +- [ ] 选择范围内日期(浅蓝色背景) +- [ ] 不可选日期(灰色背景,禁用状态) + +### 4. 交互功能实现 + +#### 4.1 日期选择逻辑 + +- [ ] 单击日期选择功能 +- [ ] 日期范围选择(入住-离店) +- [ ] 选择状态视觉反馈 +- [ ] 选择完成后的回调事件 + +#### 4.2 用户体验优化 + +- [ ] 点击动画效果 +- [ ] 触摸反馈 +- [ ] 防止快速重复点击 +- [ ] 选择范围的视觉连接线(可选) + +### 5. 数据处理功能 + +#### 5.1 价格数据管理 + +- [ ] 价格数据结构设计 +- [ ] 价格数据绑定到日期 +- [ ] 价格格式化显示 +- [ ] 无价格数据的处理 + +#### 5.2 日期计算功能 + +- [ ] 月份天数计算 +- [ ] 月份第一天星期几计算 +- [ ] 跨月份日期处理 +- [ ] 日期有效性验证 + +### 6. 响应式适配 + +#### 6.1 移动端适配 + +- [ ] 触摸屏操作优化 +- [ ] 不同屏幕尺寸适配 +- [ ] 横竖屏切换适配 +- [ ] 安全区域适配(刘海屏等) + +#### 6.2 字体和尺寸适配 + +- [ ] 字体大小响应式调整 +- [ ] 格子尺寸响应式调整 +- [ ] 间距响应式调整 + +## 技术实现细节 + +### 组件接口设计 + +#### Props 属性 + +```javascript +props: { + // 弹窗显示控制 + visible: { + type: Boolean, + default: false, + required: true + }, + + // 价格数据对象 + priceData: { + type: Object, + default: () => ({}), + // 格式: { '2024-05-17': 449, '2024-05-18': 399 } + }, + + // 默认选中日期 + defaultValue: { + type: [String, Array], + default: '', + // 单选: '2024-05-17' + // 范围选择: ['2024-05-17', '2024-05-19'] + }, + + // 选择模式 + mode: { + type: String, + default: 'single', + validator: (value) => ['single', 'range'].includes(value) + }, + + // 最小可选日期 + minDate: { + type: String, + default: () => new Date().toISOString().split('T')[0] + }, + + // 最大可选日期 + maxDate: { + type: String, + default: '' + }, + + // 禁用日期数组 + disabledDates: { + type: Array, + default: () => [] + }, + + // 自定义标签 + customLabels: { + type: Object, + default: () => ({}) + // 格式: { '2024-05-17': '入住', '2024-05-19': '离店' } + } +} +``` + +#### Events 事件 + +```javascript +// 日期选择事件 +this.$emit("select", { + date: "2024-05-17", + price: 449, + mode: "single", +}); + +// 范围选择事件 +this.$emit("range-select", { + startDate: "2024-05-17", + endDate: "2024-05-19", + startPrice: 449, + endPrice: 399, + totalDays: 2, +}); + +// 弹窗关闭事件 +this.$emit("close"); + +// 月份切换事件 +this.$emit("month-change", { + year: 2024, + month: 5, + direction: "next", // 'prev' | 'next' +}); + +// 日期点击事件(包含所有点击) +this.$emit("date-click", { + date: "2024-05-17", + price: 449, + disabled: false, + selected: true, +}); +``` + +### 核心算法实现 + +#### 日期计算工具函数 + +```javascript +// 获取月份天数 +getDaysInMonth(year, month) { + return new Date(year, month, 0).getDate() +} + +// 获取月份第一天是星期几 +getFirstDayOfMonth(year, month) { + return new Date(year, month - 1, 1).getDay() +} + +// 生成日期网格数据 +generateCalendarGrid(year, month) { + const daysInMonth = this.getDaysInMonth(year, month) + const firstDay = this.getFirstDayOfMonth(year, month) + const grid = [] + + // 填充空白格子 + for (let i = 0; i < firstDay; i++) { + grid.push(null) + } + + // 填充日期 + for (let day = 1; day <= daysInMonth; day++) { + const dateStr = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}` + grid.push({ + date: dateStr, + day: day, + price: this.priceData[dateStr] || null, + disabled: this.isDateDisabled(dateStr), + selected: this.isDateSelected(dateStr), + inRange: this.isDateInRange(dateStr), + label: this.customLabels[dateStr] || '' + }) + } + + return grid +} +``` + +#### 选择状态管理 + +```javascript +data() { + return { + selectedDates: [], + currentMonth: new Date().getMonth() + 1, + currentYear: new Date().getFullYear(), + isRangeSelecting: false, + rangeStart: null, + rangeEnd: null + } +}, + +methods: { + handleDateClick(dateInfo) { + if (dateInfo.disabled) return + + if (this.mode === 'single') { + this.selectedDates = [dateInfo.date] + this.$emit('select', dateInfo) + } else if (this.mode === 'range') { + this.handleRangeSelection(dateInfo) + } + }, + + handleRangeSelection(dateInfo) { + if (!this.rangeStart || (this.rangeStart && this.rangeEnd)) { + // 开始新的范围选择 + this.rangeStart = dateInfo.date + this.rangeEnd = null + this.isRangeSelecting = true + } else { + // 完成范围选择 + this.rangeEnd = dateInfo.date + this.isRangeSelecting = false + + // 确保开始日期小于结束日期 + if (new Date(this.rangeStart) > new Date(this.rangeEnd)) { + [this.rangeStart, this.rangeEnd] = [this.rangeEnd, this.rangeStart] + } + + this.$emit('range-select', { + startDate: this.rangeStart, + endDate: this.rangeEnd, + startPrice: this.priceData[this.rangeStart], + endPrice: this.priceData[this.rangeEnd], + totalDays: this.calculateDaysBetween(this.rangeStart, this.rangeEnd) + }) + } + } +} +``` + +### 样式设计规范 + +#### 颜色系统 + +```scss +// 主色调 +$primary-color: #1890ff; +$primary-light: #e6f7ff; +$primary-dark: #0050b3; + +// 中性色 +$text-primary: #262626; +$text-secondary: #8c8c8c; +$text-disabled: #bfbfbf; +$background-white: #ffffff; +$background-gray: #f5f5f5; +$border-color: #d9d9d9; + +// 状态色 +$success-color: #52c41a; +$warning-color: #faad14; +$error-color: #ff4d4f; +``` + +#### 尺寸规范 + +```scss +// 弹窗尺寸 +$modal-width: 350px; +$modal-max-height: 80vh; +$modal-border-radius: 12px; +$modal-padding: 20px; + +// 日期格子尺寸 +$date-cell-size: 44px; +$date-cell-gap: 2px; +$date-cell-border-radius: 6px; + +// 字体大小 +$font-size-title: 18px; +$font-size-subtitle: 14px; +$font-size-date: 16px; +$font-size-price: 12px; +$font-size-label: 10px; +``` + +## 开发实施计划 + +### 第一阶段:基础框架(1-2天) + +- [ ] 创建组件基础结构 +- [ ] 实现弹窗容器和遮罩 +- [ ] 添加头部区域和关闭功能 +- [ ] 建立基础样式系统 + +### 第二阶段:日历核心(2-3天) + +- [ ] 实现日期计算算法 +- [ ] 构建日期网格布局 +- [ ] 添加周标题和月份显示 +- [ ] 实现基础日期显示 + +### 第三阶段:交互功能(2-3天) + +- [ ] 实现日期选择逻辑 +- [ ] 添加范围选择功能 +- [ ] 实现状态管理和视觉反馈 +- [ ] 添加价格数据绑定 + +### 第四阶段:优化完善(1-2天) + +- [ ] 添加动画效果 +- [ ] 优化移动端体验 +- [ ] 完善边界情况处理 +- [ ] 性能优化和测试 + +## 使用示例 + +### 基础用法 + +```vue + + + 选择日期 + + + + + + +``` + +### 高级用法 + +```vue + + + + + +``` + +## 测试用例 + +### 单元测试 + +- [ ] 日期计算函数测试 +- [ ] 选择逻辑测试 +- [ ] 价格数据绑定测试 +- [ ] 边界条件测试 + +### 集成测试 + +- [ ] 用户交互流程测试 +- [ ] 不同设备适配测试 +- [ ] 性能压力测试 + +### 可访问性测试 + +- [ ] 键盘导航测试 +- [ ] 屏幕阅读器兼容性 +- [ ] 色彩对比度检查 diff --git a/src/components/Calender/demo.vue b/src/components/Calender/demo.vue new file mode 100644 index 0000000..e6b0a4c --- /dev/null +++ b/src/components/Calender/demo.vue @@ -0,0 +1,249 @@ + + + + + 日历组件交互演示 + 点击日期图标打开日历 + + + + + + + 选择日期 + {{ + formatDate(selectedDate) + }} + 请点击日期图标选择 + + + + + + + + + + + 已选择日期 + {{ formatDate(selectedDate) }} + 价格:¥{{ selectedPrice }} + + + + + + + + + + + diff --git a/src/components/Calender/example.vue b/src/components/Calender/example.vue new file mode 100644 index 0000000..fb9f64e --- /dev/null +++ b/src/components/Calender/example.vue @@ -0,0 +1,337 @@ + + + + + 日历组件使用示例 + + + + + + + 选择日期 + {{ + selectedDate + }} + 请选择日期 + + + + + + + + 选择日期范围 + + {{ selectedRange.start }} 至 {{ selectedRange.end }} + + 请选择日期范围 + + + + + + + + 选择有价格的日期范围 + + {{ selectedRange.start }} 至 {{ selectedRange.end }} + + 请选择价格区间(仅含有价夜晚) + + + + + + + + 选择结果: + + 单选日期: + {{ selectedDate }} + + ¥{{ selectedDatePrice }} + + + + 日期范围: + {{ selectedRange.start }} 至 {{ selectedRange.end }} + (共{{ rangeDays }}天) + + 总价:¥{{ selectedRangeTotal }} + + + + + + + + + + + + + + + + + + diff --git a/src/components/Calender/images/日期-价格弹窗.png b/src/components/Calender/images/日期-价格弹窗.png new file mode 100644 index 0000000..543d484 Binary files /dev/null and b/src/components/Calender/images/日期-价格弹窗.png differ diff --git a/src/components/Calender/index.vue b/src/components/Calender/index.vue new file mode 100644 index 0000000..a56f2b9 --- /dev/null +++ b/src/components/Calender/index.vue @@ -0,0 +1,613 @@ + + + + + + + + 日历选择 + 选择住宿日期,以下价格为单晚参考价 + + + + + + + + + + {{ day }} + + + + + + + + + {{ monthData.title }} + + + + + {{ dateInfo.label }} + + {{ dateInfo.day }} + ¥{{ dateInfo.price }} + + + + + + + + + + + + + diff --git a/src/components/Calender/styles/index.scss b/src/components/Calender/styles/index.scss new file mode 100644 index 0000000..76b6695 --- /dev/null +++ b/src/components/Calender/styles/index.scss @@ -0,0 +1,283 @@ +// 颜色系统 +$primary-color: #1890ff; +$primary-light: #e6f7ff; +$primary-dark: #1890ff; + +// 中性色 +$text-primary: #262626; +$text-secondary: #8c8c8c; +$text-disabled: #bfbfbf; +$background-white: #ffffff; +$background-gray: #f5f5f5; +$border-color: #d9d9d9; + +// 状态色 +$success-color: #52c41a; +$warning-color: #faad14; +$error-color: #ff4d4f; + +// 尺寸规范 +$modal-max-height: 80vh; +$modal-border-radius: 12px; +$modal-padding: 12px; + +// 日期格子尺寸 +$date-cell-size: 40px; +$date-cell-gap: 4px; +$date-cell-border-radius: 6px; + +// 字体大小 +$font-size-title: 18px; +$font-size-subtitle: 14px; +$font-size-date: 16px; +$font-size-price: 12px; +$font-size-label: 10px; + +// uni-popup会自动处理遮罩层和定位,这里移除相关样式 + +// 弹窗主体 +.calendar-popup { + position: relative; + width: 100%; + background-color: $background-white; + border-radius: $modal-border-radius; + overflow: hidden; +} + +// 头部区域 +.calendar-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: $modal-padding; + border-bottom: 1px solid $border-color; + background-color: $background-white; +} + +.header-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.header-title { + font-size: $font-size-title; + font-weight: 600; + color: $text-primary; + line-height: 1.4; +} + +.header-subtitle { + font-size: $font-size-subtitle; + color: $text-secondary; + line-height: 1.4; +} + +.header-close { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: $uni-border-radius-circle; + transition: background-color 0.2s; + + &:active { + background-color: $background-gray; + } +} + +// 周标题行 - 固定显示 +.week-header { + display: flex; + padding: 16px $modal-padding 8px; + background-color: $background-white; + border-bottom: 1px solid #eeeeee; +} + +// 日历主体区域 +.calendar-body { + padding: 8px $modal-padding $modal-padding; + max-height: calc(#{$modal-max-height} - 140px); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { + display: none; + } +} + +.week-day { + flex: 1; + text-align: center; + font-size: $font-size-subtitle; + color: $text-secondary; + font-weight: 500; + line-height: 1.4; +} + +// 全年容器 +.year-container { + display: flex; + flex-direction: column; + gap: 32px; +} + +// 月份区域 +.month-section { + display: flex; + flex-direction: column; +} + +.month-title { + font-size: $font-size-title; + font-weight: 600; + color: $text-primary; + text-align: center; + margin-bottom: 16px; + margin-top: 8px; + line-height: 1.4; +} + +// 日期网格 +.date-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: $date-cell-gap; +} + +// 日期格子基础样式 +.date-cell { + position: relative; + width: $date-cell-size; + height: $date-cell-size; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 2px; + border-radius: $date-cell-border-radius; + transition: all 0.2s ease; + cursor: pointer; +} + +// 空白格子 +.date-cell-empty { + background-color: transparent; + cursor: default; +} + +// 有内容的格子 +.date-cell-content { + background-color: $background-white; + border: 1px solid transparent; + + &:hover { + background-color: $background-gray; + } + + &:active { + transform: scale(0.95); + } +} + +// 禁用状态 +.date-cell-disabled { + background-color: $background-gray !important; + color: $text-disabled !important; + cursor: not-allowed !important; + + .date-number, + .date-price { + color: $text-disabled !important; + } + + &:hover { + background-color: $background-gray !important; + transform: none !important; + } +} + +// 选中状态 +.date-cell-selected { + background-color: $primary-color !important; + border-color: $primary-color !important; + + .date-number, + .date-price, + .date-label { + color: $background-white !important; + } + + &:hover { + background-color: $primary-dark !important; + } +} + +// 范围内状态 +.date-cell-in-range { + background-color: $primary-light !important; + border-color: $primary-light !important; + + .date-number { + color: $primary-color !important; + } + + .date-price { + color: $primary-dark !important; + } +} + +// 日期数字 +.date-number { + font-size: $font-size-date; + font-weight: 500; + color: $text-primary; + line-height: 1; + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + +// 价格文字 +.date-price { + font-size: $font-size-price; + color: $text-secondary; + line-height: 1; + font-weight: 400; + text-align: center; + min-height: 14px; +} + +// 无价格占位样式 +.date-cell-no-price { + .date-number { + color: $text-primary; + opacity: 0.9; + } + + .date-price { + color: $text-disabled; + font-style: italic; + } + + .date-price--empty { + color: $text-disabled; + font-style: normal; + } +} + +// 自定义标签 +.date-label { + font-size: $font-size-label; + color: $primary-color; + padding: 1px 4px; + border-radius: 2px; + line-height: 1; + white-space: nowrap; + font-weight: 500; + text-align: center; + min-height: 12px; +} diff --git a/src/components/Calender/year-demo.vue b/src/components/Calender/year-demo.vue new file mode 100644 index 0000000..9309c98 --- /dev/null +++ b/src/components/Calender/year-demo.vue @@ -0,0 +1,283 @@ + + + + 跨年日历演示 + 支持从当前月份到明年同月份的日期连续选择 + + + + + + + 📅 + + + 选择入住和离店日期 + + + {{ formatDateRange() }} + + + + + + + + 选择结果: + + 入住日期: + {{ + formatDate(selectedRange.start) + }} + + + 离店日期: + {{ formatDate(selectedRange.end) }} + + + 住宿天数: + {{ calculateDays() }}晚 + + + + + + + + + + + + diff --git a/src/components/CheckBox/index.vue b/src/components/CheckBox/index.vue new file mode 100644 index 0000000..24fc63b --- /dev/null +++ b/src/components/CheckBox/index.vue @@ -0,0 +1,39 @@ + + + + + + + + + + diff --git a/src/components/CheckBox/propmt.md b/src/components/CheckBox/propmt.md new file mode 100644 index 0000000..a865b92 --- /dev/null +++ b/src/components/CheckBox/propmt.md @@ -0,0 +1,12 @@ +## 复选框组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.checkbox-wrapper +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/CheckBox/styles/index.scss b/src/components/CheckBox/styles/index.scss new file mode 100644 index 0000000..3ed1446 --- /dev/null +++ b/src/components/CheckBox/styles/index.scss @@ -0,0 +1,10 @@ +.checkbox-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + + .checkbox-icon { + margin-right: 6px; + color: $theme-color-500; + } +} diff --git a/src/components/CommandWrapper/images/2025-07-14_144207.png b/src/components/CommandWrapper/images/2025-07-14_144207.png new file mode 100644 index 0000000..b271e83 Binary files /dev/null and b/src/components/CommandWrapper/images/2025-07-14_144207.png differ diff --git a/src/components/CommandWrapper/index.vue b/src/components/CommandWrapper/index.vue new file mode 100644 index 0000000..d2b078e --- /dev/null +++ b/src/components/CommandWrapper/index.vue @@ -0,0 +1,18 @@ + + + {{ span}} + + + + + + diff --git a/src/components/CommandWrapper/propmt.md b/src/components/CommandWrapper/propmt.md new file mode 100644 index 0000000..ad076dc --- /dev/null +++ b/src/components/CommandWrapper/propmt.md @@ -0,0 +1,12 @@ +## 消息体指令组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.command-wrapper +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/CommandWrapper/styles/index.scss b/src/components/CommandWrapper/styles/index.scss new file mode 100644 index 0000000..e9f3499 --- /dev/null +++ b/src/components/CommandWrapper/styles/index.scss @@ -0,0 +1,11 @@ +.command-wrapper { + background-color: $theme-color-500; + border-radius: 20px 4px 20px 20px; + padding: 8px 24px; + width: max-content; +} + +.command-text { + color: #fff; + font-size: $uni-font-size-base; +} diff --git a/src/components/CreateServiceOrder/images/2025-07-11_104138.png b/src/components/CreateServiceOrder/images/2025-07-11_104138.png new file mode 100644 index 0000000..f71278c Binary files /dev/null and b/src/components/CreateServiceOrder/images/2025-07-11_104138.png differ diff --git a/src/components/CreateServiceOrder/images/icon_service.png b/src/components/CreateServiceOrder/images/icon_service.png new file mode 100644 index 0000000..5afa5d4 Binary files /dev/null and b/src/components/CreateServiceOrder/images/icon_service.png differ diff --git a/src/components/CreateServiceOrder/images/icon_volume.png b/src/components/CreateServiceOrder/images/icon_volume.png new file mode 100644 index 0000000..75bc98d Binary files /dev/null and b/src/components/CreateServiceOrder/images/icon_volume.png differ diff --git a/src/components/CreateServiceOrder/index.vue b/src/components/CreateServiceOrder/index.vue new file mode 100644 index 0000000..aa35022 --- /dev/null +++ b/src/components/CreateServiceOrder/index.vue @@ -0,0 +1,287 @@ + + + + + + {{ isCallSuccess ? "服务已创建" : "呼叫服务" }} + + + + + + + 所在位置 + + + + + 联系电话 + + + + + 需求信息描述 + + + + + 照片上传 + + + + + + + + + + + {{ zniconsMap["zn-camera"] }} + + + + + + + + + + 所在位置:{{ roomId }} + + + 联系方式: {{ contactPhone }} + + + 需求描述: {{ contactspan}} + + + + + + + + {{ isCallSuccess ? "查看服务" : "立即呼叫" }} + + + + + + + + diff --git a/src/components/CreateServiceOrder/prompt.md b/src/components/CreateServiceOrder/prompt.md new file mode 100644 index 0000000..dbe2dd0 --- /dev/null +++ b/src/components/CreateServiceOrder/prompt.md @@ -0,0 +1,25 @@ +## 消息体组件信息 + +组件名称:消息体创建服务工单 +服务名称:加一台麻将机 +房间号:302 +服务时间:2025-09-12 12:00 +联系房客: +联系电话: +立即呼叫按钮 + +呼叫成功之后 +1、呼叫按钮变为两个按钮 查看工单和已完成,见图中的布局 +2、联系人和联系电话,仅展示,不能编辑 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.create-service-order +3、可以使用 uniapp 内置的组件 +4、联系房客/联系电话,需要用户自己填写 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/CreateServiceOrder/styles/index.scss b/src/components/CreateServiceOrder/styles/index.scss new file mode 100644 index 0000000..073303d --- /dev/null +++ b/src/components/CreateServiceOrder/styles/index.scss @@ -0,0 +1,22 @@ +.order-header { + height: 48px; +} + +.header-icon { + width: 98px; + height: 48px; +} + +.help-icon { + width: 16px; + height: 16px; +} + +.btn { + height: 44px; +} + +.right { + height: 88px; + width: 88px; +} diff --git a/src/components/CustomEmpty/index.vue b/src/components/CustomEmpty/index.vue new file mode 100644 index 0000000..678c4ee --- /dev/null +++ b/src/components/CustomEmpty/index.vue @@ -0,0 +1,28 @@ + + + + + {{ statusText }} + + + + + + + diff --git a/src/components/CustomEmpty/styles/index.scss b/src/components/CustomEmpty/styles/index.scss new file mode 100644 index 0000000..b07e999 --- /dev/null +++ b/src/components/CustomEmpty/styles/index.scss @@ -0,0 +1,4 @@ +.empty-image { + height: 130px; + width: 130px; +} diff --git a/src/components/DateRangeSection/index.vue b/src/components/DateRangeSection/index.vue new file mode 100644 index 0000000..6bb60b9 --- /dev/null +++ b/src/components/DateRangeSection/index.vue @@ -0,0 +1,77 @@ + + + + 入住 + + {{ selectedDate.startDate }} + + {{ selectedDate.totalDays }}晚 + 离店 + + {{ selectedDate.endDate }} + + + + 房间详情 + + + + + + + + diff --git a/src/components/DetailPopup/index.vue b/src/components/DetailPopup/index.vue new file mode 100644 index 0000000..2420e59 --- /dev/null +++ b/src/components/DetailPopup/index.vue @@ -0,0 +1,103 @@ + + + + + + 明细详情 + + + + + + + + 在线支付 + 239 + + + 房费 + 239 + + + + + + + + + diff --git a/src/components/DetailPopup/styles/index.scss b/src/components/DetailPopup/styles/index.scss new file mode 100644 index 0000000..57425d5 --- /dev/null +++ b/src/components/DetailPopup/styles/index.scss @@ -0,0 +1,9 @@ +.refund-popup { + border-radius: 15px 15px 0 0; + padding-bottom: 40px; +} + +.close { + top: 14px; + right: 12px; +} diff --git a/src/components/Divider/index.vue b/src/components/Divider/index.vue new file mode 100644 index 0000000..363502c --- /dev/null +++ b/src/components/Divider/index.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/components/Divider/styles/index.scss b/src/components/Divider/styles/index.scss new file mode 100644 index 0000000..4f0cc2f --- /dev/null +++ b/src/components/Divider/styles/index.scss @@ -0,0 +1,36 @@ +.divider { + height: 1px; + margin: 0 10px; + background: linear-gradient( + to right, + #eee, + #eee 5px, + transparent 5px, + transparent + ); + background-size: 10px 100%; + position: relative; + + // &::before, &::after { + // position: absolute; + // content: ''; + // height: 12px; + // width: 6px; + + // background-color: #E2EDF2; + // top: 50%; + // transform: translateY(-50%); + // } + + // &::before { + // border-radius: 0 20px 20px 0; + // top: 0; + // left: -10px; + // } + + // &::after { + // border-radius: 20px 0 0 20px; + // top: 0; + // right: -10px; + // } +} diff --git a/src/components/Feedback/images/icon_volume.png b/src/components/Feedback/images/icon_volume.png new file mode 100644 index 0000000..75bc98d Binary files /dev/null and b/src/components/Feedback/images/icon_volume.png differ diff --git a/src/components/Feedback/index.vue b/src/components/Feedback/index.vue new file mode 100644 index 0000000..2b3bfa5 --- /dev/null +++ b/src/components/Feedback/index.vue @@ -0,0 +1,132 @@ + + + + + + {{ isCallSuccess ? "反馈已创建" : "反馈意见" }} + + + + + + + 联系电话 + + + + 意见内容 + + + + + 立即提交 + + + + + + 联系方式: {{ contactPhone }} + + + 意见内容: {{ contactText }} + + + + + + + + + + + diff --git a/src/components/Feedback/styles/index.scss b/src/components/Feedback/styles/index.scss new file mode 100644 index 0000000..f9fff58 --- /dev/null +++ b/src/components/Feedback/styles/index.scss @@ -0,0 +1,17 @@ +.order-header { + height: 48px; +} + +.header-icon { + width: 98px; + height: 48px; +} + +.help-icon { + width: 16px; + height: 16px; +} + +.btn { + height: 44px; +} diff --git a/src/components/FormCard/README.md b/src/components/FormCard/README.md new file mode 100644 index 0000000..a4bf1f2 --- /dev/null +++ b/src/components/FormCard/README.md @@ -0,0 +1,473 @@ +# FormCard 表单卡片组件 + +一个功能完整的表单卡片组件,支持姓名和手机号输入,具备数据验证和双向绑定功能。 + +## 功能特性 + +- 📝 **双向绑定**:支持 v-model 双向数据绑定 +- ✅ **数据验证**:内置手机号格式验证 +- 🎨 **自定义标题**:可配置游客标题文本 +- 🗑️ **删除功能**:支持删除操作,可配置显示/隐藏 +- 💫 **交互反馈**:输入框聚焦效果和错误状态提示 +- 📱 **响应式设计**:适配不同屏幕尺寸 +- 🎯 **事件支持**:完整的事件系统 +- ⚡ **性能优化**:使用计算属性优化渲染 + +## 基础用法 + +### 默认使用 + +```vue + + + + + +``` + +### 自定义标题 + +```vue + + + +``` + +### 隐藏删除图标 + +```vue + + + +``` + +### 多个表单卡片 + +```vue + + + + 添加游客 + + + + +``` + +### 表单验证 + +```vue + + + 验证表单 + + + +``` + +## API 文档 + +### Props + +| 参数 | 类型 | 默认值 | 说明 | +| -------------- | ------- | ------------------------- | ------------------------------------- | +| title | String | "游客1" | 表单卡片标题 | +| form | Object | `{ name: '', phone: '' }` | 表单数据对象,包含 name 和 phone 字段 | +| form.name | String | "" | 姓名值 | +| form.phone | String | "" | 手机号值 | +| showDeleteIcon | Boolean | true | 是否显示删除图标 | + +### Events + +| 事件名 | 参数 | 说明 | +| ------------ | --------------- | -------------------------------------- | +| update:name | (value: string) | 姓名值更新时触发,自动去除首尾空格 | +| update:phone | (value: string) | 手机号值更新时触发,自动过滤非数字字符 | +| delete | - | 点击删除图标时触发 | + +### Methods (通过 ref 调用) + +| 方法名 | 参数 | 返回值 | 说明 | +| ------------- | --------------- | ------ | ---------------------- | +| validateName | - | void | 手动触发姓名验证 | +| validatePhone | - | void | 手动触发手机号验证 | +| getNameError | (name: string) | string | 获取姓名验证错误信息 | +| getPhoneError | (phone: string) | string | 获取手机号验证错误信息 | + +### 数据验证 + +组件内置完整的表单验证: + +- **姓名验证**:不能为空,自动去除首尾空格 +- **手机号验证**:支持中国大陆手机号格式(1开头,第二位为3-9,总长度11位) +- **失焦验证**:只在输入框失去焦点时进行验证,避免输入干扰 +- **错误提示**:验证失败时显示错误信息,带有淡入动画效果 +- **视觉反馈**:输入框边框变红提示错误状态 +- **自动过滤**:手机号输入时自动过滤非数字字符 + +## 样式定制 + +### CSS 变量系统 + +组件使用 CSS 变量系统,支持主题定制: + +```scss +:root { + --form-primary-color: #00a6ff; // 主色调 + --form-error-color: #ff4d4f; // 错误色 + --form-text-color: #333; // 文本色 + --form-label-color: #86909c; // 标签色 + --form-border-color: #e5e8ef; // 边框色 + --form-input-border-color: #ddd; // 输入框边框色 + --form-bg-color: #fff; // 背景色 + --form-header-bg-color: rgba(25, 144, 255, 0.06); // 头部背景色 + --form-border-radius: 8px; // 圆角大小 + --form-transition: all 0.2s ease; // 过渡动画 +} +``` + +### 主要样式类 + +```scss +.form-wrapper { + // 表单容器,支持悬停效果和阴影 +} + +.form-header { + // 表单头部,包含标题和删除按钮 +} + +.form-title { + // 标题文本,支持文本溢出省略 +} + +.form-item { + // 表单项容器,支持分隔线 +} + +.form-input { + // 输入框,支持聚焦和错误状态 +} + +.form-error { + // 错误信息,带有淡入动画 +} +``` + +### 响应式设计 + +组件内置响应式支持,在小屏幕设备上自动调整: + +- 320px 以下设备优化布局 +- 自动调整字体大小和间距 +- 保持良好的可用性 + +### 自定义主题 + +通过覆盖 CSS 变量来自定义主题: + +```scss +// 自定义主题色 +:root { + --form-primary-color: #your-primary-color; + --form-error-color: #your-error-color; + --form-border-radius: 12px; +} + +// 或者针对特定组件 +.your-custom-form { + --form-primary-color: #your-primary-color; + --form-header-bg-color: rgba(your-color, 0.1); +} +``` + +## 高级用法 + +### 表单验证集成 + +```vue + + + 提交 + + + +``` + +### 动态表单管理 + +```vue + + + + + + 添加成人 + 添加儿童 + + + + + +``` + +## 注意事项 + +1. **数据传递**:使用 `:form` 对象传递数据,包含 `name` 和 `phone` 字段 +2. **双向绑定**:通过 `@update:name` 和 `@update:phone` 事件进行双向绑定 +3. **验证机制**:只在失去焦点时进行验证,避免输入干扰 +4. **手机号验证**:仅支持中国大陆手机号格式验证(1开头,第二位3-9,总长度11位) +5. **自动处理**:手机号自动过滤非数字字符,姓名自动去除首尾空格 +6. **删除功能**:删除事件需要父组件处理具体逻辑 +7. **方法调用**:通过 `ref` 可调用组件内部的验证方法 +8. **兼容性**:支持微信小程序、H5、App等平台 + +## 更新日志 + +### v1.3.0 (2024-12-19) + +**性能与可维护性全面优化** + +- 🚀 **性能优化**:提取常量定义,优化计算属性逻辑 +- 🛠️ **代码重构**:添加完整的 JSDoc 注释和类型定义 +- 🎨 **样式升级**:使用 CSS 变量系统,支持主题定制 +- ✨ **功能增强**:手机号自动过滤非数字字符,姓名自动去除空格 +- 🎭 **UI 改进**:新增悬停效果、错误信息动画和阴影效果 +- 📱 **响应式设计**:优化小屏幕设备适配 +- 🔧 **开发体验**:添加 defineExpose 暴露验证方法,便于测试 +- 📝 **文档完善**:更新演示和使用说明 + +### v1.2.3 (2024-12-19) + +**优化验证行为** + +- 🎨 优化验证行为,移除实时验证 +- ✨ 姓名和手机号只在失去焦点时进行验证 +- 🔧 移除不必要的 watch 监听器 +- 📝 更新文档和演示说明 +- ⚡ 提升组件性能和用户体验 + +### v1.2.2 (2024-12-19) + +**新增姓名验证功能** + +- ✨ 新增姓名非空验证功能 +- 👤 姓名为空时显示"请输入姓名"提示 +- 🔄 支持姓名实时验证,输入内容时错误信息自动隐藏 +- 🎯 完善表单验证体系,提升数据完整性 +- 💫 优化用户体验,提供友好的输入提示 + +### v1.2.1 (2024-12-19) + +**优化手机号验证功能** + +- 🐛 修复validatePhone方法中props引用错误的问题 +- ✨ 新增手机号实时验证功能 +- 🔄 输入正确手机号时错误信息自动隐藏 +- 📱 优化用户输入体验,提供即时反馈 +- 🎯 完善demo页面,增加功能说明 + +### v1.2.0 (2024-12-19) + +**新增删除功能** + +- ✨ 支持删除表单卡片 +- 🎯 可配置删除图标显示/隐藏 +- 🔄 完善事件系统,支持delete事件 +- 💫 优化用户交互体验 + +### v2.0.0 + +- ✨ 重构组件,支持 props 传值和双向绑定 +- ✨ 新增 `title` 属性,支持自定义标题 +- ✨ 新增 `showDeleteIcon` 属性,控制删除图标显示 +- ✨ 新增完整的事件系统(update:name, update:phone, delete) +- 🎨 优化样式,新增错误状态和交互效果 +- 🔧 改进手机号验证逻辑 +- 📝 新增完整的文档和演示示例 + +### v1.0.0 + +- 🎉 初始版本发布 +- ✨ 基础表单功能 +- ✨ 手机号验证 +- ✨ 基础样式 + +## 技术栈 + +- Vue 3 Composition API +- SCSS +- uni-app + +## 浏览器支持 + +- 微信小程序 +- H5 (Chrome, Firefox, Safari, Edge) +- App (iOS, Android) + +## 许可证 + +MIT License diff --git a/src/components/FormCard/demo.vue b/src/components/FormCard/demo.vue new file mode 100644 index 0000000..424b39c --- /dev/null +++ b/src/components/FormCard/demo.vue @@ -0,0 +1,256 @@ + + + FormCard 表单组件演示 + + + ✨ 支持姓名和手机号输入,自动过滤非数字字符 + 👤 姓名失去焦点时验证,自动去除首尾空格 + 📱 手机号失去焦点时验证,支持中国大陆手机号格式 + 🎨 优化UI设计,支持悬停效果和动画 + 🗑️ 支持删除操作(可配置显示/隐藏) + 📱 响应式设计,适配小屏幕设备 + + + + + 示例1: 基础用法 + + + 姓名: {{ form1.name }} + 手机号: {{ form1.phone }} + + + + + + 示例2: 自定义标题 + + + 姓名: {{ form2.name }} + 手机号: {{ form2.phone }} + + + + + + 示例3: 隐藏删除图标 + + + 姓名: {{ form3.name }} + 手机号: {{ form3.phone }} + + + + + + 示例4: 多个表单卡片 + + 添加游客 + + + 表单数据: + + 游客{{ index + 1 }}: {{ item.name }} - {{ item.phone }} + + + + + + + + + diff --git a/src/components/FormCard/images/2025-07-15_161532.png b/src/components/FormCard/images/2025-07-15_161532.png new file mode 100644 index 0000000..8231aa0 Binary files /dev/null and b/src/components/FormCard/images/2025-07-15_161532.png differ diff --git a/src/components/FormCard/images/icon_minus.png b/src/components/FormCard/images/icon_minus.png new file mode 100644 index 0000000..343b1fa Binary files /dev/null and b/src/components/FormCard/images/icon_minus.png differ diff --git a/src/components/FormCard/index.vue b/src/components/FormCard/index.vue new file mode 100644 index 0000000..67c111b --- /dev/null +++ b/src/components/FormCard/index.vue @@ -0,0 +1,184 @@ + + + + + {{ title }} + + + + + + 姓 名 + + + {{ nameError }} + + + + 手机号 + + + {{ phoneError }} + + + + + + + + diff --git a/src/components/FormCard/propmt.md b/src/components/FormCard/propmt.md new file mode 100644 index 0000000..ffc14ac --- /dev/null +++ b/src/components/FormCard/propmt.md @@ -0,0 +1,14 @@ +## 表单组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、参考图片,高度还原交互设计,完成组件封装 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.form-wrapper +3、可以使用 uniapp 内置的组件 +4、姓名、手机号,需要用户自己填写 +5、验证手机号格式是否正确 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/FormCard/styles/index.scss b/src/components/FormCard/styles/index.scss new file mode 100644 index 0000000..830002e --- /dev/null +++ b/src/components/FormCard/styles/index.scss @@ -0,0 +1,152 @@ +// SASS 变量定义 +$form-primary-color: #00a6ff; +$form-error-color: #ff4d4f; +$form-text-color: $uni-text-color; +$form-label-color: #86909c; +$form-border-color: #ddd; +$form-input-border-color: #ddd; +$form-bg-color: #f5f5f5; +$form-header-bg-color: rgba(25, 144, 255, 0.06); +$form-border-radius: 8px; +$form-transition: all 0.2s ease; + +.form-wrapper { + display: inline-block; + font-size: 0; + border-radius: $form-border-radius; + overflow: hidden; + width: 280px; + margin-right: 12px; +} + +.form-header { + background-color: $form-header-bg-color; + box-sizing: border-box; + border: 1px solid $form-border-color; + border-radius: $form-border-radius $form-border-radius 0 0; + display: flex; + align-items: center; + padding: 10px 12px; + min-height: 44px; + + .uni-color { + color: $theme-color-500; + } + + .minus, + .delete { + height: 22px; + width: 22px; + cursor: pointer; + transition: $form-transition; + + &:hover { + transform: scale(1.1); + } + } + + .delete { + margin-left: auto; + } +} + +.form-title { + margin-left: 8px; + font-size: $uni-font-size-lg; + font-weight: 500; + color: $theme-color-500; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.form-item-wrapper { + background-color: $form-bg-color; + border-radius: 0 0 $form-border-radius $form-border-radius; + box-sizing: border-box; + border: 1px solid $form-border-color; + border-top: none; + padding: 12px; +} + +.form-item { + display: flex; + align-items: flex-start; + flex-direction: column; + position: relative; + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } +} + +.form-item-row { + display: flex; + align-items: center; + width: 100%; +} + +.form-label { + font-size: $uni-font-size-base; + color: $form-label-color; + width: 50px; + flex-shrink: 0; + font-weight: 500; + margin-right: 10px; +} + +.form-input { + flex: 1; + font-size: $uni-font-size-base; + color: $form-text-color; + border: none; + border-bottom: 1px solid $form-input-border-color; + background: transparent; + outline: none; + transition: $form-transition; + min-height: 20px; + + &::placeholder { + color: $form-label-color; + opacity: 0.8; + } + + &:focus { + border-bottom-color: $theme-color-500; + + &::placeholder { + opacity: 0.5; + } + } + + &.form-input-error { + border-bottom-color: $form-error-color; + + &:focus { + border-bottom-color: $form-error-color; + } + } +} + +.form-error { + font-size: $uni-font-size-sm; + color: $form-error-color; + margin-top: 6px; + margin-left: 60px; + line-height: 1.4; + animation: fadeInError 0.3s ease-in-out; +} + +// 错误信息淡入动画 +@keyframes fadeInError { + from { + opacity: 0; + transform: translateY(-4px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/components/GoodDetail/index.vue b/src/components/GoodDetail/index.vue new file mode 100644 index 0000000..1b5a36d --- /dev/null +++ b/src/components/GoodDetail/index.vue @@ -0,0 +1,71 @@ + + + + + + + + + {{ zniconsMap[moduleItem.moduleIcon] }} + + + {{ moduleItem.moduleTitle }} + + + + {{ moduleItem.moduleContent }} + + + + + + + + + + + diff --git a/src/components/ImageSwiper/README.md b/src/components/ImageSwiper/README.md new file mode 100644 index 0000000..36f5b45 --- /dev/null +++ b/src/components/ImageSwiper/README.md @@ -0,0 +1,334 @@ +# ImageSwiper 轮播图组件 + +一个功能丰富的轮播图组件,支持自定义圆角、缩略图导航和图片描述。 + +## 功能特性 + +- 🎨 **可配置圆角**:支持数字(px)或字符串形式的圆角设置 +- 📏 **可配置高度**:支持数字(px)或字符串形式的高度设置 +- 🖼️ **缩略图导航**:底部缩略图快速切换,支持左右滑动 +- 👁️ **缩略图控制**:可配置显示或隐藏缩略图 +- 📱 **响应式设计**:适配不同屏幕尺寸 +- 🎯 **自定义数据**:支持传入自定义图片数据 +- 📊 **进度指示器**:显示当前图片位置 +- 🎭 **选中状态**:缩略图选中时高亮显示,带缩放动画 +- 🔄 **自动滚动**:缩略图自动滚动到可视区域 +- ⚡ **性能优化**:使用计算属性优化渲染 + +## 基础用法 + +### 默认使用 + +```vue + + + + + +``` + +### 自定义圆角 + +```vue + + + + + + + + + + +``` + +### 自定义高度 + +```vue + + + + + + + + + + +``` + +### 隐藏缩略图 + +```vue + + + + + + + +``` + +### 自定义图片数据 + +```vue + + + + + +``` + +### 缩略图滑动功能 + +组件支持缩略图左右滑动,当图片数量较多时,缩略图会自动滚动到可视区域: + +```vue + + + + + + +``` + +## API 文档 + +### Props + +| 参数 | 类型 | 默认值 | 说明 | +| -------------- | ---------------- | ------ | -------------------------------- | +| borderRadius | Number \| String | 8 | 轮播图圆角大小,数字时单位为px | +| height | Number \| String | 200 | 轮播图高度,数字时单位为px | +| showThumbnails | Boolean | true | 是否显示缩略图 | +| images | Array | [] | 图片数据数组,为空时使用默认数据 | + +### images 数组结构 + +```typescript +interface ImageItem { + photoUrl: string; // 图片URL + photoName: string; // 图片名称/描述 +} +``` + +## 样式定制 + +### 圆角配置示例 + +```vue + + + + + + + + + + + + + + +``` + +### 动态圆角控制 + +```vue + + + + + + + + +``` + +## 高级用法 + +### 响应式配置 + +```vue + + + + + +``` + +### 动态控制示例 + +```vue + + + + + 高度: {{ dynamicHeight }}px + + + + + 显示缩略图 + + + + + + + + + +``` + +### 主题适配 + +```vue + + + + + +``` + +## 注意事项 + +1. **圆角单位**:数字类型自动添加px单位,字符串类型直接使用 +2. **高度单位**:数字类型自动添加px单位,字符串类型直接使用(支持vh、rem等) +3. **缩略图显示**:当设置 `showThumbnails` 为 `false` 时,缩略图完全隐藏 +4. **图片比例**:建议使用相同比例的图片以获得最佳显示效果 +5. **性能优化**:大量图片时建议使用懒加载 +6. **兼容性**:支持微信小程序、H5、App等平台 + +## 更新日志 + +### v1.3.0 + +- ✨ 新增 `height` 属性,支持自定义轮播图高度 +- ✨ 新增 `showThumbnails` 属性,支持隐藏缩略图 +- 🎨 优化样式系统,移除硬编码高度 +- 🔧 改进计算属性,支持动态高度和缩略图控制 +- 📝 更新文档和演示示例,新增多个高级用法示例 +- 🎯 增强组件灵活性,适应更多使用场景 + +### v1.2.0 + +- ✨ 新增缩略图左右滑动功能 +- ✨ 新增缩略图选中状态高亮显示 +- ✨ 新增缩略图自动滚动到可视区域 +- 🎨 优化缩略图动画效果和交互体验 +- 🔧 改进主轮播图与缩略图的联动机制 +- 📝 更新文档和演示示例 + +### v1.1.0 + +- ✨ 新增 `borderRadius` 属性,支持自定义圆角 +- ✨ 新增 `images` 属性,支持自定义图片数据 +- 🔧 优化组件结构,使用计算属性提升性能 +- 📝 完善文档和示例 + +### v1.0.0 + +- 🎉 初始版本发布 +- ✨ 基础轮播图功能 +- ✨ 缩略图导航 +- ✨ 进度指示器 + +## 技术栈 + +- Vue 3 Composition API +- SCSS +- uni-app + +## 浏览器支持 + +- 微信小程序 +- H5 (Chrome, Firefox, Safari, Edge) +- App (iOS, Android) + +## 许可证 + +MIT License diff --git a/src/components/ImageSwiper/demo.vue b/src/components/ImageSwiper/demo.vue new file mode 100644 index 0000000..a99c386 --- /dev/null +++ b/src/components/ImageSwiper/demo.vue @@ -0,0 +1,260 @@ + + + ImageSwiper 轮播图组件演示 + + + + 示例1: 默认圆角 (8px) + + + + + + 示例2: 无圆角 (0px) + + + + + + 示例3: 大圆角 (20px) + + + + + + 示例4: 字符串圆角 (1rem) + + + + + + 示例5: 自定义图片数据 + 圆角15px + + + + + + 示例7: 多图片测试缩略图滑动 + + + + + + 示例6: 动态圆角控制 + + 圆角大小: {{ dynamicRadius }}px + + + + + + + + 示例8: 自定义高度 (300px) + + + + + + 示例9: 小高度轮播图 (120px) + + + + + + 示例10: 隐藏缩略图 + + + + + + 示例11: 动态高度和缩略图控制 + + 高度: {{ dynamicHeight }}px + + + + 显示缩略图 + + + + + + + + 示例12: 字符串高度 (50vh) + + + + + + + + diff --git a/src/components/ImageSwiper/images/2025-07-12_180248.jpg b/src/components/ImageSwiper/images/2025-07-12_180248.jpg new file mode 100644 index 0000000..c108857 Binary files /dev/null and b/src/components/ImageSwiper/images/2025-07-12_180248.jpg differ diff --git a/src/components/ImageSwiper/index.vue b/src/components/ImageSwiper/index.vue new file mode 100644 index 0000000..5b04ba3 --- /dev/null +++ b/src/components/ImageSwiper/index.vue @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + {{ + zniconsMap["zn-camera"] + }} + {{ thumbnails.length }} + {{ + zniconsMap["zn-nav-arrow-right"] + }} + + + + + + + + diff --git a/src/components/ImageSwiper/prompt.md b/src/components/ImageSwiper/prompt.md new file mode 100644 index 0000000..62dc411 --- /dev/null +++ b/src/components/ImageSwiper/prompt.md @@ -0,0 +1,15 @@ +## 图片详情组件 + +组件名称:图片详情组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片 100%还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.image-swiper +3、可以使用 uniapp swiper 内置的组件 +4、可以使用网络图片地址 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/ImageSwiper/styles/index.scss b/src/components/ImageSwiper/styles/index.scss new file mode 100644 index 0000000..efcc5e4 --- /dev/null +++ b/src/components/ImageSwiper/styles/index.scss @@ -0,0 +1,85 @@ +@font-face { + font-family: znicons; + src: url("@/static/fonts/znicons.ttf"); +} + +.image-swiper { + position: relative; + width: 100%; +} + +.swiper-box { + overflow: hidden; + // 高度和圆角通过内联样式动态设置 +} + +.swiper-item .swiper-item-image { + width: 100%; + height: 100%; +} + +.thumbnail-box { + position: absolute; + left: 12px; + right: 12px; + display: flex; + align-items: flex-end; + flex-direction: row; + flex-wrap: nowrap; + max-width: 100%; + box-sizing: border-box; +} + +.custom-indicator { + margin-left: 12px; + background: rgba(0, 0, 0, 0.5); + border-radius: $uni-border-radius-50px; + padding: 0 6px 4px 8px; + flex: 0 0 auto; + flex-shrink: 0; + white-space: nowrap; +} + +.custom-indicator-text { + margin: 0 4px; + font-size: 10px; + text-align: center; + align-items: center; + color: #fff; +} + +.thumbnail-scroll { + flex: 1 1 auto; + min-width: 0; // 允许在flex容器中收缩以适配剩余空间 + overflow: auto; // 防止超出thumbnail-box的宽度 + height: 100%; + white-space: nowrap; +} + +.thumbnail-list { + display: flex; + align-items: center; + gap: 10px; +} + +.thumbnail-item { + flex-shrink: 0; + text-align: center; + transition: all 0.3s ease; + + &.active { + .thumbnail-image { + border: 2px solid #fff; + } + } +} + +.thumbnail-item .thumbnail-image { + width: 36px; + height: 36px; + border-radius: 8px; + box-sizing: border-box; + border: 1px solid #171717; + transition: all 0.3s ease; + display: block; +} diff --git a/src/components/LocationCard/index.vue b/src/components/LocationCard/index.vue new file mode 100644 index 0000000..7448dcd --- /dev/null +++ b/src/components/LocationCard/index.vue @@ -0,0 +1,80 @@ + + + + 位于 {{ orderData.oneLevelAddress }} + {{ orderData.commodityAddress }} + + + + + + + + 导航 + + + + + + + 电话 + + + + + + + + diff --git a/src/components/LocationCard/styles/index.scss b/src/components/LocationCard/styles/index.scss new file mode 100644 index 0000000..867bbc5 --- /dev/null +++ b/src/components/LocationCard/styles/index.scss @@ -0,0 +1,56 @@ +.store-address { + display: flex; + align-items: center; + margin-top: 12px; + padding: 12px; + background-color: #ffffff; + border-top: 1px solid #e0e0e0; + + // 左侧文本容器 + .text-container { + display: flex; + flex-direction: column; + flex: 1; + } + + .location-label { + color: $text-color-800; + font-size: $uni-font-size-base; + font-weight: 500; + } + + .address-text { + margin-top: 4px; + color: $text-color-600; + font-size: $uni-font-size-sm; + font-weight: 400; + } + + // 右侧操作按钮组 + .actions { + display: flex; + align-items: center; + gap: 10px; + margin-left: 16px; + } + + .actions-btn { + width: 28px; + height: 28px; + border-radius: 10px; + background-color: #F5F5F5; + display: flex; + align-items: center; + justify-content: center; + } + + .actions-icon { + font-size: 16px; + line-height: 16px; + } + + .actions-text { + font-size: $uni-font-size-sm; + color: $text-color-600; + } +} diff --git a/src/components/LocationInfo/index.vue b/src/components/LocationInfo/index.vue new file mode 100644 index 0000000..4065c3e --- /dev/null +++ b/src/components/LocationInfo/index.vue @@ -0,0 +1,46 @@ + + + + {{ orderData.commodityAddress }} + + + + + + + diff --git a/src/components/LocationInfo/styles/index.scss b/src/components/LocationInfo/styles/index.scss new file mode 100644 index 0000000..aa5c6ea --- /dev/null +++ b/src/components/LocationInfo/styles/index.scss @@ -0,0 +1,14 @@ +.store-address { + display: flex; + align-items: center; + font-size: $uni-font-size-base; + color: $uni-text-color; + + text { + flex: 1; + padding: 0 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/src/components/ModuleTitle/images/2025-07-12_222027.png b/src/components/ModuleTitle/images/2025-07-12_222027.png new file mode 100644 index 0000000..c4c7b00 Binary files /dev/null and b/src/components/ModuleTitle/images/2025-07-12_222027.png differ diff --git a/src/components/ModuleTitle/images/wave_icon.png b/src/components/ModuleTitle/images/wave_icon.png new file mode 100644 index 0000000..40528f4 Binary files /dev/null and b/src/components/ModuleTitle/images/wave_icon.png differ diff --git a/src/components/ModuleTitle/images/wave_icon_b.png b/src/components/ModuleTitle/images/wave_icon_b.png new file mode 100644 index 0000000..5d164bb Binary files /dev/null and b/src/components/ModuleTitle/images/wave_icon_b.png differ diff --git a/src/components/ModuleTitle/index.vue b/src/components/ModuleTitle/index.vue new file mode 100644 index 0000000..7898b71 --- /dev/null +++ b/src/components/ModuleTitle/index.vue @@ -0,0 +1,27 @@ + + + {{ title }} + + + + + + + diff --git a/src/components/ModuleTitle/prompt.md b/src/components/ModuleTitle/prompt.md new file mode 100644 index 0000000..c906bbc --- /dev/null +++ b/src/components/ModuleTitle/prompt.md @@ -0,0 +1,14 @@ +## 图片详情组件 + +组件名称:模块标题组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片 100%还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.module-title +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/ModuleTitle/styles/index.scss b/src/components/ModuleTitle/styles/index.scss new file mode 100644 index 0000000..bfea704 --- /dev/null +++ b/src/components/ModuleTitle/styles/index.scss @@ -0,0 +1,22 @@ +.module-header { + position: relative; + padding: 6px 2px 2px; + display: inline-block; +} + +.module-title { + font-size: 18px; + font-weight: bold; + color: #171717; + position: relative; + z-index: 1; +} + +.underline { + position: absolute; + bottom: 3px; + left: 0; + width: 100%; + height: 10px; + z-index: 0; +} diff --git a/src/components/Privacy/index.vue b/src/components/Privacy/index.vue new file mode 100644 index 0000000..6f4892f --- /dev/null +++ b/src/components/Privacy/index.vue @@ -0,0 +1,86 @@ + + + + 隐私保护指引 + + 请您仔细阅读并充分理解{{ privacyContractName }} + ,如您同意前述协议的全部内容,请点击“同意”开始使用。如您不同意,将被限制使用部分功能,或将在您使用具体功能前再次询问以取得您的授权同意。 + + + + 拒绝 + + 同意 + + + + + + + + + diff --git a/src/components/Privacy/styles/index.scss b/src/components/Privacy/styles/index.scss new file mode 100644 index 0000000..d0ecc42 --- /dev/null +++ b/src/components/Privacy/styles/index.scss @@ -0,0 +1,66 @@ +.privacy { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + z-index: 9999; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: end; +} + +.title { + font-size: 20px; + font-weight: bold; + color: #000; + padding-bottom: 20rpx; +} + +.content { + position: relative; + background-color: #fff; + padding: 15px; + border-radius: 20px 20px 0 0; +} + +.des { + line-height: 21px; +} + +.link { + color: #007aff; +} + +.btns { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 0 40px; +} + +.reject, +.agree { + border-radius: $uni-border-radius-50px; + width: 45%; + border: none; + font-size: 18px; + margin: 0; + + &::after { + border: none; + } +} + +.reject { + color: #000; + background-color: #f5f5f5; + border-radius: $uni-border-radius-50px; +} + +.agree { + color: #fff; + background-color: #007aff; +} diff --git a/src/components/Qrcode/index.vue b/src/components/Qrcode/index.vue new file mode 100644 index 0000000..0b01c54 --- /dev/null +++ b/src/components/Qrcode/index.vue @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + diff --git a/src/components/Qrcode/qrcode.js b/src/components/Qrcode/qrcode.js new file mode 100644 index 0000000..6ea6553 --- /dev/null +++ b/src/components/Qrcode/qrcode.js @@ -0,0 +1,1201 @@ +let QRCode = {}; +(function () { + /** + * 获取单个字符的utf8编码 + * unicode BMP平面约65535个字符 + * @param {num} code + * return {array} + */ + function unicodeFormat8(code) { + // 1 byte + var c0, c1, c2; + if (code < 128) { + return [code]; + // 2 bytes + } else if (code < 2048) { + c0 = 192 + (code >> 6); + c1 = 128 + (code & 63); + return [c0, c1]; + // 3 bytes + } else { + c0 = 224 + (code >> 12); + c1 = 128 + (code >> 6 & 63); + c2 = 128 + (code & 63); + return [c0, c1, c2]; + } + } + /** + * 获取字符串的utf8编码字节串 + * @param {string} string + * @return {array} + */ + function getUTF8Bytes(string) { + var utf8codes = []; + for (var i = 0; i < string.length; i++) { + var code = string.charCodeAt(i); + var utf8 = unicodeFormat8(code); + for (var j = 0; j < utf8.length; j++) { + utf8codes.push(utf8[j]); + } + } + return utf8codes; + } + /** + * 二维码算法实现 + * @param {string} data 要编码的信息字符串 + * @param {num} errorCorrectLevel 纠错等级 + */ + function QRCodeAlg(data, errorCorrectLevel) { + this.typeNumber = -1; //版本 + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; //二维矩阵,存放最终结果 + this.moduleCount = 0; //矩阵大小 + this.dataCache = null; //数据缓存 + this.rsBlocks = null; //版本数据信息 + this.totalDataCount = -1; //可使用的数据量 + this.data = data; + this.utf8bytes = getUTF8Bytes(data); + this.make(); + } + QRCodeAlg.prototype = { + constructor: QRCodeAlg, + /** + * 获取二维码矩阵大小 + * @return {num} 矩阵大小 + */ + getModuleCount: function () { + return this.moduleCount; + }, + /** + * 编码 + */ + make: function () { + this.getRightType(); + this.dataCache = this.createData(); + this.createQrcode(); + }, + /** + * 设置二位矩阵功能图形 + * @param {bool} test 表示是否在寻找最好掩膜阶段 + * @param {num} maskPattern 掩膜的版本 + */ + makeImpl: function (maskPattern) { + this.moduleCount = this.typeNumber * 4 + 17; + this.modules = new Array(this.moduleCount); + for (var row = 0; row < this.moduleCount; row++) { + this.modules[row] = new Array(this.moduleCount); + } + this.setupPositionProbePattern(0, 0); + this.setupPositionProbePattern(this.moduleCount - 7, 0); + this.setupPositionProbePattern(0, this.moduleCount - 7); + this.setupPositionAdjustPattern(); + this.setupTimingPattern(); + this.setupTypeInfo(true, maskPattern); + if (this.typeNumber >= 7) { + this.setupTypeNumber(true); + } + this.mapData(this.dataCache, maskPattern); + }, + /** + * 设置二维码的位置探测图形 + * @param {num} row 探测图形的中心横坐标 + * @param {num} col 探测图形的中心纵坐标 + */ + setupPositionProbePattern: function (row, col) { + for (var r = -1; r <= 7; r++) { + if (row + r <= -1 || this.moduleCount <= row + r) continue; + for (var c = -1; c <= 7; c++) { + if (col + c <= -1 || this.moduleCount <= col + c) continue; + if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + }, + /** + * 创建二维码 + * @return {[type]} [description] + */ + createQrcode: function () { + var minLostPoint = 0; + var pattern = 0; + var bestModules = null; + for (var i = 0; i < 8; i++) { + this.makeImpl(i); + var lostPoint = QRUtil.getLostPoint(this); + if (i == 0 || minLostPoint > lostPoint) { + minLostPoint = lostPoint; + pattern = i; + bestModules = this.modules; + } + } + this.modules = bestModules; + this.setupTypeInfo(false, pattern); + if (this.typeNumber >= 7) { + this.setupTypeNumber(false); + } + }, + /** + * 设置定位图形 + * @return {[type]} [description] + */ + setupTimingPattern: function () { + for (var r = 8; r < this.moduleCount - 8; r++) { + if (this.modules[r][6] != null) { + continue; + } + this.modules[r][6] = (r % 2 == 0); + if (this.modules[6][r] != null) { + continue; + } + this.modules[6][r] = (r % 2 == 0); + } + }, + /** + * 设置矫正图形 + * @return {[type]} [description] + */ + setupPositionAdjustPattern: function () { + var pos = QRUtil.getPatternPosition(this.typeNumber); + for (var i = 0; i < pos.length; i++) { + for (var j = 0; j < pos.length; j++) { + var row = pos[i]; + var col = pos[j]; + if (this.modules[row][col] != null) { + continue; + } + for (var r = -2; r <= 2; r++) { + for (var c = -2; c <= 2; c++) { + if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { + this.modules[row + r][col + c] = true; + } else { + this.modules[row + r][col + c] = false; + } + } + } + } + } + }, + /** + * 设置版本信息(7以上版本才有) + * @param {bool} test 是否处于判断最佳掩膜阶段 + * @return {[type]} [description] + */ + setupTypeNumber: function (test) { + var bits = QRUtil.getBCHTypeNumber(this.typeNumber); + for (var i = 0; i < 18; i++) { + var mod = (!test && ((bits >> i) & 1) == 1); + this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod; + this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; + } + }, + /** + * 设置格式信息(纠错等级和掩膜版本) + * @param {bool} test + * @param {num} maskPattern 掩膜版本 + * @return {} + */ + setupTypeInfo: function (test, maskPattern) { + var data = (QRErrorCorrectLevel[this.errorCorrectLevel] << 3) | maskPattern; + var bits = QRUtil.getBCHTypeInfo(data); + // vertical + for (var i = 0; i < 15; i++) { + var mod = (!test && ((bits >> i) & 1) == 1); + if (i < 6) { + this.modules[i][8] = mod; + } else if (i < 8) { + this.modules[i + 1][8] = mod; + } else { + this.modules[this.moduleCount - 15 + i][8] = mod; + } + // horizontal + var mod = (!test && ((bits >> i) & 1) == 1); + if (i < 8) { + this.modules[8][this.moduleCount - i - 1] = mod; + } else if (i < 9) { + this.modules[8][15 - i - 1 + 1] = mod; + } else { + this.modules[8][15 - i - 1] = mod; + } + } + // fixed module + this.modules[this.moduleCount - 8][8] = (!test); + }, + /** + * 数据编码 + * @return {[type]} [description] + */ + createData: function () { + var buffer = new QRBitBuffer(); + var lengthBits = this.typeNumber > 9 ? 16 : 8; + buffer.put(4, 4); //添加模式 + buffer.put(this.utf8bytes.length, lengthBits); + for (var i = 0, l = this.utf8bytes.length; i < l; i++) { + buffer.put(this.utf8bytes[i], 8); + } + if (buffer.length + 4 <= this.totalDataCount * 8) { + buffer.put(0, 4); + } + // padding + while (buffer.length % 8 != 0) { + buffer.putBit(false); + } + // padding + while (true) { + if (buffer.length >= this.totalDataCount * 8) { + break; + } + buffer.put(QRCodeAlg.PAD0, 8); + if (buffer.length >= this.totalDataCount * 8) { + break; + } + buffer.put(QRCodeAlg.PAD1, 8); + } + return this.createBytes(buffer); + }, + /** + * 纠错码编码 + * @param {buffer} buffer 数据编码 + * @return {[type]} + */ + createBytes: function (buffer) { + var offset = 0; + var maxDcCount = 0; + var maxEcCount = 0; + var length = this.rsBlock.length / 3; + var rsBlocks = new Array(); + for (var i = 0; i < length; i++) { + var count = this.rsBlock[i * 3 + 0]; + var totalCount = this.rsBlock[i * 3 + 1]; + var dataCount = this.rsBlock[i * 3 + 2]; + for (var j = 0; j < count; j++) { + rsBlocks.push([dataCount, totalCount]); + } + } + var dcdata = new Array(rsBlocks.length); + var ecdata = new Array(rsBlocks.length); + for (var r = 0; r < rsBlocks.length; r++) { + var dcCount = rsBlocks[r][0]; + var ecCount = rsBlocks[r][1] - dcCount; + maxDcCount = Math.max(maxDcCount, dcCount); + maxEcCount = Math.max(maxEcCount, ecCount); + dcdata[r] = new Array(dcCount); + for (var i = 0; i < dcdata[r].length; i++) { + dcdata[r][i] = 0xff & buffer.buffer[i + offset]; + } + offset += dcCount; + var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount); + var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); + var modPoly = rawPoly.mod(rsPoly); + ecdata[r] = new Array(rsPoly.getLength() - 1); + for (var i = 0; i < ecdata[r].length; i++) { + var modIndex = i + modPoly.getLength() - ecdata[r].length; + ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0; + } + } + var data = new Array(this.totalDataCount); + var index = 0; + for (var i = 0; i < maxDcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < dcdata[r].length) { + data[index++] = dcdata[r][i]; + } + } + } + for (var i = 0; i < maxEcCount; i++) { + for (var r = 0; r < rsBlocks.length; r++) { + if (i < ecdata[r].length) { + data[index++] = ecdata[r][i]; + } + } + } + return data; + + }, + /** + * 布置模块,构建最终信息 + * @param {} data + * @param {} maskPattern + * @return {} + */ + mapData: function (data, maskPattern) { + var inc = -1; + var row = this.moduleCount - 1; + var bitIndex = 7; + var byteIndex = 0; + for (var col = this.moduleCount - 1; col > 0; col -= 2) { + if (col == 6) col--; + while (true) { + for (var c = 0; c < 2; c++) { + if (this.modules[row][col - c] == null) { + var dark = false; + if (byteIndex < data.length) { + dark = (((data[byteIndex] >>> bitIndex) & 1) == 1); + } + var mask = QRUtil.getMask(maskPattern, row, col - c); + if (mask) { + dark = !dark; + } + this.modules[row][col - c] = dark; + bitIndex--; + if (bitIndex == -1) { + byteIndex++; + bitIndex = 7; + } + } + } + row += inc; + if (row < 0 || this.moduleCount <= row) { + row -= inc; + inc = -inc; + break; + } + } + } + } + }; + /** + * 填充字段 + */ + QRCodeAlg.PAD0 = 0xEC; + QRCodeAlg.PAD1 = 0x11; + //--------------------------------------------------------------------- + // 纠错等级对应的编码 + //--------------------------------------------------------------------- + var QRErrorCorrectLevel = [1, 0, 3, 2]; + //--------------------------------------------------------------------- + // 掩膜版本 + //--------------------------------------------------------------------- + var QRMaskPattern = { + PATTERN000: 0, + PATTERN001: 1, + PATTERN010: 2, + PATTERN011: 3, + PATTERN100: 4, + PATTERN101: 5, + PATTERN110: 6, + PATTERN111: 7 + }; + //--------------------------------------------------------------------- + // 工具类 + //--------------------------------------------------------------------- + var QRUtil = { + /* + 每个版本矫正图形的位置 + */ + PATTERN_POSITION_TABLE: [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ], + G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), + G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), + G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), + /* + BCH编码格式信息 + */ + getBCHTypeInfo: function (data) { + var d = data << 10; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { + d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15))); + } + return ((data << 10) | d) ^ QRUtil.G15_MASK; + }, + /* + BCH编码版本信息 + */ + getBCHTypeNumber: function (data) { + var d = data << 12; + while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { + d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18))); + } + return (data << 12) | d; + }, + /* + 获取BCH位信息 + */ + getBCHDigit: function (data) { + var digit = 0; + while (data != 0) { + digit++; + data >>>= 1; + } + return digit; + }, + /* + 获取版本对应的矫正图形位置 + */ + getPatternPosition: function (typeNumber) { + return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; + }, + /* + 掩膜算法 + */ + getMask: function (maskPattern, i, j) { + switch (maskPattern) { + case QRMaskPattern.PATTERN000: + return (i + j) % 2 == 0; + case QRMaskPattern.PATTERN001: + return i % 2 == 0; + case QRMaskPattern.PATTERN010: + return j % 3 == 0; + case QRMaskPattern.PATTERN011: + return (i + j) % 3 == 0; + case QRMaskPattern.PATTERN100: + return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0; + case QRMaskPattern.PATTERN101: + return (i * j) % 2 + (i * j) % 3 == 0; + case QRMaskPattern.PATTERN110: + return ((i * j) % 2 + (i * j) % 3) % 2 == 0; + case QRMaskPattern.PATTERN111: + return ((i * j) % 3 + (i + j) % 2) % 2 == 0; + default: + throw new Error("bad maskPattern:" + maskPattern); + } + }, + /* + 获取RS的纠错多项式 + */ + getErrorCorrectPolynomial: function (errorCorrectLength) { + var a = new QRPolynomial([1], 0); + for (var i = 0; i < errorCorrectLength; i++) { + a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0)); + } + return a; + }, + /* + 获取评价 + */ + getLostPoint: function (qrCode) { + var moduleCount = qrCode.getModuleCount(), + lostPoint = 0, + darkCount = 0; + for (var row = 0; row < moduleCount; row++) { + var sameCount = 0; + var head = qrCode.modules[row][0]; + for (var col = 0; col < moduleCount; col++) { + var current = qrCode.modules[row][col]; + //level 3 评价 + if (col < moduleCount - 6) { + if (current && !qrCode.modules[row][col + 1] && qrCode.modules[row][col + 2] && qrCode.modules[row][col + 3] && qrCode.modules[row][col + 4] && !qrCode.modules[row][col + 5] && qrCode.modules[row][col + 6]) { + if (col < moduleCount - 10) { + if (qrCode.modules[row][col + 7] && qrCode.modules[row][col + 8] && qrCode.modules[row][col + 9] && qrCode.modules[row][col + 10]) { + lostPoint += 40; + } + } else if (col > 3) { + if (qrCode.modules[row][col - 1] && qrCode.modules[row][col - 2] && qrCode.modules[row][col - 3] && qrCode.modules[row][col - 4]) { + lostPoint += 40; + } + } + } + } + //level 2 评价 + if ((row < moduleCount - 1) && (col < moduleCount - 1)) { + var count = 0; + if (current) count++; + if (qrCode.modules[row + 1][col]) count++; + if (qrCode.modules[row][col + 1]) count++; + if (qrCode.modules[row + 1][col + 1]) count++; + if (count == 0 || count == 4) { + lostPoint += 3; + } + } + //level 1 评价 + if (head ^ current) { + sameCount++; + } else { + head = current; + if (sameCount >= 5) { + lostPoint += (3 + sameCount - 5); + } + sameCount = 1; + } + //level 4 评价 + if (current) { + darkCount++; + } + } + } + for (var col = 0; col < moduleCount; col++) { + var sameCount = 0; + var head = qrCode.modules[0][col]; + for (var row = 0; row < moduleCount; row++) { + var current = qrCode.modules[row][col]; + //level 3 评价 + if (row < moduleCount - 6) { + if (current && !qrCode.modules[row + 1][col] && qrCode.modules[row + 2][col] && qrCode.modules[row + 3][col] && qrCode.modules[row + 4][col] && !qrCode.modules[row + 5][col] && qrCode.modules[row + 6][col]) { + if (row < moduleCount - 10) { + if (qrCode.modules[row + 7][col] && qrCode.modules[row + 8][col] && qrCode.modules[row + 9][col] && qrCode.modules[row + 10][col]) { + lostPoint += 40; + } + } else if (row > 3) { + if (qrCode.modules[row - 1][col] && qrCode.modules[row - 2][col] && qrCode.modules[row - 3][col] && qrCode.modules[row - 4][col]) { + lostPoint += 40; + } + } + } + } + //level 1 评价 + if (head ^ current) { + sameCount++; + } else { + head = current; + if (sameCount >= 5) { + lostPoint += (3 + sameCount - 5); + } + sameCount = 1; + } + } + } + // LEVEL4 + var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5; + lostPoint += ratio * 10; + return lostPoint; + } + + }; + //--------------------------------------------------------------------- + // QRMath使用的数学工具 + //--------------------------------------------------------------------- + var QRMath = { + /* + 将n转化为a^m + */ + glog: function (n) { + if (n < 1) { + throw new Error("glog(" + n + ")"); + } + return QRMath.LOG_TABLE[n]; + }, + /* + 将a^m转化为n + */ + gexp: function (n) { + while (n < 0) { + n += 255; + } + while (n >= 256) { + n -= 255; + } + return QRMath.EXP_TABLE[n]; + }, + EXP_TABLE: new Array(256), + LOG_TABLE: new Array(256) + + }; + for (var i = 0; i < 8; i++) { + QRMath.EXP_TABLE[i] = 1 << i; + } + for (var i = 8; i < 256; i++) { + QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8]; + } + for (var i = 0; i < 255; i++) { + QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i; + } + //--------------------------------------------------------------------- + // QRPolynomial 多项式 + //--------------------------------------------------------------------- + /** + * 多项式类 + * @param {Array} num 系数 + * @param {num} shift a^shift + */ + function QRPolynomial(num, shift) { + if (num.length == undefined) { + throw new Error(num.length + "/" + shift); + } + var offset = 0; + while (offset < num.length && num[offset] == 0) { + offset++; + } + this.num = new Array(num.length - offset + shift); + for (var i = 0; i < num.length - offset; i++) { + this.num[i] = num[i + offset]; + } + } + QRPolynomial.prototype = { + get: function (index) { + return this.num[index]; + }, + getLength: function () { + return this.num.length; + }, + /** + * 多项式乘法 + * @param {QRPolynomial} e 被乘多项式 + * @return {[type]} [description] + */ + multiply: function (e) { + var num = new Array(this.getLength() + e.getLength() - 1); + for (var i = 0; i < this.getLength(); i++) { + for (var j = 0; j < e.getLength(); j++) { + num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j))); + } + } + return new QRPolynomial(num, 0); + }, + /** + * 多项式模运算 + * @param {QRPolynomial} e 模多项式 + * @return {} + */ + mod: function (e) { + var tl = this.getLength(), + el = e.getLength(); + if (tl - el < 0) { + return this; + } + var num = new Array(tl); + for (var i = 0; i < tl; i++) { + num[i] = this.get(i); + } + while (num.length >= el) { + var ratio = QRMath.glog(num[0]) - QRMath.glog(e.get(0)); + + for (var i = 0; i < e.getLength(); i++) { + num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio); + } + while (num[0] == 0) { + num.shift(); + } + } + return new QRPolynomial(num, 0); + } + }; + + //--------------------------------------------------------------------- + // RS_BLOCK_TABLE + //--------------------------------------------------------------------- + /* + 二维码各个版本信息[块数, 每块中的数据块数, 每块中的信息块数] + */ + var RS_BLOCK_TABLE = [ + // L + // M + // Q + // H + // 1 + [1, 26, 19], + [1, 26, 16], + [1, 26, 13], + [1, 26, 9], + + // 2 + [1, 44, 34], + [1, 44, 28], + [1, 44, 22], + [1, 44, 16], + + // 3 + [1, 70, 55], + [1, 70, 44], + [2, 35, 17], + [2, 35, 13], + + // 4 + [1, 100, 80], + [2, 50, 32], + [2, 50, 24], + [4, 25, 9], + + // 5 + [1, 134, 108], + [2, 67, 43], + [2, 33, 15, 2, 34, 16], + [2, 33, 11, 2, 34, 12], + + // 6 + [2, 86, 68], + [4, 43, 27], + [4, 43, 19], + [4, 43, 15], + + // 7 + [2, 98, 78], + [4, 49, 31], + [2, 32, 14, 4, 33, 15], + [4, 39, 13, 1, 40, 14], + + // 8 + [2, 121, 97], + [2, 60, 38, 2, 61, 39], + [4, 40, 18, 2, 41, 19], + [4, 40, 14, 2, 41, 15], + + // 9 + [2, 146, 116], + [3, 58, 36, 2, 59, 37], + [4, 36, 16, 4, 37, 17], + [4, 36, 12, 4, 37, 13], + + // 10 + [2, 86, 68, 2, 87, 69], + [4, 69, 43, 1, 70, 44], + [6, 43, 19, 2, 44, 20], + [6, 43, 15, 2, 44, 16], + + // 11 + [4, 101, 81], + [1, 80, 50, 4, 81, 51], + [4, 50, 22, 4, 51, 23], + [3, 36, 12, 8, 37, 13], + + // 12 + [2, 116, 92, 2, 117, 93], + [6, 58, 36, 2, 59, 37], + [4, 46, 20, 6, 47, 21], + [7, 42, 14, 4, 43, 15], + + // 13 + [4, 133, 107], + [8, 59, 37, 1, 60, 38], + [8, 44, 20, 4, 45, 21], + [12, 33, 11, 4, 34, 12], + + // 14 + [3, 145, 115, 1, 146, 116], + [4, 64, 40, 5, 65, 41], + [11, 36, 16, 5, 37, 17], + [11, 36, 12, 5, 37, 13], + + // 15 + [5, 109, 87, 1, 110, 88], + [5, 65, 41, 5, 66, 42], + [5, 54, 24, 7, 55, 25], + [11, 36, 12], + + // 16 + [5, 122, 98, 1, 123, 99], + [7, 73, 45, 3, 74, 46], + [15, 43, 19, 2, 44, 20], + [3, 45, 15, 13, 46, 16], + + // 17 + [1, 135, 107, 5, 136, 108], + [10, 74, 46, 1, 75, 47], + [1, 50, 22, 15, 51, 23], + [2, 42, 14, 17, 43, 15], + + // 18 + [5, 150, 120, 1, 151, 121], + [9, 69, 43, 4, 70, 44], + [17, 50, 22, 1, 51, 23], + [2, 42, 14, 19, 43, 15], + + // 19 + [3, 141, 113, 4, 142, 114], + [3, 70, 44, 11, 71, 45], + [17, 47, 21, 4, 48, 22], + [9, 39, 13, 16, 40, 14], + + // 20 + [3, 135, 107, 5, 136, 108], + [3, 67, 41, 13, 68, 42], + [15, 54, 24, 5, 55, 25], + [15, 43, 15, 10, 44, 16], + + // 21 + [4, 144, 116, 4, 145, 117], + [17, 68, 42], + [17, 50, 22, 6, 51, 23], + [19, 46, 16, 6, 47, 17], + + // 22 + [2, 139, 111, 7, 140, 112], + [17, 74, 46], + [7, 54, 24, 16, 55, 25], + [34, 37, 13], + + // 23 + [4, 151, 121, 5, 152, 122], + [4, 75, 47, 14, 76, 48], + [11, 54, 24, 14, 55, 25], + [16, 45, 15, 14, 46, 16], + + // 24 + [6, 147, 117, 4, 148, 118], + [6, 73, 45, 14, 74, 46], + [11, 54, 24, 16, 55, 25], + [30, 46, 16, 2, 47, 17], + + // 25 + [8, 132, 106, 4, 133, 107], + [8, 75, 47, 13, 76, 48], + [7, 54, 24, 22, 55, 25], + [22, 45, 15, 13, 46, 16], + + // 26 + [10, 142, 114, 2, 143, 115], + [19, 74, 46, 4, 75, 47], + [28, 50, 22, 6, 51, 23], + [33, 46, 16, 4, 47, 17], + + // 27 + [8, 152, 122, 4, 153, 123], + [22, 73, 45, 3, 74, 46], + [8, 53, 23, 26, 54, 24], + [12, 45, 15, 28, 46, 16], + + // 28 + [3, 147, 117, 10, 148, 118], + [3, 73, 45, 23, 74, 46], + [4, 54, 24, 31, 55, 25], + [11, 45, 15, 31, 46, 16], + + // 29 + [7, 146, 116, 7, 147, 117], + [21, 73, 45, 7, 74, 46], + [1, 53, 23, 37, 54, 24], + [19, 45, 15, 26, 46, 16], + + // 30 + [5, 145, 115, 10, 146, 116], + [19, 75, 47, 10, 76, 48], + [15, 54, 24, 25, 55, 25], + [23, 45, 15, 25, 46, 16], + + // 31 + [13, 145, 115, 3, 146, 116], + [2, 74, 46, 29, 75, 47], + [42, 54, 24, 1, 55, 25], + [23, 45, 15, 28, 46, 16], + + // 32 + [17, 145, 115], + [10, 74, 46, 23, 75, 47], + [10, 54, 24, 35, 55, 25], + [19, 45, 15, 35, 46, 16], + + // 33 + [17, 145, 115, 1, 146, 116], + [14, 74, 46, 21, 75, 47], + [29, 54, 24, 19, 55, 25], + [11, 45, 15, 46, 46, 16], + + // 34 + [13, 145, 115, 6, 146, 116], + [14, 74, 46, 23, 75, 47], + [44, 54, 24, 7, 55, 25], + [59, 46, 16, 1, 47, 17], + + // 35 + [12, 151, 121, 7, 152, 122], + [12, 75, 47, 26, 76, 48], + [39, 54, 24, 14, 55, 25], + [22, 45, 15, 41, 46, 16], + + // 36 + [6, 151, 121, 14, 152, 122], + [6, 75, 47, 34, 76, 48], + [46, 54, 24, 10, 55, 25], + [2, 45, 15, 64, 46, 16], + + // 37 + [17, 152, 122, 4, 153, 123], + [29, 74, 46, 14, 75, 47], + [49, 54, 24, 10, 55, 25], + [24, 45, 15, 46, 46, 16], + + // 38 + [4, 152, 122, 18, 153, 123], + [13, 74, 46, 32, 75, 47], + [48, 54, 24, 14, 55, 25], + [42, 45, 15, 32, 46, 16], + + // 39 + [20, 147, 117, 4, 148, 118], + [40, 75, 47, 7, 76, 48], + [43, 54, 24, 22, 55, 25], + [10, 45, 15, 67, 46, 16], + + // 40 + [19, 148, 118, 6, 149, 119], + [18, 75, 47, 31, 76, 48], + [34, 54, 24, 34, 55, 25], + [20, 45, 15, 61, 46, 16] + ]; + + /** + * 根据数据获取对应版本 + * @return {[type]} [description] + */ + QRCodeAlg.prototype.getRightType = function () { + for (var typeNumber = 1; typeNumber < 41; typeNumber++) { + var rsBlock = RS_BLOCK_TABLE[(typeNumber - 1) * 4 + this.errorCorrectLevel]; + if (rsBlock == undefined) { + throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + this.errorCorrectLevel); + } + var length = rsBlock.length / 3; + var totalDataCount = 0; + for (var i = 0; i < length; i++) { + var count = rsBlock[i * 3 + 0]; + var dataCount = rsBlock[i * 3 + 2]; + totalDataCount += dataCount * count; + } + var lengthBytes = typeNumber > 9 ? 2 : 1; + if (this.utf8bytes.length + lengthBytes < totalDataCount || typeNumber == 40) { + this.typeNumber = typeNumber; + this.rsBlock = rsBlock; + this.totalDataCount = totalDataCount; + break; + } + } + }; + + //--------------------------------------------------------------------- + // QRBitBuffer + //--------------------------------------------------------------------- + function QRBitBuffer() { + this.buffer = new Array(); + this.length = 0; + } + QRBitBuffer.prototype = { + get: function (index) { + var bufIndex = Math.floor(index / 8); + return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1); + }, + put: function (num, length) { + for (var i = 0; i < length; i++) { + this.putBit(((num >>> (length - i - 1)) & 1)); + } + }, + putBit: function (bit) { + var bufIndex = Math.floor(this.length / 8); + if (this.buffer.length <= bufIndex) { + this.buffer.push(0); + } + if (bit) { + this.buffer[bufIndex] |= (0x80 >>> (this.length % 8)); + } + this.length++; + } + }; + + + + // xzedit + let qrcodeAlgObjCache = []; + /** + * 二维码构造函数,主要用于绘制 + * @param {参数列表} opt 传递参数 + * @return {} + */ + QRCode = function (opt) { + //设置默认参数 + this.options = { + text: '', + size: 256, + correctLevel: 3, + background: '#ffffff', + foreground: '#000000', + pdground: '#000000', + image: '', + imageSize: 30, + canvasId: opt.canvasId, + context: opt.context, + usingComponents: opt.usingComponents, + showLoading: opt.showLoading, + loadingText: opt.loadingText, + }; + if (typeof opt === 'string') { // 只编码ASCII字符串 + opt = { + text: opt + }; + } + if (opt) { + for (var i in opt) { + this.options[i] = opt[i]; + } + } + //使用QRCodeAlg创建二维码结构 + var qrCodeAlg = null; + for (var i = 0, l = qrcodeAlgObjCache.length; i < l; i++) { + if (qrcodeAlgObjCache[i].text == this.options.text && qrcodeAlgObjCache[i].text.correctLevel == this.options.correctLevel) { + qrCodeAlg = qrcodeAlgObjCache[i].obj; + break; + } + } + if (i == l) { + qrCodeAlg = new QRCodeAlg(this.options.text, this.options.correctLevel); + qrcodeAlgObjCache.push({ + text: this.options.text, + correctLevel: this.options.correctLevel, + obj: qrCodeAlg + }); + } + /** + * 计算矩阵点的前景色 + * @param {Obj} config + * @param {Number} config.row 点x坐标 + * @param {Number} config.col 点y坐标 + * @param {Number} config.count 矩阵大小 + * @param {Number} config.options 组件的options + * @return {String} + */ + let getForeGround = function (config) { + var options = config.options; + if (options.pdground && ( + (config.row > 1 && config.row < 5 && config.col > 1 && config.col < 5) || + (config.row > (config.count - 6) && config.row < (config.count - 2) && config.col > 1 && config.col < 5) || + (config.row > 1 && config.row < 5 && config.col > (config.count - 6) && config.col < (config.count - 2)) + )) { + return options.pdground; + } + return options.foreground; + } + // 创建canvas + let createCanvas = function (options) { + if (options.showLoading) { + uni.showLoading({ + title: options.loadingText, + mask: true + }); + } + var ctx = uni.createCanvasContext(options.canvasId, options.context); + var count = qrCodeAlg.getModuleCount(); + var ratioSize = options.size; + var ratioImgSize = options.imageSize; + //计算每个点的长宽 + var tileW = (ratioSize / count).toPrecision(4); + var tileH = (ratioSize / count).toPrecision(4); + //绘制 + for (var row = 0; row < count; row++) { + for (var col = 0; col < count; col++) { + var w = (Math.ceil((col + 1) * tileW) - Math.floor(col * tileW)); + var h = (Math.ceil((row + 1) * tileW) - Math.floor(row * tileW)); + var foreground = getForeGround({ + row: row, + col: col, + count: count, + options: options + }); + ctx.setFillStyle(qrCodeAlg.modules[row][col] ? foreground : options.background); + ctx.fillRect(Math.round(col * tileW), Math.round(row * tileH), w, h); + } + } + if (options.image) { + var x = Number(((ratioSize - ratioImgSize) / 2).toFixed(2)); + var y = Number(((ratioSize - ratioImgSize) / 2).toFixed(2)); + drawRoundedRect(ctx, x, y, ratioImgSize, ratioImgSize, 2, 6, true, true) + ctx.drawImage(options.image, x, y, ratioImgSize, ratioImgSize); + // 画圆角矩形 + function drawRoundedRect(ctxi, x, y, width, height, r, lineWidth, fill, stroke) { + ctxi.setLineWidth(lineWidth); + ctxi.setFillStyle(options.background); + ctxi.setStrokeStyle(options.background); + ctxi.beginPath(); // draw top and top right corner + ctxi.moveTo(x + r, y); + ctxi.arcTo(x + width, y, x + width, y + r, r); // draw right side and bottom right corner + ctxi.arcTo(x + width, y + height, x + width - r, y + height, r); // draw bottom and bottom left corner + ctxi.arcTo(x, y + height, x, y + height - r, r); // draw left and top left corner + ctxi.arcTo(x, y, x + r, y, r); + ctxi.closePath(); + if (fill) { + ctxi.fill(); + } + if (stroke) { + ctxi.stroke(); + } + } + } + setTimeout(() => { + ctx.draw(true, () => { + // 保存到临时区域 + setTimeout(() => { + uni.canvasToTempFilePath({ + width: options.width, + height: options.height, + destWidth: options.width, + destHeight: options.height, + canvasId: options.canvasId, + quality: Number(1), + success: function (res) { + if (options.cbResult) { + options.cbResult(res.tempFilePath) + } + }, + fail: function (res) { + if (options.cbResult) { + options.cbResult(res) + } + }, + complete: function () { + if (options.showLoading){ + uni.hideLoading(); + } + }, + }, options.context); + }, options.text.length + 100); + }); + }, options.usingComponents ? 0 : 150); + } + createCanvas(this.options); + // 空判定 + let empty = function (v) { + let tp = typeof v, + rt = false; + if (tp == "number" && String(v) == "") { + rt = true + } else if (tp == "undefined") { + rt = true + } else if (tp == "object") { + if (JSON.stringify(v) == "{}" || JSON.stringify(v) == "[]" || v == null) rt = true + } else if (tp == "string") { + if (v == "" || v == "undefined" || v == "null" || v == "{}" || v == "[]") rt = true + } else if (tp == "function") { + rt = false + } + return rt + } + }; + QRCode.prototype.clear = function (fn) { + var ctx = uni.createCanvasContext(this.options.canvasId, this.options.context) + ctx.clearRect(0, 0, this.options.size, this.options.size) + ctx.draw(false, () => { + if (fn) { + fn() + } + }) + }; +})() + +export default QRCode \ No newline at end of file diff --git a/src/components/RefundPopup/index.vue b/src/components/RefundPopup/index.vue new file mode 100644 index 0000000..6adf2d6 --- /dev/null +++ b/src/components/RefundPopup/index.vue @@ -0,0 +1,123 @@ + + + + + + 取消政策 + + + + + + + + + {{ zniconsMap["zn-refund"] }} + + + {{ refundTitle }} + + + + {{ item }} + + + + + + + + + diff --git a/src/components/RefundPopup/styles/index.scss b/src/components/RefundPopup/styles/index.scss new file mode 100644 index 0000000..57425d5 --- /dev/null +++ b/src/components/RefundPopup/styles/index.scss @@ -0,0 +1,9 @@ +.refund-popup { + border-radius: 15px 15px 0 0; + padding-bottom: 40px; +} + +.close { + top: 14px; + right: 12px; +} diff --git a/src/components/ResponseIntro/images/2025-07-14_155933.png b/src/components/ResponseIntro/images/2025-07-14_155933.png new file mode 100644 index 0000000..1882d8a Binary files /dev/null and b/src/components/ResponseIntro/images/2025-07-14_155933.png differ diff --git a/src/components/ResponseIntro/index.vue b/src/components/ResponseIntro/index.vue new file mode 100644 index 0000000..3d3664c --- /dev/null +++ b/src/components/ResponseIntro/index.vue @@ -0,0 +1,21 @@ + + + {{ introText }} + + + + + + diff --git a/src/components/ResponseIntro/prompt.md b/src/components/ResponseIntro/prompt.md new file mode 100644 index 0000000..e0f2ea6 --- /dev/null +++ b/src/components/ResponseIntro/prompt.md @@ -0,0 +1,14 @@ +## 消息响应体文本介绍组件 + +组件名称:消息响应体文本介绍组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、参考图片,还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.response-intro-text +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/ResponseIntro/styles/index.scss b/src/components/ResponseIntro/styles/index.scss new file mode 100644 index 0000000..d78a423 --- /dev/null +++ b/src/components/ResponseIntro/styles/index.scss @@ -0,0 +1,11 @@ +.response-intro-wrapper { + padding: 4px 8px 12px 12px; +} + +.response-intro-text { + font-weight: 500; + font-size: $uni-font-size-base; + color: $uni-text-color; + line-height: 20px; + text-align: justify; +} diff --git a/src/components/ResponseWrapper/index.vue b/src/components/ResponseWrapper/index.vue new file mode 100644 index 0000000..b1dc954 --- /dev/null +++ b/src/components/ResponseWrapper/index.vue @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/src/components/ResponseWrapper/styles/index.scss b/src/components/ResponseWrapper/styles/index.scss new file mode 100644 index 0000000..a352aeb --- /dev/null +++ b/src/components/ResponseWrapper/styles/index.scss @@ -0,0 +1,7 @@ +.response-wrapper { + background: rgba(255, 255, 255, 0.4); + box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.1); + border-radius: 4px 20px 20px 20px; + border: 1px solid #fff; + padding: 12px; +} diff --git a/src/components/ServiceTipsWord/images/2025-07-11_141148.png b/src/components/ServiceTipsWord/images/2025-07-11_141148.png new file mode 100644 index 0000000..5b61cd2 Binary files /dev/null and b/src/components/ServiceTipsWord/images/2025-07-11_141148.png differ diff --git a/src/components/ServiceTipsWord/images/icon_refresh.png b/src/components/ServiceTipsWord/images/icon_refresh.png new file mode 100644 index 0000000..bf85d77 Binary files /dev/null and b/src/components/ServiceTipsWord/images/icon_refresh.png differ diff --git a/src/components/ServiceTipsWord/index.vue b/src/components/ServiceTipsWord/index.vue new file mode 100644 index 0000000..d9b6866 --- /dev/null +++ b/src/components/ServiceTipsWord/index.vue @@ -0,0 +1,40 @@ + + + + 你可以这样问我 + + + + + 帮我加一张床 + + + 房间热水不够热 + + + 帮忙加一台麻将机 + + + + + + + + diff --git a/src/components/ServiceTipsWord/prompt.md b/src/components/ServiceTipsWord/prompt.md new file mode 100644 index 0000000..5d3fa49 --- /dev/null +++ b/src/components/ServiceTipsWord/prompt.md @@ -0,0 +1,16 @@ +## 消息体提示词组件 + +组件名称:消息体提示词组件 +帮我加一张床、房间热水不够热、帮我加一台麻将机 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.service-prompt +3、可以使用 uniapp 内置的组件 +4、帮忙加一张床、房间热水不够热、帮忙加一台麻将机有点击交互 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/ServiceTipsWord/styles/index.scss b/src/components/ServiceTipsWord/styles/index.scss new file mode 100644 index 0000000..54dcda8 --- /dev/null +++ b/src/components/ServiceTipsWord/styles/index.scss @@ -0,0 +1,53 @@ +.service-prompt { + padding: 12px 18px; +} + +.service-header { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.header-text { + font-size: $uni-font-size-sm; + color: #6b84a2; +} + +.header-icon { + width: 9px; + height: 9px; + margin-left: 8px; +} + +.service-buttons { + display: flex; + flex-wrap: nowrap; +} + +.service-button { + height: 36px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; + padding-left: 12px; + padding-right: 12px; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0.5) 0%, + #ffffff 100% + ); + box-shadow: 0px 2px 6px 0px rgba(16, 78, 137, 0.1); + border-radius: 20px 20px 20px 20px; + border: 1px solid #fff; + border-image: linear-gradient( + 137deg, + rgba(255, 255, 255, 1), + rgba(255, 255, 255, 0.5), + rgba(255, 255, 255, 1) + ); + font-size: $uni-font-size-sm; + font-weight: 500; + color: $theme-color-500; + border-radius: $uni-border-radius-50px; +} diff --git a/src/components/Speech/RecordingWaveBtn.vue b/src/components/Speech/RecordingWaveBtn.vue new file mode 100644 index 0000000..4cb2b81 --- /dev/null +++ b/src/components/Speech/RecordingWaveBtn.vue @@ -0,0 +1,129 @@ + + + + + + + + + + + diff --git a/src/components/Sprite/SpriteAnimator.vue b/src/components/Sprite/SpriteAnimator.vue new file mode 100644 index 0000000..f799e34 --- /dev/null +++ b/src/components/Sprite/SpriteAnimator.vue @@ -0,0 +1,118 @@ + + + + + + + diff --git a/src/components/Stepper/README.md b/src/components/Stepper/README.md new file mode 100644 index 0000000..2411ffc --- /dev/null +++ b/src/components/Stepper/README.md @@ -0,0 +1,72 @@ +# Stepper 数字步进器组件 + +一个简洁易用的数字步进器组件,支持增减操作和数值范围限制。 + +## 功能特性 + +- ✨ **双向数据绑定**:支持 v-model 语法糖 +- 🔢 **数值范围控制**:可设置最小值和最大值 +- 🎯 **响应式更新**:实时响应外部数值变化 +- 🎨 **简洁UI设计**:使用 uni-icons 图标,界面清爽 +- 📱 **移动端适配**:完美适配各种屏幕尺寸 + +## 使用方法 + +### 基础用法 + +```vue + + + + + +``` + +### 设置数值范围 + +```vue + + + +``` + +## API + +### Props + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| modelValue | Number | 1 | 当前数值,支持 v-model | +| min | Number | 1 | 最小值 | +| max | Number | 100 | 最大值 | + +### Events + +| 事件名 | 说明 | 回调参数 | +|--------|------|----------| +| update:modelValue | 数值变化时触发 | (value: number) | + +## 更新日志 + +### v1.1.0 (2024-12-19) +**修复响应性问题** +- 🐛 修复组件不响应外部 modelValue 变化的问题 +- 🔄 添加 watch 监听器,确保实时同步外部值变化 +- ✨ 提升组件响应性和数据同步准确性 +- 🎯 完善与父组件的双向数据绑定 + +### v1.0.0 +**初始版本** +- ✨ 基础数字步进器功能 +- 🔢 支持数值范围控制 +- 🎨 简洁的UI设计 +- 📱 移动端适配 \ No newline at end of file diff --git a/src/components/Stepper/images/2025-07-15_100917.png b/src/components/Stepper/images/2025-07-15_100917.png new file mode 100644 index 0000000..881189f Binary files /dev/null and b/src/components/Stepper/images/2025-07-15_100917.png differ diff --git a/src/components/Stepper/index.vue b/src/components/Stepper/index.vue new file mode 100644 index 0000000..9633d7a --- /dev/null +++ b/src/components/Stepper/index.vue @@ -0,0 +1,73 @@ + + + + + {{ value }} {{ unit }} + + + + + + + + diff --git a/src/components/Stepper/propmt.md b/src/components/Stepper/propmt.md new file mode 100644 index 0000000..efbfc76 --- /dev/null +++ b/src/components/Stepper/propmt.md @@ -0,0 +1,12 @@ +## 步进器组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、参考图片,高度还原交互设计,完成组件封装 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.stepper-wrapper +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/Stepper/styles/index.scss b/src/components/Stepper/styles/index.scss new file mode 100644 index 0000000..2b3f89c --- /dev/null +++ b/src/components/Stepper/styles/index.scss @@ -0,0 +1,8 @@ +.stepper-wrapper { + background-color: #f3f9ff; + padding: 4px 7px; +} + +.stepper-text { + max-width: 40px; +} diff --git a/src/components/SumCard/images/2025-07-15_154422.png b/src/components/SumCard/images/2025-07-15_154422.png new file mode 100644 index 0000000..e464b36 Binary files /dev/null and b/src/components/SumCard/images/2025-07-15_154422.png differ diff --git a/src/components/SumCard/index.vue b/src/components/SumCard/index.vue new file mode 100644 index 0000000..d41e914 --- /dev/null +++ b/src/components/SumCard/index.vue @@ -0,0 +1,29 @@ + + + + 价格 + ¥{{ referencePrice }} + + + 折扣优惠 + -¥{{ discount }} + + + + + + + diff --git a/src/components/SumCard/propmt.md b/src/components/SumCard/propmt.md new file mode 100644 index 0000000..095fcf5 --- /dev/null +++ b/src/components/SumCard/propmt.md @@ -0,0 +1,12 @@ +## 价格组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、参考图片,高度还原交互设计,完成组件封装 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.sum-wrapper +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/SumCard/styles/index.scss b/src/components/SumCard/styles/index.scss new file mode 100644 index 0000000..bdcc88c --- /dev/null +++ b/src/components/SumCard/styles/index.scss @@ -0,0 +1,33 @@ +.sum-wrapper { + border-radius: 8px; + background-color: #f5f5f5; + padding: 0 12px; + box-sizing: border-box; +} + +.sum-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-bottom: 1px solid #ddd; + + &:last-child { + border-bottom: none; + } +} + +.sum-label { + font-size: 15px; + color: $uni-text-color; +} + +.sum-value { + font-size: $uni-font-size-base; + color: $uni-text-color-grey; +} + +.sum-discount { + font-size: $uni-font-size-base; + color: #ff5722; +} diff --git a/src/components/SurveyQuestionnaire/index.vue b/src/components/SurveyQuestionnaire/index.vue new file mode 100644 index 0000000..4d43d12 --- /dev/null +++ b/src/components/SurveyQuestionnaire/index.vue @@ -0,0 +1,51 @@ + + + + + + 调查问卷 + + + + + + + + 前往填写 + + + + + + + + diff --git a/src/components/SwipeCards/index.vue b/src/components/SwipeCards/index.vue new file mode 100644 index 0000000..298c446 --- /dev/null +++ b/src/components/SwipeCards/index.vue @@ -0,0 +1,239 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/components/SwipeCards/styles/index.scss b/src/components/SwipeCards/styles/index.scss new file mode 100644 index 0000000..d3bab55 --- /dev/null +++ b/src/components/SwipeCards/styles/index.scss @@ -0,0 +1,61 @@ +.card { + height: 308px; +} + +.card-item { + inset: 0; + will-change: transform, opacity; + height: 277px; + box-shadow: 0 8px 8px rgba(0, 0, 0, 0.08); + border-radius: 20px; +} + +.inner-card { + width: 100%; + height: 100%; +} + +/* 商品大图部分:撑满除相册外的空间 */ +.goods-image-wrapper { + width: 100%; + height: 193px; +} + + +.album-item { + width: 96px; + height: 60px; +} + +.album-title { + background: rgba(0, 0, 0, 0.5); + height: 20px; +} + +/* 底部价格与购买按钮 */ +.card-footer { + border-top: 1px solid rgba(0,0,0,0.04); +} + +.card-footer .price-left { + display: flex; + align-items: baseline; + color: #171717; +} + +.buy-btn { + background-color: $theme-color-500; + color: #fff; + margin-top: 12px; + padding: 12px 24px; + border-radius: 12px; + font-size: 14px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.buy-btn:active { + opacity: 0.9; +} diff --git a/src/components/TagsGroup/images/2025-07-13_102028.png b/src/components/TagsGroup/images/2025-07-13_102028.png new file mode 100644 index 0000000..d850fd5 Binary files /dev/null and b/src/components/TagsGroup/images/2025-07-13_102028.png differ diff --git a/src/components/TagsGroup/index.vue b/src/components/TagsGroup/index.vue new file mode 100644 index 0000000..ed54ec4 --- /dev/null +++ b/src/components/TagsGroup/index.vue @@ -0,0 +1,15 @@ + + + + {{ tag }} + + + + + + + diff --git a/src/components/TagsGroup/prompt.md b/src/components/TagsGroup/prompt.md new file mode 100644 index 0000000..ff73993 --- /dev/null +++ b/src/components/TagsGroup/prompt.md @@ -0,0 +1,14 @@ +## 标签组件 + +组件名称:标签组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片 100%还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.tags-group +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/components/TagsGroup/styles/index.scss b/src/components/TagsGroup/styles/index.scss new file mode 100644 index 0000000..e1be6a2 --- /dev/null +++ b/src/components/TagsGroup/styles/index.scss @@ -0,0 +1,13 @@ +.tags-group { + display: flex; + gap: 8px; +} + +.tag-item { + background-color: #fff; + border-radius: 10px; + padding: 8px 16px; + font-size: $uni-font-size-sm; + color: $theme-color-500; + white-space: nowrap; +} diff --git a/src/components/TopNavBar/README.md b/src/components/TopNavBar/README.md new file mode 100644 index 0000000..679f3d7 --- /dev/null +++ b/src/components/TopNavBar/README.md @@ -0,0 +1,180 @@ +# TopNavBar 顶部导航栏组件 + +一个功能完整的顶部导航栏组件,支持固定定位、自定义样式和插槽内容。 + +## 功能特性 + +- ✅ 支持固定在页面顶部(可配置) +- ✅ 自动适配状态栏高度 +- ✅ 支持自定义标题和颜色 +- ✅ 支持插槽自定义内容 +- ✅ 内置返回按钮功能 +- ✅ 响应式设计 +- ✅ 深色模式支持 +- ✅ 安全区域适配 + +## 基础用法 + +### 简单使用 + +```vue + + + + + +``` + +### 固定在顶部 + +```vue + + + +``` + +### 自定义样式 + +```vue + + + +``` + +### 标题对齐方式 + +```vue + + + + + + + +``` + +### 使用插槽 + +```vue + + + + + 自定义标题内容 + + + + + + + + +``` + +## API + +### Props + +| 参数 | 类型 | 默认值 | 说明 | +| --------------- | ------- | --------- | -------------------------------------- | +| title | String | '' | 导航栏标题 | +| fixed | Boolean | false | 是否固定在页面顶部 | +| showBack | Boolean | true | 是否显示返回按钮 | +| backgroundColor | String | '#ffffff' | 背景颜色 | +| titleColor | String | '#333333' | 标题文字颜色 | +| backIconColor | String | '#333333' | 返回按钮图标颜色 | +| hideStatusBar | Boolean | false | 是否隐藏状态栏占位 | +| zIndex | Number | 999 | 层级索引 | +| titleAlign | String | 'center' | 标题对齐方式,可选值:'center'、'left' | + +### Events + +| 事件名 | 说明 | 参数 | +| ------ | ------------------ | ---- | +| back | 点击返回按钮时触发 | - | + +### Slots + +| 插槽名 | 说明 | +| ------ | -------------- | +| title | 自定义标题内容 | +| right | 自定义右侧内容 | + +## 使用示例 + +### 订单列表页面 + +```vue + + + + + + + + + + + + + + +``` + +### 商品详情页面 + +```vue + + + + + + + + + + + + + + + + +``` + +## 注意事项 + +1. **固定定位使用**:当设置 `fixed="true"` 时,组件会固定在页面顶部,此时需要为页面内容添加适当的顶部间距。 + +2. **状态栏适配**:组件会自动获取系统状态栏高度并进行适配,无需手动处理。 + +3. **返回按钮**:默认点击返回按钮会执行 `uni.navigateBack()`,如果需要自定义返回逻辑,请监听 `@back` 事件。 + +4. **样式覆盖**:如需自定义样式,建议通过 props 传入颜色值,或在父组件中使用深度选择器覆盖样式。 + +5. **插槽使用**:title 插槽会完全替换默认的标题显示,right 插槽用于添加右侧操作按钮。 + +## 更新日志 + +### v1.0.0 + +- 初始版本发布 +- 支持基础导航栏功能 +- 支持固定定位配置 +- 支持自定义样式和插槽 diff --git a/src/components/TopNavBar/demo.vue b/src/components/TopNavBar/demo.vue new file mode 100644 index 0000000..f99b2cd --- /dev/null +++ b/src/components/TopNavBar/demo.vue @@ -0,0 +1,156 @@ + + + + + 基础用法 + + + + + + 固定在顶部 + + + + + + 自定义颜色 + + + + + + 隐藏返回按钮 + + + + + + 标题左对齐 + + + + + + 标题居中对齐(默认) + + + + + + 使用插槽 + + + + + 自定义标题 + + + + + + + + + + + + + + + 渐变背景 + + + + + + + 内容项 {{ i }} + + + + + + + + diff --git a/src/components/TopNavBar/index.vue b/src/components/TopNavBar/index.vue new file mode 100644 index 0000000..75cb5b4 --- /dev/null +++ b/src/components/TopNavBar/index.vue @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + {{ title }} + + + + + + + + + + + + + + + diff --git a/src/components/TopNavBar/styles/index.scss b/src/components/TopNavBar/styles/index.scss new file mode 100644 index 0000000..bbd7273 --- /dev/null +++ b/src/components/TopNavBar/styles/index.scss @@ -0,0 +1,49 @@ +// TopNavBar 组件样式 +.top-nav-bar { + width: 100%; + background-color: $theme-color-100; + + &--fixed { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 999; + } +} + +.nav-bar-left, +.nav-bar-right { + width: 30px; + height: 30px; +} + +.nav-bar-center { + flex: 1; + height: 30px; + padding: 0 20px; // 为左右按钮留出空间 + + // 居中对齐(默认) + &--center { + justify-content: center; + + .nav-bar-title { + text-align: center; + } + } + + // 左对齐 + &--left { + justify-content: flex-start; + padding-left: 20px; // 为返回按钮留出更多空间 + + .nav-bar-title { + text-align: left; + } + } +} + +// 固定导航栏时的页面内容适配 +.page-with-fixed-navbar { + padding-top: calc(var(--status-bar-height, 44px) + 44px); +} diff --git a/src/components/TopNavBar/使用指南.md b/src/components/TopNavBar/使用指南.md new file mode 100644 index 0000000..ab692aa --- /dev/null +++ b/src/components/TopNavBar/使用指南.md @@ -0,0 +1,298 @@ +# TopNavBar 组件使用指南 + +## 组件概述 + +TopNavBar 是一个功能完整的顶部导航栏组件,专为 uni-app 项目设计。该组件支持固定定位、自定义样式、插槽内容等功能,可以满足大部分页面的导航需求。 + +## 核心特性 + +### 1. 可配置固定定位 + +- **默认行为**: 组件默认不固定,跟随页面滚动 +- **固定模式**: 设置 `fixed="true"` 可将导航栏固定在页面顶部 +- **自动适配**: 固定模式下自动处理状态栏高度和安全区域 + +### 2. 智能状态栏适配 + +- 自动获取系统状态栏高度 +- 支持不同平台的导航栏高度适配(iOS: 44px, Android: 48px) +- 可选择隐藏状态栏占位区域 + +### 3. 灵活的自定义选项 + +- 支持自定义背景色、标题色、图标色 +- 可控制返回按钮显示/隐藏 +- 支持自定义 z-index 层级 +- 支持标题对齐方式配置(居中/左对齐) + +## 快速开始 + +### 基础使用 + +```vue + + +``` + +### 固定在顶部 + +```vue + + + + + + + + + + + +``` + +### 自定义样式 + +```vue + + + + + + + + + + + +``` + +## 高级用法 + +### 使用插槽自定义内容 + +```vue + + + + + + + 品牌名称 + + + + + + + + + + + + + + +``` + +### 监听返回事件 + +```vue + + + + + +``` + +## 实际应用场景 + +### 1. 商品详情页 + +```vue + + + + + + + + + + + + + +``` + +### 2. 订单列表页 + +```vue + + + + + + + + + + + + + +``` + +### 3. 聊天页面 + +```vue + + + + + + + + + + + + + +``` + +## 最佳实践 + +### 1. 固定导航栏的页面布局 + +```scss +// 推荐的页面结构 +.page-container { + .page-content { + // 方法1: 使用 padding-top + padding-top: calc(var(--status-bar-height, 44px) + 44px); + + // 方法2: 使用 margin-top + // margin-top: calc(var(--status-bar-height, 44px) + 44px); + } +} +``` + +### 2. 响应式设计 + +```scss +// 适配不同屏幕尺寸 +@media screen and (max-width: 375px) { + .page-content { + padding-top: calc(var(--status-bar-height, 44px) + 40px); + } +} +``` + +### 3. 主题适配 + +```vue + + + + + +``` + +## 注意事项 + +1. **固定定位的性能考虑**: 固定导航栏会创建新的层叠上下文,在复杂页面中可能影响性能 + +2. **状态栏适配**: 在不同设备上状态栏高度可能不同,组件会自动处理,但建议在测试时验证各种设备 + +3. **插槽内容**: 使用插槽时注意内容的响应式设计,确保在不同屏幕尺寸下都能正常显示 + +4. **z-index 管理**: 如果页面中有其他固定定位元素,注意调整 z-index 避免层级冲突 + +5. **返回按钮**: 默认返回行为是 `uni.navigateBack()`,如需自定义请监听 `@back` 事件 + +## 故障排除 + +### 常见问题 + +**Q: 固定导航栏下的内容被遮挡了?** +A: 需要为页面内容添加顶部间距,参考上面的最佳实践。 + +**Q: 在某些设备上状态栏高度不正确?** +A: 组件会自动获取状态栏高度,如果仍有问题,可以手动设置 `hideStatusBar="true"` 并自行处理。 + +**Q: 自定义颜色不生效?** +A: 确保传入的颜色值格式正确,支持 hex、rgb、rgba 等标准 CSS 颜色格式。 + +**Q: 插槽内容显示异常?** +A: 检查插槽内容的样式,确保没有影响导航栏布局的 CSS 属性。 diff --git a/src/components/UseDateRange/index.vue b/src/components/UseDateRange/index.vue new file mode 100644 index 0000000..9b0ba7c --- /dev/null +++ b/src/components/UseDateRange/index.vue @@ -0,0 +1,76 @@ + + + + 使用日期 + + + + + + + {{ item.weekDesc }} + + {{ formatMD(item.date) }} + + {{ item.canOrder }} + + ✔ + + + + + + + + + + + + diff --git a/src/components/UseDateRange/styles/index.scss b/src/components/UseDateRange/styles/index.scss new file mode 100644 index 0000000..e03ad34 --- /dev/null +++ b/src/components/UseDateRange/styles/index.scss @@ -0,0 +1,62 @@ +.date-scroll { + width: 100%; +} +.date-list { + display: flex; + padding: 8px 12px 12px; +} +.date-item { + width: 76px; + min-width: 76px; + height: 86px; + background: #fff; + border-radius: 10px; + box-shadow: 0 0 0 1px rgba(0,0,0,0.04); + margin-right: 12px; + padding: 8px 10px; + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; +} +.date-item .label { + margin-bottom: 6px; + color: #999; +} +.date-item .md { + margin-bottom: 6px; + color: #000; +} +.date-item .status { + font-size: 12px; + color: #999; } +.date-item.selected { + border: 1px solid $theme-color-500; + background: $theme-color-50; +} +.date-item.selected .label, +.date-item.selected .md, +.date-item.selected .status { + color: $theme-color-500; +} +.date-item.disabled { + opacity: 0.45; +} +.date-item .check { + position: absolute; + right: 0; + bottom: 0; + background: $theme-color-500; + color: #fff; + width: 18px; + height: 18px; + border-top-left-radius: 10px; + border-bottom-right-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; +} \ No newline at end of file diff --git a/src/pages/booking/components/ConactSection/index.vue b/src/pages/booking/components/ConactSection/index.vue new file mode 100644 index 0000000..87c8501 --- /dev/null +++ b/src/pages/booking/components/ConactSection/index.vue @@ -0,0 +1,92 @@ + + + + + 选择数量 + 请选择套餐数量 + + + + + + + + + + + + + 联系方式 + + + 联系人姓名 + + + + + + + 手机号码 + + + + + + + + + + diff --git a/src/pages/booking/components/FooterSection/index.vue b/src/pages/booking/components/FooterSection/index.vue new file mode 100644 index 0000000..88e76f8 --- /dev/null +++ b/src/pages/booking/components/FooterSection/index.vue @@ -0,0 +1,115 @@ + + + + + + + diff --git a/src/pages/booking/components/UserSection/index.vue b/src/pages/booking/components/UserSection/index.vue new file mode 100644 index 0000000..19f0546 --- /dev/null +++ b/src/pages/booking/components/UserSection/index.vue @@ -0,0 +1,71 @@ + + + + + 入住信息 + + + + + + + + + 住客姓名 + + + + + + + 联系手机 + + + + + + + + + + + diff --git a/src/pages/booking/index.vue b/src/pages/booking/index.vue new file mode 100644 index 0000000..df09382 --- /dev/null +++ b/src/pages/booking/index.vue @@ -0,0 +1,338 @@ + + + + + {{ GOODS_TYPE[orderData.orderType] }} + + + + + + + + + + + {{ orderData.commodityName }} + + + + + {{ orderData.commodityDescription }} + + + + + + {{ item }} + + + + + + 取消政策及说明 + + 取消政策 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/booking/styles/index.scss b/src/pages/booking/styles/index.scss new file mode 100644 index 0000000..d51d90b --- /dev/null +++ b/src/pages/booking/styles/index.scss @@ -0,0 +1,4 @@ +.booking { + background: linear-gradient(180deg, $theme-color-100 0%, #f5f7fa 100%) 0 86px / 100% + 100px no-repeat; +} diff --git a/src/pages/bridge/SaveImage.vue b/src/pages/bridge/SaveImage.vue new file mode 100644 index 0000000..8a6d137 --- /dev/null +++ b/src/pages/bridge/SaveImage.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/pages/bridge/UploadImage.vue b/src/pages/bridge/UploadImage.vue new file mode 100644 index 0000000..08ae2c4 --- /dev/null +++ b/src/pages/bridge/UploadImage.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/pages/quick/index.vue b/src/pages/goods/README.md similarity index 100% rename from src/pages/quick/index.vue rename to src/pages/goods/README.md diff --git a/src/pages/goods/album/index.vue b/src/pages/goods/album/index.vue new file mode 100644 index 0000000..5a05fb4 --- /dev/null +++ b/src/pages/goods/album/index.vue @@ -0,0 +1,103 @@ + + + + + 全部({{ albumList.length }}) + + + + + + + + + {{ item.name }} + + + + + + + + + + + diff --git a/src/pages/goods/components/DateSelector/index.vue b/src/pages/goods/components/DateSelector/index.vue new file mode 100644 index 0000000..766d928 --- /dev/null +++ b/src/pages/goods/components/DateSelector/index.vue @@ -0,0 +1,67 @@ + + + + 入住日期 + + + {{ checkInDate }} + {{ checkInDay }} + + + + + + {{ nights }}晚 + + + + 退房日期 + + + {{ checkOutDate }} + {{ checkOutDay }} + + + + + + + + + diff --git a/src/pages/goods/components/DateSelector/styles/index.scss b/src/pages/goods/components/DateSelector/styles/index.scss new file mode 100644 index 0000000..ab0aaa5 --- /dev/null +++ b/src/pages/goods/components/DateSelector/styles/index.scss @@ -0,0 +1,64 @@ +.date-selector { + display: flex; + align-items: flex-end; + justify-content: space-between; + margin: 12px 0; + padding: 0 12px; +} + +.date-item { + flex: 1; + position: relative; +} + +.date-label { + position: absolute; + top: -12rpx; + left: 24rpx; + font-size: 20rpx; + color: $uni-text-color-grey; + background-color: $uni-bg-color; + padding: 0 8rpx; + z-index: 1; +} + +.date-box { + padding: 20rpx 24rpx; + background-color: $uni-bg-color; + border-radius: 16rpx; + border: 2rpx solid #f0f0f0; + display: flex; + align-items: center; + justify-content: start; +} + +.date-content { + display: flex; + align-items: baseline; + gap: 8rpx; +} + +.date-text { + font-size: 32rpx; + font-weight: 500; + color: $uni-text-color; +} + +.day-text { + font-size: 20rpx; + color: #666666; +} + +.nights-info { + display: flex; + align-items: center; + justify-content: center; + min-width: 80rpx; + margin: 0 16rpx; + margin-bottom: 20rpx; +} + +.nights-text { + font-size: 24rpx; + color: #666666; +} diff --git a/src/pages/goods/components/GoodConfirm/README.md b/src/pages/goods/components/GoodConfirm/README.md new file mode 100644 index 0000000..2bed92e --- /dev/null +++ b/src/pages/goods/components/GoodConfirm/README.md @@ -0,0 +1,300 @@ +# GoodConfirm 商品确认组件 + +基于 uni-popup 弹出层的商品确认组件,提供优雅的商品购买确认界面。 + +## 功能特性 + +- 🎨 **现代化设计** - 采用底部弹出设计,符合移动端交互习惯 +- 📱 **响应式布局** - 完美适配各种屏幕尺寸 +- 🛒 **商品信息展示** - 支持商品图片、标题、价格、标签展示 +- 🔢 **数量选择** - 提供加减按钮和手动输入两种方式 +- 👥 **动态表单管理** - 根据数量自动添加/删除游客信息表单 +- 📝 **表单数据绑定** - 支持姓名和手机号的双向绑定 +- 📱 **横向滚动支持** - 游客信息区域支持横向滚动浏览多个表单 +- 🗑️ **删除表单支持** - 支持删除游客信息表单,自动同步数量和列表长度 +- 💰 **实时计算** - 自动计算并显示总价 +- ⚡ **性能优化** - 基于 Vue 3 Composition API,性能卓越 +- 🎭 **动画效果** - 流畅的弹出和交互动画 +- 🔧 **高度可配置** - 支持自定义商品数据和事件处理 + +## 基础用法 + +### 默认使用 + +```vue + + + 显示确认弹窗 + + + + + + +``` + +### 自定义商品数据 + +```vue + +``` + +## API 文档 + +### Props + +| 属性名 | 类型 | 默认值 | 说明 | +| --------- | ------ | ------ | ------------ | +| goodsData | Object | {} | 商品数据对象 | + +#### goodsData 对象结构 + +```typescript +interface GoodsData { + commodityName?: string; // 商品名称 + price?: number; // 商品价格 + timeTag?: string; // 时间标签(如:随时可退) + commodityPhotoList?: Array<{ + // 商品图片列表 + photoUrl: string; // 图片URL + }>; +} +``` + +### Events + +| 事件名 | 参数 | 说明 | +| ------- | --------- | -------------- | +| confirm | orderData | 确认购买时触发 | +| close | - | 关闭弹窗时触发 | + +#### confirm 事件参数 + +```typescript +interface OrderData { + goodsData: GoodsData; // 商品数据 + quantity: number; // 购买数量 + totalPrice: string; // 总价(字符串格式) + userFormList: Array<{ + // 游客信息列表 + name: string; // 游客姓名 + phone: string; // 游客手机号 + }>; +} +``` + +### Methods + +| 方法名 | 参数 | 说明 | +| ---------- | ---- | -------- | +| showPopup | - | 显示弹窗 | +| closePopup | - | 关闭弹窗 | + +## 样式定制 + +组件使用 SCSS 编写样式,支持以下自定义变量: + +```scss +// 主色调 +$primary-color: #ff6b35; +$primary-gradient: linear-gradient(135deg, #ff6b35 0%, #ff8f65 100%); + +// 文字颜色 +$text-primary: #333; +$text-secondary: #666; + +// 背景颜色 +$bg-white: #fff; +$bg-gray: #f8f9fa; +$border-color: #f5f5f5; + +// 圆角 +$border-radius: 8px; +$border-radius-large: 20px; +``` + +## 高级用法 + +### 响应式数据绑定 + +```vue + +``` + +### 订单处理集成 + +```vue + +``` + +## 注意事项 + +1. **依赖要求**:组件依赖 `uni-popup` 和 `uni-icons`,请确保项目中已安装相关依赖 +2. **图片资源**:请确保商品图片路径正确,建议使用绝对路径或网络图片 +3. **数量限制**:组件默认最小购买数量为 1,可根据业务需求调整 +4. **价格格式**:价格支持数字类型,组件内部会自动处理格式化 +5. **事件处理**:建议在 `confirm` 事件中添加适当的错误处理和用户反馈 +6. **表单管理**:游客信息表单会根据购买数量自动调整,无需手动管理 + +## 更新日志 + +### v1.4.2 (2024-12-19) + +**修复Stepper组件响应性问题** + +- 🐛 修复Stepper组件不响应外部modelValue变化的问题 +- 🔄 添加watch监听器,确保Stepper组件能实时同步外部值变化 +- ✨ 完善删除表单卡片时Stepper显示值的自动更新 +- 🎯 提升组件间数据同步的准确性和实时性 + +### v1.4.1 (2024-12-19) + +**修复quantity更新问题** + +- 🐛 修复删除表单时quantity值未正确更新的问题 +- 🔧 优化watch监听器逻辑,添加删除标志位防止冲突 +- 🎯 改进deleteUserForm方法,确保数据同步准确 +- 🔍 增强demo页面调试信息,便于测试验证 + +### v1.4.0 + +- 🗑️ **新增删除功能** - 支持点击删除图标删除游客信息表单 +- 🔄 **智能数量同步** - 删除表单时自动同步quantity数量和userFormList长度 +- 🛡️ **最小限制保护** - 确保至少保留一位游客信息,防止全部删除 +- 🎯 **动态图标控制** - 只有多于一个表单时才显示删除图标 +- 📱 **用户体验优化** - 删除操作提供友好的提示信息 + +### v1.3.0 + +- 🚫 **防换行优化** - 确保FormCard组件在任何情况下都不会换行显示 +- 📐 **布局增强** - 添加flex-shrink: 0确保表单卡片保持固定宽度 +- 🎯 **滚动优化** - 改进横向滚动体验,支持触摸滚动 +- 🧪 **测试改进** - demo页面新增多人数测试按钮,便于测试横向滚动效果 + +### v1.2.0 + +- 📱 **新增横向滚动** - 游客信息区域支持横向滚动浏览多个表单 +- 🎨 **优化布局设计** - 表单卡片采用固定宽度,提升视觉体验 +- 🔧 **改进滚动体验** - 隐藏滚动条,提供更清爽的界面 +- 📐 **响应式优化** - 确保在不同屏幕尺寸下的良好表现 + +### v1.1.0 + +- 🎉 **新增动态表单管理** - 根据购买数量自动添加/删除游客信息表单 +- 📝 **新增表单数据绑定** - 支持游客姓名和手机号的双向绑定 +- 🔄 **优化数据结构** - 确认订单时返回完整的用户信息列表 +- 🎯 **改进用户体验** - 表单项数量与购买数量实时同步 +- 🐛 **修复已知问题** - 优化组件初始化和数据更新逻辑 + +### v1.0.0 (2024-01-XX) + +- ✨ 初始版本发布 +- 🎨 基于 uni-popup 的底部弹出设计 +- 🛒 完整的商品信息展示功能 +- 🔢 数量选择和总价计算 +- 📱 响应式移动端适配 +- 🎭 流畅的动画效果 +- 📚 完整的文档和示例 + +## 技术栈 + +- **框架**: Vue 3 + Composition API +- **UI组件**: uni-app + uni-ui +- **样式**: SCSS +- **构建工具**: Vite + +## 浏览器支持 + +- iOS Safari 10+ +- Android Chrome 50+ +- 微信小程序 +- 支付宝小程序 +- H5 现代浏览器 + +## 许可证 + +MIT License diff --git a/src/pages/goods/components/GoodConfirm/demo.vue b/src/pages/goods/components/GoodConfirm/demo.vue new file mode 100644 index 0000000..6ea4f56 --- /dev/null +++ b/src/pages/goods/components/GoodConfirm/demo.vue @@ -0,0 +1,233 @@ + + + + GoodConfirm 组件演示 + + + + 基础用法 + 显示商品确认弹窗 + + 设置5人测试横向滚动 + + + + + 当前状态 + + Stepper数量: {{ quantity }} + 表单项数量: {{ userFormCount }} + 总价: ¥{{ totalPrice }} + 实时quantity值: {{ quantity }} + ✨ 支持横向滚动浏览多个游客信息 + 🗑️ 支持删除游客信息(至少保留一位) + + + + + 最后提交的数据 + + 商品: {{ lastOrderData.goodsData.commodityName }} + 数量: {{ lastOrderData.quantity }} + 总价: ¥{{ lastOrderData.totalPrice }} + 用户信息: + + + 游客{{ index + 1 }}: {{ user.name || "未填写" }} - + {{ user.phone || "未填写" }} + + + + + + + + + + + + diff --git a/src/pages/goods/components/GoodConfirm/images/商品2级 门票.png b/src/pages/goods/components/GoodConfirm/images/商品2级 门票.png new file mode 100644 index 0000000..25dc8bb Binary files /dev/null and b/src/pages/goods/components/GoodConfirm/images/商品2级 门票.png differ diff --git a/src/pages/goods/components/GoodConfirm/index.vue b/src/pages/goods/components/GoodConfirm/index.vue new file mode 100644 index 0000000..50bd72a --- /dev/null +++ b/src/pages/goods/components/GoodConfirm/index.vue @@ -0,0 +1,369 @@ + + + + + + 填写信息 + + + + + + + + + + + + + + + + {{ goodsData.commodityName || "商品名称" }} + + + ¥ + + + {{ goodsData.calculatedTotalPrice || 0 }} + + + ({{ goodsData.startDate }}至{{ goodsData.endDate }}) 共{{ + goodsData.totalDays + }}晚 + + + + {{ goodsData.specificationPrice || 0 }} + + + + 包含服务 + + {{ item.serviceTitle }} + {{ item.serviceAmount }} + + + + + + + + + + + + + + + + updateUserForm(index, 'visitorName', value) + " + @update:contactPhone=" + (value) => updateUserForm(index, 'contactPhone', value) + " + @delete="() => deleteUserForm(index)" + /> + + + + + + + + + + + + + + + + diff --git a/src/pages/goods/components/GoodConfirm/styles/index.scss b/src/pages/goods/components/GoodConfirm/styles/index.scss new file mode 100644 index 0000000..a54937b --- /dev/null +++ b/src/pages/goods/components/GoodConfirm/styles/index.scss @@ -0,0 +1,208 @@ +.good-container { + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + box-sizing: border-box; + padding: 12px; + border-bottom: 1px solid #ddd; + position: relative; + + .header-title { + font-size: 18px; + font-weight: 600; + color: $uni-text-color; + flex: 1; + text-align: center; + } + + .close-btn { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + } +} + +.good-content { + background: #fff; + box-sizing: border-box; + max-height: 60vh; + overflow: hidden; + + .wrapper { + box-sizing: border-box; + padding: 12px; + } + + .good-info-wrapper { + border-radius: 12px; + overflow: hidden; + } + + .goods-info { + display: flex; + box-sizing: border-box; + background-color: #f5f5f5; + padding: 12px; + + .goods-details { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + + .goods-title { + font-size: $uni-font-size-lg; + font-weight: 500; + color: $uni-text-color; + line-height: 22px; + margin-bottom: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .goods-price { + display: flex; + align-items: baseline; + margin-bottom: 12px; + + .currency { + font-size: $uni-font-size-base; + color: #ff6b35; + font-weight: 500; + } + + .price { + font-size: 20px; + color: #ff6b35; + font-weight: 600; + margin-left: 2px; + } + + .price-desc { + font-size: $uni-font-size-base; + color: $uni-text-color-grey; + font-weight: 400; + margin-left: 12px; + } + } + + .goods-service-list { + background-color: #f5f5f5; + border-radius: 6px; + padding: 12px; + box-sizing: border-box; + } + + .service-title { + font-size: $uni-font-size-base; + font-weight: 500; + color: $uni-text-color; + } + + .goods-service-item { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 8px; + + .service-label, + .service-value { + font-size: $uni-font-size-base; + font-weight: 500; + } + + .service-label { + color: $uni-text-color; + } + + .service-value { + color: $uni-text-color-grey; + } + } + } + } + + .quantity-section { + display: flex; + justify-content: space-between; + align-items: center; + box-sizing: border-box; + padding: 12px 0; + } + + .user-form-list { + margin-bottom: 12px; + display: flex; + flex-direction: row; + white-space: nowrap; + } +} + +.footer { + margin-top: 12px; + display: flex; + align-items: center; + box-sizing: border-box; + padding-left: 12px; + padding-right: 12px; + padding-bottom: 24px; + + .left { + display: flex; + align-items: baseline; + } + + .total-count { + font-size: $uni-font-size-sm; + color: $uni-text-color; + } + + .total-price { + display: flex; + align-items: baseline; + font-size: 24px; + color: #f55726; + + &::before { + content: "¥"; + font-size: $uni-font-size-sm; + } + } + + .confirm-btn { + width: 160px; + height: 48px; + background: linear-gradient(179deg, $theme-color-500 0%, $theme-color-700 100%); + color: #fff; + border: none; + border-radius: 24px; + font-size: $uni-font-size-lg; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + margin-left: auto; + + &:active { + transform: translateY(1px); + } + + &::after { + border: none; + } + } +} diff --git a/src/pages/goods/components/GoodFacility/index.vue b/src/pages/goods/components/GoodFacility/index.vue new file mode 100644 index 0000000..f5d335e --- /dev/null +++ b/src/pages/goods/components/GoodFacility/index.vue @@ -0,0 +1,54 @@ + + + + + + + {{ zniconsMap[moduleItem.icon] }} + + + {{ moduleItem.title }} + + + + + {{ span }} + + + + + + + + + + diff --git a/src/pages/goods/components/GoodInfo/README.md b/src/pages/goods/components/GoodInfo/README.md new file mode 100644 index 0000000..02af5c1 --- /dev/null +++ b/src/pages/goods/components/GoodInfo/README.md @@ -0,0 +1,255 @@ +# GoodInfo 商品信息组件 + +## 概述 + +`GoodInfo` 是一个高性能的商品信息展示组件,专为电商、旅游、服务类应用设计。组件采用现代化的UI设计,支持响应式布局和暗色模式,提供优秀的用户体验。 + +## 功能特性 + +### 🎯 核心功能 + +- **价格展示**: 突出显示商品价格,支持货币符号和价格标签 +- **商品标题**: 清晰展示商品名称和相关标签 +- **地址信息**: 显示商品/服务地址,支持图标和交互 +- **设施展示**: 网格布局展示商品特色设施或服务项目 + +### ⚡ 性能优化 + +- **计算属性缓存**: 使用 `computed` 优化设施列表渲染 +- **按需渲染**: 条件渲染减少不必要的DOM节点 +- **轻量级设计**: 最小化组件体积和依赖 +- **懒加载支持**: 支持图标和内容的懒加载 + +### 🎨 UI特性 + +- **现代化设计**: 圆角卡片、阴影效果、渐变背景 +- **响应式布局**: 适配不同屏幕尺寸 +- **暗色模式**: 自动适配系统主题 +- **交互反馈**: 悬停效果和过渡动画 + +## 基础用法 + +### 简单使用 + +```vue + + + + + +``` + +### 完整配置 + +```vue + + + + + +``` + +## API 文档 + +### Props + +| 参数 | 类型 | 默认值 | 说明 | +| --------- | ------ | ------ | ------------ | +| goodsInfo | Object | `{}` | 商品信息对象 | + +### goodsInfo 对象结构 + +| 字段 | 类型 | 必填 | 默认值 | 说明 | +| ---------- | ------------- | ---- | --------------------------------------------- | ------------------------------ | +| price | Number/String | 否 | `399` | 商品价格 | +| title | String | 否 | `'【成人票】云从朵花温泉门票'` | 商品标题 | +| tag | String | 否 | - | 价格标签(如:热销、限时优惠) | +| timeTag | String | 否 | `'随时可退'` | 时间相关标签 | +| address | String | 否 | `'距您43.1公里 黔南州布依族苗族自治州龙里县'` | 地址信息 | +| facilities | Array | 否 | 默认设施列表 | 设施/特色列表 | + +### facilities 数组结构 + +```javascript +[ + { + icon: "home", // uni-icons 图标名称 + name: "48个泡池", // 设施名称 + }, +]; +``` + +## 样式定制 + +### CSS 变量 + +组件支持通过 CSS 变量进行主题定制: + +```scss +.good-info { + --primary-color: #ff6b35; // 主色调 + --background-color: #fff; // 背景色 + --text-color: #333; // 文字颜色 + --border-radius: 16rpx; // 圆角大小 + --shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08); // 阴影 +} +``` + +### 响应式断点 + +- **小屏设备**: `max-width: 750rpx` +- **暗色模式**: `prefers-color-scheme: dark` + +## 性能优化建议 + +### 1. 数据结构优化 + +```javascript +// ✅ 推荐:使用 reactive 包装数据 +const goodsData = reactive({ + price: 399, + title: "商品标题", +}); + +// ❌ 避免:频繁的深层对象更新 +const goodsData = ref({ + nested: { + deep: { + value: "data", + }, + }, +}); +``` + +### 2. 设施列表优化 + +```javascript +// ✅ 推荐:预定义设施列表 +const FACILITY_PRESETS = { + spa: [ + { icon: "home", name: "48个泡池" }, + { icon: "water", name: "恒温泳池" }, + ], + hotel: [ + { icon: "bed", name: "豪华客房" }, + { icon: "car", name: "免费停车" }, + ], +}; + +// 使用预设 +const goodsData = { + facilities: FACILITY_PRESETS.spa, +}; +``` + +### 3. 图标优化 + +```javascript +// ✅ 推荐:使用常见图标 +const commonIcons = ["home", "person", "heart", "star"]; + +// ❌ 避免:使用过多不同图标增加包体积 +``` + +## 最佳实践 + +### 1. 数据验证 + +```javascript +// 添加数据验证 +const validateGoodsInfo = (data) => { + return { + price: Number(data.price) || 0, + title: String(data.title || ""), + facilities: Array.isArray(data.facilities) ? data.facilities : [], + }; +}; +``` + +### 2. 错误处理 + +```vue + + + + + +``` + +### 3. 无障碍访问 + +```vue + + + + + +``` + +## 注意事项 + +1. **图标依赖**: 组件依赖 `uni-icons`,请确保项目中已安装 +2. **单位适配**: 样式使用 `rpx` 单位,适配小程序和H5 +3. **性能考虑**: 设施列表较多时建议分页或虚拟滚动 +4. **主题适配**: 支持暗色模式,但需要系统支持 + +## 更新日志 + +### v1.0.0 (2024-01-XX) + +- ✨ 初始版本发布 +- 🎨 现代化UI设计 +- ⚡ 性能优化 +- 📱 响应式布局 +- 🌙 暗色模式支持 + +## 技术栈 + +- **框架**: Vue 3 Composition API +- **样式**: SCSS +- **图标**: uni-icons +- **构建**: Vite/Webpack + +## 浏览器支持 + +- ✅ Chrome 80+ +- ✅ Firefox 75+ +- ✅ Safari 13+ +- ✅ Edge 80+ +- ✅ 微信小程序 +- ✅ 支付宝小程序 +- ✅ H5 diff --git a/src/pages/goods/components/GoodInfo/images/商品详情.png b/src/pages/goods/components/GoodInfo/images/商品详情.png new file mode 100644 index 0000000..aa76422 Binary files /dev/null and b/src/pages/goods/components/GoodInfo/images/商品详情.png differ diff --git a/src/pages/goods/components/GoodInfo/index.vue b/src/pages/goods/components/GoodInfo/index.vue new file mode 100644 index 0000000..969f0e9 --- /dev/null +++ b/src/pages/goods/components/GoodInfo/index.vue @@ -0,0 +1,54 @@ + + + + + + {{ goodsData.commodityName || "【成人票】云从朵花温泉门票" }} + + + {{ goodsData.timeTag || "随时可退" }} + + + + + + + + {{ facility }} + + + + + + + + + diff --git a/src/pages/goods/components/GoodInfo/styles/index.scss b/src/pages/goods/components/GoodInfo/styles/index.scss new file mode 100644 index 0000000..e29522f --- /dev/null +++ b/src/pages/goods/components/GoodInfo/styles/index.scss @@ -0,0 +1,67 @@ +.good-info { + background: #fff; + + // 标题区域 + .title-section { + display: flex; + align-items: center; + margin-bottom: 12px; + + .title { + font-size: $uni-font-size-lg; + color: $uni-text-color; + font-weight: 600; + line-height: 1.4; + flex: 0 280px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .tag-wrapper { + display: flex; + align-items: center; + + .time-tag { + color: #f55726; + padding: 3px 6px; + border-radius: 6px; + font-size: 9px; + border: 1px solid #f55726; + } + } + + .calender-icon { + margin-left: auto; + } + } + + // 设施信息区域 + .facilities-section { + margin-top: 12px; + + .facilities-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + + .facility-item { + display: flex; + align-items: center; + gap: 4px; + padding: 8px; + background: #fafafa; + border-radius: 6px; + + .facility-text { + font-size: $uni-font-size-sm; + color: $uni-text-color; + line-height: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + } +} diff --git a/src/pages/goods/components/GoodPackage/index.vue b/src/pages/goods/components/GoodPackage/index.vue new file mode 100644 index 0000000..e2486f3 --- /dev/null +++ b/src/pages/goods/components/GoodPackage/index.vue @@ -0,0 +1,39 @@ + + + + + + {{ item.name }} + + {{ item.count }}{{ item.unit }} + + + + + + + + diff --git a/src/pages/goods/components/GoodPackage/styles/index.scss b/src/pages/goods/components/GoodPackage/styles/index.scss new file mode 100644 index 0000000..dbc2721 --- /dev/null +++ b/src/pages/goods/components/GoodPackage/styles/index.scss @@ -0,0 +1,20 @@ +.title-row { + display: flex; + align-items: center; + justify-content: flex-start; +} + +.title-row .left, +.title-row .right { + white-space: nowrap; + flex: 0 0 auto; +} + +.title-row .sep { + flex: 1 1 auto; + height: 1px; + margin: 0 8px; + background-image: repeating-linear-gradient(to right, #CACFD8 0 10px, transparent 10px 16px); + background-repeat: repeat-x; + background-position: center; +} \ No newline at end of file diff --git a/src/pages/goods/images/商品详情.png b/src/pages/goods/images/商品详情.png new file mode 100644 index 0000000..761cb68 Binary files /dev/null and b/src/pages/goods/images/商品详情.png differ diff --git a/src/pages/goods/index.vue b/src/pages/goods/index.vue new file mode 100644 index 0000000..4541ca2 --- /dev/null +++ b/src/pages/goods/index.vue @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/goods/styles/index.scss b/src/pages/goods/styles/index.scss new file mode 100644 index 0000000..4e04098 --- /dev/null +++ b/src/pages/goods/styles/index.scss @@ -0,0 +1,107 @@ +$button-color: #00a6ff; + +.goods-container { + display: flex; + flex-direction: column; + height: 100vh; + background-color: #fff; + + // 顶部导航栏固定样式 + :deep(.top-nav-bar) { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + flex-shrink: 0; + transition: background-color 0.3s ease; + } + + // 使用须知样式 + .use-notice { + margin: 16px 0; + } + + .use-notice-content { + .module-item { + display: flex; + align-items: flex-start; + flex-direction: column; + padding: 12px 0; + + .module-icon { + display: flex; + flex-direction: row; + align-items: center; + margin-right: 8px; + flex-shrink: 0; + + .module-title { + font-size: $uni-font-size-base; + color: $uni-text-color; + text-align: center; + word-wrap: break-word; + margin-left: 4px; + } + } + + .module-desc { + flex: 1; + font-size: $uni-font-size-sm; + color: #666; + line-height: 1.5; + margin-top: 4px; + } + } + .border-bottom { + border-bottom: 1px solid #f0f0f0; + } + } + + .content-wrapper { + flex: 1; + height: 0; // 关键:让flex子项能够正确计算高度 + overflow-y: auto; + -webkit-overflow-scrolling: touch; // iOS平滑滚动 + } + + .goods-content { + border-radius: 28px 28px 0 0; + background-color: #fff; + padding: 20px 0; + position: relative; + margin-top: -30px; + z-index: 1; + } +} + +.footer { + position: sticky; + bottom: 0; + background-color: #fff; + box-sizing: border-box; + padding: 12px 12px 24px; + z-index: 10; + flex-shrink: 0; // 防止被压缩 + display: flex; + align-items: center; + + .amt { + &::before { + content: "¥"; + font-size: 16px; + font-weight: 500; + } + } + + .btn { + width: 120px; + height: 48px; + background: linear-gradient(90deg, #ff3d60 57%, #ff990c 100%); + } + + .icon { + height: 48px; + width: 34px; + } +} diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index e69de29..e6b08c7 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/pages/login/components/AgreePopup/README.md b/src/pages/login/components/AgreePopup/README.md new file mode 100644 index 0000000..90df96f --- /dev/null +++ b/src/pages/login/components/AgreePopup/README.md @@ -0,0 +1,71 @@ +# AgreePopup 用户协议同意弹窗组件 + +## 组件概述 + +AgreePopup 是一个用于登录流程中的用户协议同意弹窗组件,用于向用户展示隐私政策和用户协议,并获取用户的同意确认。 + +## 功能需求 + +### 界面设计 +- **弹窗标题**:显示"温馨提示"标题,居中显示 +- **关闭按钮**:右上角显示"×"关闭按钮,点击可关闭弹窗 +- **内容区域**: + - 主要说明文字:"您在使用朵花温泉服务前,请仔细阅读用户隐私条款及用户注册须知,当您点击同意,即表示您已经理解并同意该条款,该条款将构成对您具有法律约束力的文件。" + - 注意事项:"请您注意:如果您不同意上述用户注册须知、隐私政策或其中任何约定,请您停止注册。如您阅读并点击同意即表示您已充分阅读理解并接受其全部内容,并表明您也同意朵花温泉可以依据以上隐私政策来处理您的个人信息。" + +### 交互功能 +- **复选框**: + - 显示蓝色勾选框 + - 文字说明:"本人已仔细阅读《用户协议》和《隐私协议》,知悉并诺遵守该内容。" + - 支持点击切换选中/未选中状态 +- **确认按钮**: + - 显示"我知道了"按钮 + - 蓝色背景,白色文字 + - 圆角设计 + - 点击后触发同意事件并关闭弹窗 + +### 技术要求 +- 使用 Vue 3 Composition API +- 支持弹窗显示/隐藏控制 +- 提供事件回调:同意、关闭 +- 响应式设计,适配移动端 +- 使用 uni-app 框架 + +### 样式规范 +- 弹窗背景:白色 +- 圆角设计 +- 文字颜色:深灰色 +- 按钮:蓝色主题色 +- 复选框:蓝色选中状态 +- 适当的内边距和间距 + +### 使用场景 +- 用户首次登录时显示 +- 隐私政策更新后重新确认 +- 注册流程中的协议确认 + +## 组件接口 + +### Props +- `visible`: Boolean - 控制弹窗显示/隐藏 +- `title`: String - 弹窗标题,默认"温馨提示" + +### Events +- `@agree`: 用户点击同意时触发 +- `@close`: 用户关闭弹窗时触发 +- `@cancel`: 用户取消操作时触发 + +### Methods +- `show()`: 显示弹窗 +- `hide()`: 隐藏弹窗 + +## 文件结构 +``` +AgreePopup/ +├── README.md # 组件说明文档 +├── index.vue # 组件主文件 +├── styles/ +│ └── index.scss # 组件样式文件 +└── images/ + └── 登录授权1.png # 设计稿参考图 +``` \ No newline at end of file diff --git a/src/pages/login/components/AgreePopup/demo.vue b/src/pages/login/components/AgreePopup/demo.vue new file mode 100644 index 0000000..911b661 --- /dev/null +++ b/src/pages/login/components/AgreePopup/demo.vue @@ -0,0 +1,133 @@ + + + + AgreePopup 组件演示 + + + + 显示用户协议弹窗 + + + 组件状态: + 弹窗可见:{{ popupVisible }} + 用户操作:{{ userAction }} + + + + + + + + + + + diff --git a/src/pages/login/components/AgreePopup/images/登录授权1.png b/src/pages/login/components/AgreePopup/images/登录授权1.png new file mode 100644 index 0000000..dc5c8dd Binary files /dev/null and b/src/pages/login/components/AgreePopup/images/登录授权1.png differ diff --git a/src/pages/login/components/AgreePopup/index.vue b/src/pages/login/components/AgreePopup/index.vue new file mode 100644 index 0000000..655d2e9 --- /dev/null +++ b/src/pages/login/components/AgreePopup/index.vue @@ -0,0 +1,87 @@ + + + + + + {{ title }} + + + + + + + + + + + + + + + 我知道了 + + + + + + + + diff --git a/src/pages/login/components/AgreePopup/styles/index.scss b/src/pages/login/components/AgreePopup/styles/index.scss new file mode 100644 index 0000000..421c4b5 --- /dev/null +++ b/src/pages/login/components/AgreePopup/styles/index.scss @@ -0,0 +1,83 @@ +// AgreePopup 组件样式 +.agree-popup { + width: 327px; + background-color: $uni-bg-color; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + + // 弹窗头部 + .popup-header { + position: relative; + padding: 20px 20px 0 20px; + + .popup-title { + font-size: 18px; + font-weight: 600; + color: $uni-text-color; + text-align: center; + line-height: 24px; + } + + .close-btn { + position: absolute; + top: 16px; + right: 16px; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + &:hover { + background: #f5f5f5; + border-radius: $uni-border-radius-circle; + } + } + } + + // 弹窗内容 + .popup-content { + padding: 12px; + max-height: 400px; // 设置最大高度 + overflow-y: auto; // 启用垂直滚动 + + // 自定义滚动条样式 + &::-webkit-scrollbar { + display: none; + } + } + + // 按钮区域 + .button-area { + padding: 20px; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + + .confirm-btn { + width: 148px; + height: 44px; + background: linear-gradient(90deg, #22a7ff 0%, #2567ff 100%); + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + border-radius: $uni-border-radius-50px; + font-size: $uni-font-size-lg; + font-weight: 500; + transition: all 0.3s ease; + + &:hover { + background: #0056cc; + } + + &:active { + background: #004499; + transform: scale(0.98); + } + } + } +} diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue new file mode 100644 index 0000000..a5297d7 --- /dev/null +++ b/src/pages/login/index.vue @@ -0,0 +1,223 @@ + + + + + + {{ zniconsMap["zn-nav-arrow-left"] }} + + + + + + + + + + + + + 我已阅读并同意 + 《服务协议》 + 和 + 《隐私协议》 + ,\n + 授权与账号关联操作 + + + + + + + + {{ loginButtonspan }} + + + {{ loginButtonspan }} + + + + + + + + + + diff --git a/src/pages/login/styles/index.scss b/src/pages/login/styles/index.scss new file mode 100644 index 0000000..6e45ead --- /dev/null +++ b/src/pages/login/styles/index.scss @@ -0,0 +1,43 @@ +.login-wrapper { + display: flex; + flex-direction: column; + align-items: center; + box-sizing: border-box; + height: 100vh; + padding-top: 100px; + position: relative; + + .back-btn { + position: absolute; + top: 44px; + left: 4px; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + } + + .login-header { + margin-top: 40px; + width: 200px; + height: 200px; + } + + .login-btn-area { + margin-top: 20px; + width: 304px; + + .login-btn { + background: $theme-color-500; + width: 100%; + border-radius: 10px; + } + } + + .login-agreement { + margin-top: 80px; + width: 304px; + } +} diff --git a/src/pages/order/order/README.md b/src/pages/order/order/README.md new file mode 100644 index 0000000..d8c4a6c --- /dev/null +++ b/src/pages/order/order/README.md @@ -0,0 +1,288 @@ +# 工单管理系统 + +## 项目概述 + +这是一个基于 uniapp + Vue3 组合式 API 开发的微信小程序工单管理系统,提供完整的工单展示、管理和操作功能。 + +## 系统架构 + +### 页面结构 + +``` +pages/order/ +├── list.vue # 工单列表主页面 +├── detail.vue # 工单详情页面 +├── demo.vue # 功能演示页面 +├── components/ # 组件目录 +│ ├── TopNavBar/ # 顶部导航栏组件 +│ ├── Tabs/ # Tab切换组件 +│ ├── OrderCard/ # 工单卡片组件 +│ ├── OrderList/ # 工单列表组件 +│ └── ConsultationBar/ # 底部咨询栏组件 +├── styles/ # 样式文件 +└── images/ # 图片资源 +``` + +## 核心组件 + +### 1. TopNavBar - 顶部导航栏组件 + +**功能特性:** +- 左侧返回按钮 +- 自适应状态栏高度 +- 支持自定义标题内容 +- 响应式设计 + +**使用示例:** +```vue + + + + + +``` + +### 2. Tabs - Tab切换组件 + +**功能特性:** +- 多标签页切换 +- 动态下划线指示器 +- 平滑动画过渡 +- 自定义标签内容 +- 固定15px宽度,2px圆角下划线 + +**使用示例:** +```vue + +``` + +### 3. OrderCard - 工单卡片组件 + +**功能特性:** +- 工单信息展示 +- 多种状态支持(待处理、处理中、已完成、已取消) +- 状态图标和标签 +- 操作按钮(呼叫、完成) +- 自定义操作区域 +- 响应式设计 + +**使用示例:** +```vue + +``` + +### 4. OrderList - 工单列表组件 + +**功能特性:** +- 工单列表展示 +- 下拉刷新 +- 上拉加载更多 +- 空状态显示 +- 加载状态管理 +- 蓝色渐变背景 + +**使用示例:** +```vue + +``` + +### 5. ConsultationBar - 底部咨询栏组件 + +**功能特性:** +- 客服咨询入口 +- 固定底部显示 +- 安全区域适配 +- 自定义咨询文案 +- 跳转链接支持 + +**使用示例:** +```vue + +``` + +## 数据结构 + +### 工单数据结构 + +```javascript +{ + id: String, // 工单ID + title: String, // 工单标题 + createTime: String, // 创建时间 + contactName: String, // 联系人姓名 + contactPhone: String, // 联系电话 + status: String, // 工单状态:pending/processing/completed/cancelled + type: String // 工单类型:service/order +} +``` + +### Tab数据结构 + +```javascript +{ + label: String, // 显示文本 + value: String // 值 +} +``` + +## 功能特性 + +### ✅ 已实现功能 + +1. **工单管理** + - 工单列表展示 + - 工单状态管理 + - 工单详情查看 + - 工单操作(呼叫、完成) + +2. **交互功能** + - Tab页面切换 + - 下拉刷新 + - 上拉加载更多 + - 一键拨号 + - 客服咨询 + +3. **UI/UX** + - 响应式设计 + - 暗色模式支持 + - 流畅动画效果 + - 优雅的加载状态 + - 空状态处理 + +4. **技术特性** + - Vue3 组合式 API + - TypeScript 支持 + - 组件化架构 + - SCSS 样式管理 + - 错误处理机制 + +## 使用指南 + +### 快速开始 + +1. **进入工单列表** + ```javascript + uni.navigateTo({ + url: '/pages/order/list' + }) + ``` + +2. **查看功能演示** + ```javascript + uni.navigateTo({ + url: '/pages/order/demo' + }) + ``` + +### 自定义配置 + +1. **修改Tab配置** + ```javascript + const tabList = ref([ + { label: "全部订单", value: "all" }, + { label: "服务工单", value: "service" }, + { label: "自定义Tab", value: "custom" } + ]) + ``` + +2. **自定义工单状态** + ```javascript + const statusMap = { + pending: '待处理', + processing: '处理中', + completed: '已完成', + cancelled: '已取消', + custom: '自定义状态' + } + ``` + +3. **自定义样式主题** + ```scss + :root { + --primary-color: #007AFF; + --success-color: #52C41A; + --warning-color: #FF8C00; + --danger-color: #FF3B30; + } + ``` + +## API 接口 + +### 工单相关接口 + +```javascript +// 获取工单列表 +const getOrderList = async (params) => { + return await uni.request({ + url: '/api/orders', + method: 'GET', + data: params + }) +} + +// 更新工单状态 +const updateOrderStatus = async (orderId, status) => { + return await uni.request({ + url: `/api/orders/${orderId}/status`, + method: 'PUT', + data: { status } + }) +} +``` + +## 性能优化 + +1. **虚拟滚动**:大量数据时建议使用虚拟滚动 +2. **图片懒加载**:工单图片支持懒加载 +3. **防抖处理**:搜索和筛选功能防抖优化 +4. **缓存机制**:Tab切换时数据缓存 + +## 兼容性 + +- **微信小程序**:✅ 完全支持 +- **支付宝小程序**:✅ 支持 +- **H5**:✅ 支持 +- **App**:✅ 支持 + +## 更新日志 + +### v1.0.0 (2024-01-15) +- 🎉 初始版本发布 +- ✨ 完整的工单管理功能 +- ✨ 响应式设计和暗色模式 +- ✨ 组件化架构 +- ✨ 完善的文档和演示 + +## 开发团队 + +- **开发者**:AI Assistant +- **技术栈**:uniapp + Vue3 + SCSS +- **设计规范**:微信小程序设计指南 + +## 许可证 + +MIT License + +--- + +如有问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/src/pages/order/order/components/AmtSection/index.vue b/src/pages/order/order/components/AmtSection/index.vue new file mode 100644 index 0000000..e65b5a1 --- /dev/null +++ b/src/pages/order/order/components/AmtSection/index.vue @@ -0,0 +1,54 @@ + + + + + + 订单金额 + + + {{ orderData.payAmt }} + + + + + 超时后,订单将自动取消 + + + 取消政策及说明 + + 查看详情 + + + + + + + + + diff --git a/src/pages/order/order/components/FooterSection/index.vue b/src/pages/order/order/components/FooterSection/index.vue new file mode 100644 index 0000000..e64d408 --- /dev/null +++ b/src/pages/order/order/components/FooterSection/index.vue @@ -0,0 +1,172 @@ + + + + + + + diff --git a/src/pages/order/order/components/GoodsInfo/images/2025-07-13_105446.png b/src/pages/order/order/components/GoodsInfo/images/2025-07-13_105446.png new file mode 100644 index 0000000..0c17896 Binary files /dev/null and b/src/pages/order/order/components/GoodsInfo/images/2025-07-13_105446.png differ diff --git a/src/pages/order/order/components/GoodsInfo/images/food.png b/src/pages/order/order/components/GoodsInfo/images/food.png new file mode 100644 index 0000000..8252626 Binary files /dev/null and b/src/pages/order/order/components/GoodsInfo/images/food.png differ diff --git a/src/pages/order/order/components/GoodsInfo/images/icon_house.png b/src/pages/order/order/components/GoodsInfo/images/icon_house.png new file mode 100644 index 0000000..4ebc59f Binary files /dev/null and b/src/pages/order/order/components/GoodsInfo/images/icon_house.png differ diff --git a/src/pages/order/order/components/GoodsInfo/images/ticket.png b/src/pages/order/order/components/GoodsInfo/images/ticket.png new file mode 100644 index 0000000..0ab3334 Binary files /dev/null and b/src/pages/order/order/components/GoodsInfo/images/ticket.png differ diff --git a/src/pages/order/order/components/GoodsInfo/index.vue b/src/pages/order/order/components/GoodsInfo/index.vue new file mode 100644 index 0000000..a25f9ca --- /dev/null +++ b/src/pages/order/order/components/GoodsInfo/index.vue @@ -0,0 +1,94 @@ + + + + + + + {{ orderData.commodityName }} + + + + {{ orderData.commodityDescription }} + + + + + + {{ item }} + + + + + + + {{ orderData.commodityName }} + + + + 购买数量 + {{ commodityAmount }} + + + + 凭[电子凭证]直接验证使用 + + + + + + + diff --git a/src/pages/order/order/components/GoodsInfo/styles/index.scss b/src/pages/order/order/components/GoodsInfo/styles/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/order/order/components/NoticeInfo/images/2025-07-13_104948.png b/src/pages/order/order/components/NoticeInfo/images/2025-07-13_104948.png new file mode 100644 index 0000000..b89fb69 Binary files /dev/null and b/src/pages/order/order/components/NoticeInfo/images/2025-07-13_104948.png differ diff --git a/src/pages/order/order/components/NoticeInfo/images/icon_arrow.png b/src/pages/order/order/components/NoticeInfo/images/icon_arrow.png new file mode 100644 index 0000000..3348692 Binary files /dev/null and b/src/pages/order/order/components/NoticeInfo/images/icon_arrow.png differ diff --git a/src/pages/order/order/components/NoticeInfo/images/icon_card.png b/src/pages/order/order/components/NoticeInfo/images/icon_card.png new file mode 100644 index 0000000..1146c11 Binary files /dev/null and b/src/pages/order/order/components/NoticeInfo/images/icon_card.png differ diff --git a/src/pages/order/order/components/NoticeInfo/images/icon_clock.png b/src/pages/order/order/components/NoticeInfo/images/icon_clock.png new file mode 100644 index 0000000..83e3876 Binary files /dev/null and b/src/pages/order/order/components/NoticeInfo/images/icon_clock.png differ diff --git a/src/pages/order/order/components/NoticeInfo/index.vue b/src/pages/order/order/components/NoticeInfo/index.vue new file mode 100644 index 0000000..4c85d7f --- /dev/null +++ b/src/pages/order/order/components/NoticeInfo/index.vue @@ -0,0 +1,30 @@ + + + 购买须知 + + + + + + + diff --git a/src/pages/order/order/components/NoticeInfo/prompt.md b/src/pages/order/order/components/NoticeInfo/prompt.md new file mode 100644 index 0000000..e15da1a --- /dev/null +++ b/src/pages/order/order/components/NoticeInfo/prompt.md @@ -0,0 +1,14 @@ +## 游玩须知组件 + +组件名称:游玩须知组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片,高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.notice-info +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/pages/order/order/components/NoticeInfo/styles/index.scss b/src/pages/order/order/components/NoticeInfo/styles/index.scss new file mode 100644 index 0000000..8cd882a --- /dev/null +++ b/src/pages/order/order/components/NoticeInfo/styles/index.scss @@ -0,0 +1,13 @@ +.notice-info { + background-color: #fff; + border-radius: 10px; + padding: 16px 18px; +} + +.notice-title { + display: flex; + align-items: center; + font-size: $uni-font-size-lg; + font-weight: 500; + color: $uni-text-color; +} diff --git a/src/pages/order/order/components/OrderCard/InfoRow.vue b/src/pages/order/order/components/OrderCard/InfoRow.vue new file mode 100644 index 0000000..dc14b49 --- /dev/null +++ b/src/pages/order/order/components/OrderCard/InfoRow.vue @@ -0,0 +1,26 @@ + + + {{ label }}: + {{ value }} + + + + + + diff --git a/src/pages/order/order/components/OrderCard/OrderCardContent.vue b/src/pages/order/order/components/OrderCard/OrderCardContent.vue new file mode 100644 index 0000000..1839328 --- /dev/null +++ b/src/pages/order/order/components/OrderCard/OrderCardContent.vue @@ -0,0 +1,126 @@ + + + + + {{ orderData.commodityName }} + + + {{ orderData.orderAmt }} + + + + + + + + + + diff --git a/src/pages/order/order/components/OrderCard/README.md b/src/pages/order/order/components/OrderCard/README.md new file mode 100644 index 0000000..04f7067 --- /dev/null +++ b/src/pages/order/order/components/OrderCard/README.md @@ -0,0 +1,109 @@ +# OrderCard 组件 + +订单卡片组件,用于显示订单和工单信息。 + +## 组件结构 + +``` +OrderCard/ +├── index.vue # 主组件 +├── OrderCardContent.vue # 卡片内容组件 +├── InfoRow.vue # 信息行组件 +├── images/ # 图片资源 +├── styles/ # 样式文件 +└── README.md # 说明文档 +``` + +## 组件说明 + +### OrderCard (主组件) +- 负责整体布局和事件处理 +- 包含卡片头部、分割线、内容区域和操作区域 +- 处理点击、呼叫等交互事件 + +### OrderCardContent (内容组件) +- 负责根据 `orderType` 动态渲染不同的内容 +- 支持订单类型(0-酒店订单,1-门票订单,2-其他订单)和工单类型 +- 使用条件渲染显示对应的信息字段 + +### InfoRow (信息行组件) +- 可复用的信息展示组件 +- 统一的标签和值的显示格式 +- 支持字符串和数字类型的值 + +## 使用方式 + +```vue + + + + + +``` + +## Props + +### orderData (Object) + +| 字段 | 类型 | 说明 | 必填 | +|------|------|------|------| +| id | String | 订单ID | 是 | +| commodityName | String | 商品名称 | 是 | +| orderType | Number/undefined | 订单类型:0-酒店订单,1-门票订单,2-其他订单,undefined-工单 | 否 | +| orderNumber | String | 订单编号 | 否 | +| checkInTime | String | 入住时间(orderType=0时使用) | 否 | +| visitorName | String | 游客姓名/联系房客 | 否 | +| contactPhone | String | 联系电话 | 否 | +| quantity | Number | 份数(orderType=1,2时使用) | 否 | +| createTime | String | 创建时间(工单时使用) | 否 | +| orderStatus | String | 订单状态 | 否 | +| status | String | 状态 | 否 | + +## Events + +| 事件名 | 说明 | 参数 | +|--------|------|------| +| click | 卡片点击事件 | orderData | +| call | 呼叫事件 | orderData | +| complete | 完成事件 | orderData | + +## 显示逻辑 + +### 订单类型 (orderType !== undefined) + +- **orderType = 0 (酒店订单)**:显示订单编号、入住时间、游客姓名、联系电话 +- **orderType = 1 (门票订单)**:显示订单编号、份数 +- **orderType = 2 (其他订单)**:显示订单编号、份数 + +### 工单类型 (orderType === undefined) + +显示创建时间、联系房客、联系电话 + +## 优势 + +1. **可读性**:组件职责单一,代码结构清晰 +2. **可维护性**:组件化拆分,便于独立维护和测试 +3. **可复用性**:InfoRow 组件可在其他地方复用 +4. **健壮性**:类型检查和默认值处理 +5. **扩展性**:新增订单类型只需修改 OrderCardContent 组件 \ No newline at end of file diff --git a/src/pages/order/order/components/OrderCard/constants/order.js b/src/pages/order/order/components/OrderCard/constants/order.js new file mode 100644 index 0000000..fc349fd --- /dev/null +++ b/src/pages/order/order/components/OrderCard/constants/order.js @@ -0,0 +1,59 @@ +// 订单类型 +export const ORDER_TYPE_MAP = { + 0: [ + { + label: '订单编号', + key: 'orderId' + }, + { + label: '入住时间', + key: 'checkInData' + }, + { + label: '游客姓名', + key: 'visitorName' + }, + { + label: '联系电话', + key: 'contactPhone' + } + ], + 1: [ + { + label: '订单编号', + key: 'orderId' + }, + { + label: '份数', + key: 'commodityAmount' + } + ], + 2: [ + { + label: '订单编号', + key: 'orderId' + }, + { + label: '份数', + key: 'commodityAmount' + } + ], +} + +// 工单类型 +export const SERVICE_TYPE_MAP = { + 0: [ + { + label: '创建时间', + key: 'createTime' + }, + { + label: '联系房客', + key: 'userName' + }, + { + label: '联系电话', + key: 'userPhone' + } + ], +} diff --git a/src/pages/order/order/components/OrderCard/images/food.png b/src/pages/order/order/components/OrderCard/images/food.png new file mode 100644 index 0000000..87cf338 Binary files /dev/null and b/src/pages/order/order/components/OrderCard/images/food.png differ diff --git a/src/pages/order/order/components/OrderCard/images/hotel.png b/src/pages/order/order/components/OrderCard/images/hotel.png new file mode 100644 index 0000000..60bba2e Binary files /dev/null and b/src/pages/order/order/components/OrderCard/images/hotel.png differ diff --git a/src/pages/order/order/components/OrderCard/images/service.png b/src/pages/order/order/components/OrderCard/images/service.png new file mode 100644 index 0000000..90bd017 Binary files /dev/null and b/src/pages/order/order/components/OrderCard/images/service.png differ diff --git a/src/pages/order/order/components/OrderCard/images/ticket.png b/src/pages/order/order/components/OrderCard/images/ticket.png new file mode 100644 index 0000000..175189a Binary files /dev/null and b/src/pages/order/order/components/OrderCard/images/ticket.png differ diff --git a/src/pages/order/order/components/OrderCard/index.vue b/src/pages/order/order/components/OrderCard/index.vue new file mode 100644 index 0000000..a72613a --- /dev/null +++ b/src/pages/order/order/components/OrderCard/index.vue @@ -0,0 +1,135 @@ + + + + + + + + {{ getOrderTypeName() }} + + + + {{ getStatusspan(orderData.orderStatus || orderData.workOrderStatus) }} + + + + + + + + + 去支付 + + + + + + + + diff --git a/src/pages/order/order/components/OrderCard/prompt.md b/src/pages/order/order/components/OrderCard/prompt.md new file mode 100644 index 0000000..0865022 --- /dev/null +++ b/src/pages/order/order/components/OrderCard/prompt.md @@ -0,0 +1,170 @@ +# OrderCard 工单卡片组件 + +## 组件概述 + +OrderCard 是一个用于显示工单信息的卡片组件,支持多种工单状态展示、操作按钮和自定义内容。适用于工单管理、客服系统等场景。 + +## 功能特性 + +- ✅ 多种工单状态支持(待处理、处理中、已完成、已取消) +- ✅ 状态图标和标签显示 +- ✅ 工单基本信息展示 +- ✅ 可配置的操作按钮 +- ✅ 自定义操作区域插槽 +- ✅ 点击事件和操作事件 +- ✅ 响应式设计 +- ✅ 暗色模式支持 +- ✅ 优雅的交互动画 + +## 组件属性 (Props) + +| 属性名 | 类型 | 默认值 | 必填 | 说明 | +|--------|------|--------|------|------| +| orderData | Object | - | 是 | 工单数据对象 | +| showActions | Boolean | true | 否 | 是否显示操作按钮区域 | + +### orderData 对象结构 + +```javascript +{ + id: String, // 工单ID + title: String, // 工单标题 + createTime: String, // 创建时间 + contactName: String, // 联系人姓名 + contactPhone: String, // 联系电话 + status: String // 工单状态:pending/processing/completed/cancelled +} +``` + +## 组件事件 (Events) + +| 事件名 | 参数 | 说明 | +|--------|------|------| +| click | orderData | 卡片点击事件 | +| call | orderData | 呼叫按钮点击事件 | +| complete | orderData | 完成按钮点击事件 | + +## 插槽 (Slots) + +| 插槽名 | 说明 | +|--------|------| +| actions | 自定义操作按钮区域 | + +## 工单状态说明 + +| 状态值 | 显示文本 | 图标颜色 | 标签样式 | +|--------|----------|----------|----------| +| pending | 待处理 | 橙色 | 橙色边框 | +| processing | 处理中 | 蓝色 | 蓝色边框 | +| completed | 已完成 | 绿色 | 绿色边框 | +| cancelled | 已取消 | 灰色 | 灰色边框 | + +## 使用示例 + +### 基础用法 + +```vue + + + + + +``` + +### 隐藏操作按钮 + +```vue + + + +``` + +### 自定义操作按钮 + +```vue + + + + + 编辑 + + + 删除 + + + + +``` + +## 样式定制 + +组件支持通过 CSS 变量进行样式定制: + +```css +.order-card { + --card-bg: #ffffff; + --card-radius: 12px; + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + --primary-color: #007AFF; + --success-color: #52C41A; + --warning-color: #FF8C00; + --danger-color: #FF3B30; +} +``` + +## 响应式支持 + +- 在小屏设备(≤375px)上自动调整字体大小和间距 +- 支持暗色模式自动适配 +- 触摸设备优化的交互体验 + +## 注意事项 + +1. **数据格式**:确保传入的 `orderData` 包含所有必需字段 +2. **状态值**:`status` 字段必须是预定义的状态值之一 +3. **事件处理**:使用 `@click.stop` 防止操作按钮事件冒泡 +4. **性能优化**:大量卡片时建议使用虚拟滚动 + +## 更新日志 + +### v1.0.0 (2024-01-15) +- 初始版本发布 +- 支持基础工单信息展示 +- 支持多种状态和操作按钮 +- 支持自定义插槽 +- 响应式设计和暗色模式支持 \ No newline at end of file diff --git a/src/pages/order/order/components/OrderCard/styles/index.scss b/src/pages/order/order/components/OrderCard/styles/index.scss new file mode 100644 index 0000000..43e108e --- /dev/null +++ b/src/pages/order/order/components/OrderCard/styles/index.scss @@ -0,0 +1,43 @@ +.order-card { + transition: all 0.3s ease; + + &:active { + transform: scale(0.98); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + } +} + +.status-icon { + width: 20px; + height: 20px; +} + +.status-tag { + &.tag-0 { + color: #ff3d60; + } + + &.tag-1 { + color: #f00044; + } + + &.tag-2 { + color: #40ae36; + } + + &.tag-3 { + color: #808389; + } + + &.tag-4 { + color: $theme-color-500; + } + + &.tag-5 { + color: #808389; + } + + &.tag-6 { + color: #fd8702; + } +} diff --git a/src/pages/order/order/components/OrderInfo/index.vue b/src/pages/order/order/components/OrderInfo/index.vue new file mode 100644 index 0000000..9bfccc5 --- /dev/null +++ b/src/pages/order/order/components/OrderInfo/index.vue @@ -0,0 +1,52 @@ + + + + 订单编号 + {{ orderData.orderId }} + + + 下单时间 + {{ orderData.createTime }} + + + 支付状态 + + {{ statusspan }} + + + + + + + + diff --git a/src/pages/order/order/components/OrderQrcode/index.vue b/src/pages/order/order/components/OrderQrcode/index.vue new file mode 100644 index 0000000..67213e4 --- /dev/null +++ b/src/pages/order/order/components/OrderQrcode/index.vue @@ -0,0 +1,304 @@ + + + + + + + 核销凭证 + + + 请向工作人员出示此码 + + + + + + + + + + + {{ selectedVoucher.name }} + + + + + + 总计{{ selectedVoucher.count }}{{ selectedVoucher.unit }} + + + 剩{{ selectedVoucher.count - selectedVoucher.writeOffCount + }}{{ selectedVoucher.unit }}可用 + + + + + + + + 凭证{{ + selectedVoucherList.length > 1 ? currentVoucherIndex + 1 : "" + }} + + + 此码仅可核销{{ + selectedVoucher.count - selectedVoucher.writeOffCount + }}份 + + + + + + + + + {{ selectedVoucher.name }} + + + + + + + + + + + + {{ currentVoucherIndex + 1 }} + + + /{{ selectedVoucherList.length }} + + + + 扫码后点击下一张 + + + + + + + + + + + + + + + diff --git a/src/pages/order/order/components/OrderQrcode/styles/index.scss b/src/pages/order/order/components/OrderQrcode/styles/index.scss new file mode 100644 index 0000000..a7a78da --- /dev/null +++ b/src/pages/order/order/components/OrderQrcode/styles/index.scss @@ -0,0 +1,23 @@ +.refund-popup { + border-radius: 15px 15px 0 0; + padding-bottom: 40px; +} + +.close { + top: 14px; + right: 12px; +} + +.borderColor { + border-color: $theme-color-500; +} + +.actions-btn { + width: 40px; + height: 40px; + border-radius: 20px; + background-color: #F5F5F5; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/src/pages/order/order/components/OrderStatusInfo/images/2025-07-13_115156.png b/src/pages/order/order/components/OrderStatusInfo/images/2025-07-13_115156.png new file mode 100644 index 0000000..112743d Binary files /dev/null and b/src/pages/order/order/components/OrderStatusInfo/images/2025-07-13_115156.png differ diff --git a/src/pages/order/order/components/OrderStatusInfo/index.vue b/src/pages/order/order/components/OrderStatusInfo/index.vue new file mode 100644 index 0000000..6971f0b --- /dev/null +++ b/src/pages/order/order/components/OrderStatusInfo/index.vue @@ -0,0 +1,48 @@ + + + {{ currentStatusspan }} + + + + + + diff --git a/src/pages/order/order/components/OrderStatusInfo/prompt.md b/src/pages/order/order/components/OrderStatusInfo/prompt.md new file mode 100644 index 0000000..a6724fe --- /dev/null +++ b/src/pages/order/order/components/OrderStatusInfo/prompt.md @@ -0,0 +1,14 @@ +## 订单状态组件 + +组件名称:订单状态组件 + +## 提示词: + +使用 uniapp + vue3 组合式 api 开发微信小程序,要求如下: +1、按照提供的图片,高度还原交互设计 +2、要求布局样式结构简洁明了,class 命名请按照模块名称来命名,例如:.order-status +3、可以使用 uniapp 内置的组件 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/pages/order/order/components/OrderStatusInfo/styles/index.scss b/src/pages/order/order/components/OrderStatusInfo/styles/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/order/order/components/Tabs/README.md b/src/pages/order/order/components/Tabs/README.md new file mode 100644 index 0000000..54f8c81 --- /dev/null +++ b/src/pages/order/order/components/Tabs/README.md @@ -0,0 +1,250 @@ +# Tab 切换组件 + +一个功能完整的 Tab 切换组件,支持动画过渡、自定义内容和响应式设计。 + +## 功能特性 + +- ✅ **多标签切换**:支持任意数量的标签页切换 +- ✅ **动画指示器**:选中状态下划线,支持平滑滑动动画 +- ✅ **自定义内容**:支持插槽自定义标签内容 +- ✅ **响应式设计**:适配不同屏幕尺寸 +- ✅ **动态宽度**:下划线宽度根据文字宽度动态调整 +- ✅ **事件支持**:完整的切换事件和双向绑定 +- ✅ **主题定制**:支持自定义指示器颜色 +- ✅ **uniapp 兼容**:使用 uniapp 内置组件开发 + +## 基础用法 + +```vue + + + + + +``` + +## 自定义标签内容 + +使用 `tab-item` 插槽可以完全自定义标签的显示内容: + +```vue + + + + + + {{ item.label }} + {{ item.badge }} + + + + + + +``` + +## 双向绑定 + +支持 `v-model` 双向绑定当前选中的索引: + +```vue + + + 当前选中索引:{{ activeIndex }} + + + +``` + +## 动态标签 + +支持动态添加和删除标签: + +```vue + + + 添加标签 + 删除标签 + + + +``` + +## Props + +| 参数 | 类型 | 默认值 | 说明 | +| -------------- | ------ | --------------------------------------------------------------------- | ---------------------------- | +| tabs | Array | `[{label:'全部订单',value:'all'},{label:'服务工单',value:'service'}]` | 标签数据数组 | +| defaultActive | Number | `0` | 默认选中的标签索引 | +| indicatorColor | String | `#007AFF` | 指示器颜色 | +| modelValue | Number | - | 当前选中索引(用于 v-model) | + +### tabs 数组项结构 + +```typescript +interface TabItem { + label: string; // 标签显示文本 + value: string; // 标签值 + [key: string]: any; // 其他自定义属性 +} +``` + +## Events + +| 事件名 | 说明 | 参数 | +| ----------------- | --------------------- | ---------------------------------- | +| change | 标签切换时触发 | `{ index: number, item: TabItem }` | +| update:modelValue | 用于 v-model 双向绑定 | `index: number` | + +## Slots + +| 插槽名 | 说明 | 作用域参数 | +| -------- | -------------- | ----------------------------------------------------- | +| tab-item | 自定义标签内容 | `{ item: TabItem, index: number, isActive: boolean }` | + +## 方法 + +通过 `ref` 可以调用组件的方法: + +```vue + + + 切换到第二个标签 + + + +``` + +| 方法名 | 说明 | 参数 | 返回值 | +| -------------- | ---------------------- | --------------- | --------- | +| setActiveIndex | 设置当前选中的标签 | `index: number` | - | +| getActiveIndex | 获取当前选中的标签索引 | - | `number` | +| getActiveItem | 获取当前选中的标签项 | - | `TabItem` | + +## 样式定制 + +### CSS 变量 + +组件支持通过 CSS 变量进行样式定制: + +```scss +.tab-container { + --tab-bg-color: #fff; // 背景色 + --tab-text-color: #666; // 文字颜色 + --tab-active-color: #333; // 选中文字颜色 + --tab-indicator-color: #007aff; // 指示器颜色 + --tab-border-color: #f0f0f0; // 边框颜色 +} +``` + +### 自定义主题 + +```vue + + + + + +``` + +## 技术实现 + +- **框架**:Vue 3 组合式 API +- **平台**:uniapp 跨平台开发 +- **动画**:CSS3 transition + transform +- **响应式**:CSS media queries +- **兼容性**:微信小程序、H5、App + +## 设计规范 + +- 遵循微信小程序设计规范 +- 支持无障碍访问 +- 响应式设计,适配不同设备 +- 流畅的动画过渡效果 +- 一致的视觉风格 + +## 兼容性 + +| 平台 | 支持情况 | +| ------------ | ----------- | +| 微信小程序 | ✅ 完全支持 | +| H5 | ✅ 完全支持 | +| App | ✅ 完全支持 | +| 支付宝小程序 | ✅ 完全支持 | +| 百度小程序 | ✅ 完全支持 | + +## 更新日志 + +### v1.0.0 + +- ✨ 初始版本发布 +- ✨ 支持基础标签切换功能 +- ✨ 支持动画指示器 +- ✨ 支持自定义标签内容 +- ✨ 支持响应式设计 +- ✨ 支持事件和双向绑定 + +## 备注 + +仅供学习、交流使用,请勿用于商业用途。 diff --git a/src/pages/order/order/components/Tabs/index.vue b/src/pages/order/order/components/Tabs/index.vue new file mode 100644 index 0000000..b844635 --- /dev/null +++ b/src/pages/order/order/components/Tabs/index.vue @@ -0,0 +1,275 @@ + + + + + + + {{ item.label }} + + + + + + + + + + + + + diff --git a/src/pages/order/order/components/Tabs/propmt.md b/src/pages/order/order/components/Tabs/propmt.md new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/order/order/components/Tabs/styles/index.scss b/src/pages/order/order/components/Tabs/styles/index.scss new file mode 100644 index 0000000..385205c --- /dev/null +++ b/src/pages/order/order/components/Tabs/styles/index.scss @@ -0,0 +1,100 @@ +.tab-container { + position: relative; +} + +.tab-wrapper { + display: flex; + align-items: center; + justify-content: center; + height: 30px; +} + +.tab-item { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + position: relative; + transition: all 0.3s ease; + padding: 0 8px; +} + +.tab-text { + font-size: $uni-font-size-base; + color: #666; + font-weight: 400; + transition: all 0.3s ease; + white-space: nowrap; +} + +.tab-text-active { + color: $uni-text-color; + font-size: $uni-font-size-lg; + font-weight: 600; +} + +.tab-item-active { + .tab-text { + color: $uni-text-color; + font-weight: 600; + } +} + +.tab-indicator { + position: absolute; + bottom: 0; + height: 3px; + min-height: 3px; /* 确保最小高度 */ + background-color: #007aff; + border-radius: 10px; + transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), + width 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: 1; + transform: translateZ(0); /* 启用硬件加速 */ + will-change: left, width; /* 优化动画性能 */ + + /* 初始状态:未初始化时隐藏 */ + opacity: 0; + width: 15px; /* 默认宽度15px */ + left: 0; +} + +/* 已初始化状态 */ +.tab-indicator.initialized { + opacity: 1; +} + +/* 点击效果 */ +.tab-item:active { + opacity: 0.7; +} + +/* 自定义主题色支持 */ +.tab-container[data-indicator-color="red"] .tab-indicator { + background-color: #ff4d4f; +} + +.tab-container[data-indicator-color="green"] .tab-indicator { + background-color: #52c41a; +} + +.tab-container[data-indicator-color="orange"] .tab-indicator { + background-color: #fa8c16; +} + +/* 动画增强 */ +@keyframes tabSwitch { + 0% { + transform: translateZ(0) scaleX(0.8); + opacity: 0.6; + } + 100% { + transform: translateZ(0) scaleX(1); + opacity: 1; + } +} + +.tab-indicator.animating { + animation: tabSwitch 0.3s ease-out; +} diff --git a/src/pages/order/order/components/Tabs/test.vue b/src/pages/order/order/components/Tabs/test.vue new file mode 100644 index 0000000..a2638f5 --- /dev/null +++ b/src/pages/order/order/components/Tabs/test.vue @@ -0,0 +1,308 @@ + + + + Tab组件测试 + + + + 基础用法 + + + 当前选中: {{ currentTab.label }} + + + + + + 多标签测试 + + + 当前选中: {{ currentMultiTab.label }} + + + + + + 快速切换测试 + + + 当前选中: {{ currentFastTab.label }} + + + + 切换到{{ tab.label }} + + + + + + + 初始化测试 + 测试指示器的动态高度和宽度初始化及错误处理 + + + 当前选中: {{ currentInitTab.label }} + + + + {{ showInitTest ? "隐藏" : "显示" }}组件 + + + 切换默认激活项 (当前: {{ initActiveIndex }}) + + 添加Tab + 移除Tab + 快速切换测试 + + + 错误处理测试:组件现在能够安全处理实例为null的情况 + + + + + + + + + diff --git a/src/pages/order/order/components/UserInfo/index.vue b/src/pages/order/order/components/UserInfo/index.vue new file mode 100644 index 0000000..400232c --- /dev/null +++ b/src/pages/order/order/components/UserInfo/index.vue @@ -0,0 +1,42 @@ + + + + + 住客姓名 + {{ item.visitorName }} + + + + 联系电话 + {{ contactPhone }} + + + + + + + diff --git a/src/pages/order/order/components/VoucherList/index.vue b/src/pages/order/order/components/VoucherList/index.vue new file mode 100644 index 0000000..04143d5 --- /dev/null +++ b/src/pages/order/order/components/VoucherList/index.vue @@ -0,0 +1,69 @@ + + + + 核销凭证列表 + + + + + {{ item.name }} + + + 总计{{ item.count }}{{ item.unit }} + + + 剩{{ item.count - item.writeOffCount }}{{ item.unit }}可用 + + + + + + 出示凭证 + 已核销 + + + + + + diff --git a/src/pages/order/order/detail.vue b/src/pages/order/order/detail.vue new file mode 100644 index 0000000..145b369 --- /dev/null +++ b/src/pages/order/order/detail.vue @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/order/order/list.vue b/src/pages/order/order/list.vue new file mode 100644 index 0000000..f05df7d --- /dev/null +++ b/src/pages/order/order/list.vue @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + diff --git a/src/pages/order/order/prompt.md b/src/pages/order/order/prompt.md new file mode 100644 index 0000000..f80e6e0 --- /dev/null +++ b/src/pages/order/order/prompt.md @@ -0,0 +1,181 @@ +# 订单管理系统组件需求文档 + +## 项目概述 + +订单管理系统的核心组件库,包含工单卡片、工单列表、咨询栏等核心功能组件。 + +## TopNavBar 组件 + +### 功能要求 + +- 顶部导航栏展示 +- 支持自定义标题内容 +- 支持插槽扩展 + +### 设计要求 + +- 固定在页面顶部 +- 背景色与主题一致 +- 高度适中,不占用过多空间 + +## Tabs 组件 + +### 功能要求 + +- 标签页切换功能 +- 支持默认激活项 +- 切换动画效果 +- 事件回调 + +### 设计要求 + +- 标签间距均匀 +- 激活状态明显 +- 切换动画流畅 +- 响应式适配 + +## OrderCard 组件 + +### 功能要求 + +- 展示工单基本信息(标题、时间、联系人、状态等) +- 支持点击事件 +- 支持呼叫功能 +- 支持完成操作 +- 状态标识清晰 + +### 设计要求 + +- 卡片式布局,圆角设计 +- 信息层次分明 +- 操作按钮位置合理 +- 状态颜色区分明显 +- 支持不同状态的视觉反馈 + +## OrderList 组件 + +### 功能要求 + +- 显示工单列表 +- 集成z-paging组件,支持虚拟列表 +- 支持自定义下拉刷新(文案、样式、阈值) +- 支持自定义上拉加载更多(文案、样式、阈值) +- 自动管理空数据状态 +- 支持固定高度和自适应高度模式 +- 完整的事件回调机制 +- 加载状态管理 + +### 设计要求 + +- 列表项间距合理 +- 加载动画流畅 +- 空状态友好提示 +- 响应式布局 +- 虚拟列表优化大数据渲染性能 + +### z-paging配置 + +- `useVirtualList`: 是否启用虚拟列表(默认true) +- `virtualListHeight`: 虚拟列表高度(默认100%) +- `cellHeightMode`: 单元格高度模式(auto/fixed) +- `fixedHeight`: 固定高度值(当cellHeightMode为fixed时使用) +- `customEmptydiv`: 是否使用自定义空状态 + +## ConsultationBar 组件 + +### 功能要求 + +- 底部固定咨询栏 +- 显示客服信息和联系方式 +- 支持立即咨询功能 +- 默认隐藏,点击"立即呼叫"按钮后显示 +- "立即咨询"按钮单独一行显示 +- 支持显示/隐藏动画效果 + +### 设计要求 + +- 固定在页面底部 +- 背景半透明或纯色 +- 按钮样式与主题一致 +- 信息布局清晰 +- 支持安全区域适配 +- 显示/隐藏动画流畅 + +## 数据结构 + +### 工单数据结构 + +```javascript +{ + id: String, // 工单ID + title: String, // 工单标题 + createTime: String, // 创建时间 + contactName: String, // 联系人姓名 + contactPhone: String, // 联系电话 + status: String, // 状态:pending-待处理, processing-处理中, completed-已完成, cancelled-已取消 + type: String // 类型:service-服务工单, order-普通订单 +} +``` + +### Tab数据结构 + +```javascript +{ + label: String, // 显示文本 + value: String // 值 +} +``` + +## 技术要求 + +### 框架和库 + +- Vue 3 Composition API +- uni-app框架 +- z-paging组件(用于列表优化) +- SCSS样式预处理 + +### 性能优化 + +- 虚拟列表支持大数据量渲染 +- 图片懒加载 +- 组件按需加载 +- 合理的缓存策略 + +### 兼容性 + +- 支持微信小程序 +- 支持H5 +- 支持APP +- 响应式设计,适配不同屏幕尺寸 + +## 更新日志 + +### v1.2.0 (最新) + +- ✅ 集成z-paging组件到OrderList +- ✅ 支持虚拟列表,提升大数据渲染性能 +- ✅ 自定义下拉刷新和上拉加载更多 +- ✅ 自动管理空数据状态 +- ✅ 支持固定高度和自适应高度模式 +- ✅ 完整的事件回调机制 +- ✅ 创建OrderList演示页面 + +### v1.1.0 + +- ✅ 修改ConsultationBar组件布局 +- ✅ "立即咨询"按钮单独一行显示 +- ✅ 默认隐藏,点击"立即呼叫"后显示 +- ✅ 添加显示/隐藏动画效果 +- ✅ 更新相关样式和交互逻辑 + +### v1.0.0 + +- ✅ 完成OrderCard组件开发 +- ✅ 完成OrderList组件开发 +- ✅ 完成ConsultationBar组件开发 +- ✅ 完成TopNavBar组件开发 +- ✅ 完成Tabs组件开发 +- ✅ 完成订单管理页面集成 +- ✅ 创建组件演示页面 +- ✅ 编写技术文档 diff --git a/src/pages/order/order/styles/detail.scss b/src/pages/order/order/styles/detail.scss new file mode 100644 index 0000000..973ef3e --- /dev/null +++ b/src/pages/order/order/styles/detail.scss @@ -0,0 +1,4 @@ +.order-detail-wrapper { + background: linear-gradient(180deg, $theme-color-100 0%, #f5f7fa 100%); + padding: 0 12px 40px; +} diff --git a/src/pages/order/order/styles/list.scss b/src/pages/order/order/styles/list.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/quick/components/Card/index.vue b/src/pages/quick/components/Card/index.vue new file mode 100644 index 0000000..0cc5bc0 --- /dev/null +++ b/src/pages/quick/components/Card/index.vue @@ -0,0 +1,81 @@ + + + + + + {{ item.commodityName }} + + + {{ item.commodityFacility.join(" ") }} + + + {{ item.commodityTradeRuleList.join(" / ") }} + + + + + {{ item.specificationPrice }} + + + /{{ item.stockUnitLabel }} + + 订 + + + + + + + + diff --git a/src/pages/quick/components/Card/styles/index.scss b/src/pages/quick/components/Card/styles/index.scss new file mode 100644 index 0000000..3f1f74d --- /dev/null +++ b/src/pages/quick/components/Card/styles/index.scss @@ -0,0 +1,22 @@ +.left { + height: 80px; + width: 80px; +} + +.right { + max-width: 259px; + height: 100%; +} + +.amt { + &::before { + content: "¥"; + font-size: 12px; + margin-right: 4px; + } +} + +.btn { + background: linear-gradient(90deg, #ff3d60 57%, #ff990c 100%); + padding: 4px 8px; +} diff --git a/src/pages/quick/components/Tabs/index.vue b/src/pages/quick/components/Tabs/index.vue new file mode 100644 index 0000000..af8d947 --- /dev/null +++ b/src/pages/quick/components/Tabs/index.vue @@ -0,0 +1,146 @@ + + + + + + + {{ zniconsMap[item.iconCode] }} + + + + {{ item.iconTitle }} + + + + + + + + + + + + diff --git a/src/pages/quick/components/Tabs/styles/index.scss b/src/pages/quick/components/Tabs/styles/index.scss new file mode 100644 index 0000000..d412c4a --- /dev/null +++ b/src/pages/quick/components/Tabs/styles/index.scss @@ -0,0 +1,90 @@ +.tab-wrapper { + background-color: $theme-color-100; + height: 48px; + overflow-x: auto; + /* 支持横向滚动 */ + -webkit-overflow-scrolling: touch; + /* 平滑滚动(移动端) */ + white-space: nowrap; + /* 防止换行 */ + justify-content: flex-start; + /* 覆盖工具类,靠左排列以便滚动 */ +} + +.tab-item { + height: 100%; + flex: 0 0 auto; + /* 不让子项拉伸,按内容宽度排列 */ + padding: 0 12px; + /* 增加横向间距,便于触控 */ + min-width: 68px; + /* 保证可点击区域 */ +} + +.icon { + height: 20px; + width: 20px; + color: #525866; +} + +.icon-active { + color: $theme-color-500; +} + +/* 组件模板中使用了绝对定位的内部元素,为保证父元素宽度基于内容,重置该子元素为静态布局 */ +.tab-item>.absolute { + position: static !important; + display: flex; + align-items: center; +} + +.tab-item-active { + &::before { + content: ""; + position: absolute; + left: 4px; + top: 0; + right: 4px; + bottom: 0; + background-color: #fff; + border-radius: 20px 20px 0 0; + transform: perspective(40px) rotateX(6deg) translate(0, -1px); + transform-origin: bottom bottom; + box-shadow: 0 -0.5px 0 $theme-color-500; + } +} + +.tab-text-active { + color: $theme-color-500; + z-index: 3; +} + +/* 已改为每项内部指示器,移除了全局指示器样式 */ + +/* 每项内的指示器(替代全局指示器) */ +.tab-item-inner { + display: inline-flex; + align-items: center; + position: relative; + z-index: 3; + /* 确保内容(icon/text)位于 .tab-item-active::before 之上 */ +} + +.tab-item-indicator { + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%) scaleX(0.9); + height: 3px; + width: 24px; + background-color: $theme-color-500; + border-radius: 3px 3px 0 0; + opacity: 0; + transition: opacity 0.2s ease, transform 0.2s ease; + z-index: 3; +} + +.tab-item-indicator.visible { + opacity: 1; + transform: translateX(-50%) scaleX(1); +} \ No newline at end of file diff --git a/src/pages/quick/list.vue b/src/pages/quick/list.vue new file mode 100644 index 0000000..c3f402c --- /dev/null +++ b/src/pages/quick/list.vue @@ -0,0 +1,151 @@ + + + + + + + + + + + 入住 + + {{ selectedDate.startDate }} + + + + + + {{ selectedDate.totalDays }}晚 + + + + 离店 + + {{ selectedDate.endDate }} + + + + + + + + + + + + + + + + + + diff --git a/src/pages/service/order/components/OrderCard/index.vue b/src/pages/service/order/components/OrderCard/index.vue new file mode 100644 index 0000000..2e8eff2 --- /dev/null +++ b/src/pages/service/order/components/OrderCard/index.vue @@ -0,0 +1,128 @@ + + + + + + 工单 + {{ + isCancelWork ? "已取消" : workOrderStatus(orderData.workOrderStatus) + }} + + + + + + 所在位置:{{ orderData.roomNo }} + + + 联系方式: {{ orderData.userPhone }} + + + 需求描述: {{ orderData.content }} + + + + + + + + + + 服务人员:{{ orderData.processMemberName }} + + + 拨打电话 + + + + + 取消呼叫 + + + + + + + diff --git a/src/pages/service/order/components/OrderCard/styles/index.scss b/src/pages/service/order/components/OrderCard/styles/index.scss new file mode 100644 index 0000000..3e680a8 --- /dev/null +++ b/src/pages/service/order/components/OrderCard/styles/index.scss @@ -0,0 +1,28 @@ +.order-card { + transition: all 0.3s ease; + + &:active { + transform: scale(0.98); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + } +} + +.icon { + height: 20px; + width: 20px; +} + +.right { + height: 88px; + width: 88px; +} + +.service-user { + background-color: rgba(67, 103, 153, 0.1); + border-radius: 5px; + color: #436799; +} + +.cancel { + padding: 4px 10px; +} diff --git a/src/pages/service/order/list.vue b/src/pages/service/order/list.vue new file mode 100644 index 0000000..9d293a1 --- /dev/null +++ b/src/pages/service/order/list.vue @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + diff --git a/src/styles/main.css b/src/styles/main.css index d56ff01..c4eac3c 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -94,10 +94,14 @@ button:disabled { cursor: not-allowed; } -.app-viewport { +.app-divport { min-height: 100dvh; background: - linear-gradient(180deg, rgba(238, 248, 255, 0.95) 0%, rgba(245, 247, 250, 0) 280px), + linear-gradient( + 180deg, + rgba(238, 248, 255, 0.95) 0%, + rgba(245, 247, 250, 0) 280px + ), var(--color-app-bg); } diff --git a/src/utils/requets.ts b/src/utils/requets.ts new file mode 100644 index 0000000..e69de29