feat: 商品详情交互开发
This commit is contained in:
461
components/FormCard/README.md
Normal file
461
components/FormCard/README.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# FormCard 表单卡片组件
|
||||
|
||||
一个功能完整的表单卡片组件,支持姓名和手机号输入,具备数据验证和双向绑定功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 📝 **双向绑定**:支持 v-model 双向数据绑定
|
||||
- ✅ **数据验证**:内置手机号格式验证
|
||||
- 🎨 **自定义标题**:可配置游客标题文本
|
||||
- 🗑️ **删除功能**:支持删除操作,可配置显示/隐藏
|
||||
- 💫 **交互反馈**:输入框聚焦效果和错误状态提示
|
||||
- 📱 **响应式设计**:适配不同屏幕尺寸
|
||||
- 🎯 **事件支持**:完整的事件系统
|
||||
- ⚡ **性能优化**:使用计算属性优化渲染
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 默认使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FormCard
|
||||
:form="form"
|
||||
@update:name="form.name = $event"
|
||||
@update:phone="form.phone = $event"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import FormCard from '@/components/FormCard/index.vue'
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('删除表单')
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 自定义标题
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FormCard
|
||||
:form="form"
|
||||
title="成人票"
|
||||
@update:name="form.name = $event"
|
||||
@update:phone="form.phone = $event"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 隐藏删除图标
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FormCard
|
||||
:form="form"
|
||||
title="联系人信息"
|
||||
:show-delete-icon="false"
|
||||
@update:name="form.name = $event"
|
||||
@update:phone="form.phone = $event"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 多个表单卡片
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<FormCard
|
||||
v-for="(item, index) in formList"
|
||||
:key="index"
|
||||
:form="item"
|
||||
:title="`游客${index + 1}`"
|
||||
@update:name="item.name = $event"
|
||||
@update:phone="item.phone = $event"
|
||||
@delete="handleDeleteForm(index)"
|
||||
/>
|
||||
<button @click="addForm">添加游客</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const formList = ref([
|
||||
{ name: '', phone: '' },
|
||||
{ name: '', phone: '' }
|
||||
])
|
||||
|
||||
const handleDeleteForm = (index) => {
|
||||
formList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const addForm = () => {
|
||||
formList.value.push({ name: '', phone: '' })
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 表单验证
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<FormCard
|
||||
ref="formCardRef"
|
||||
:form="form"
|
||||
title="验证示例"
|
||||
@update:name="form.name = $event"
|
||||
@update:phone="form.phone = $event"
|
||||
/>
|
||||
<button @click="validateForm">验证表单</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const formCardRef = ref()
|
||||
const form = reactive({
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
// 手动触发验证
|
||||
formCardRef.value.validateName()
|
||||
formCardRef.value.validatePhone()
|
||||
|
||||
// 或者使用工具函数检查
|
||||
const nameError = formCardRef.value.getNameError(form.name)
|
||||
const phoneError = formCardRef.value.getPhoneError(form.phone)
|
||||
|
||||
if (!nameError && !phoneError) {
|
||||
console.log('表单验证通过')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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
|
||||
<template>
|
||||
<FormCard
|
||||
ref="formCardRef"
|
||||
:form="form"
|
||||
title="游客信息"
|
||||
@update:name="form.name = $event"
|
||||
@update:phone="form.phone = $event"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
<button @click="validateForm">提交</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
const formCardRef = ref()
|
||||
const form = reactive({
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const validateForm = () => {
|
||||
// 使用组件内置的验证方法
|
||||
const nameError = formCardRef.value.getNameError(form.name)
|
||||
const phoneError = formCardRef.value.getPhoneError(form.phone)
|
||||
|
||||
if (nameError) {
|
||||
uni.showToast({
|
||||
title: nameError,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (phoneError) {
|
||||
uni.showToast({
|
||||
title: phoneError,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
console.log('表单数据:', form)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 动态表单管理
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<FormCard
|
||||
v-for="(item, index) in passengers"
|
||||
:key="item.id"
|
||||
:form="item"
|
||||
@update:name="item.name = $event"
|
||||
@update:phone="item.phone = $event"
|
||||
:title="getPassengerTitle(item.type, index)"
|
||||
:show-delete-icon="passengers.length > 1"
|
||||
@delete="removePassenger(index)"
|
||||
/>
|
||||
|
||||
<view class="action-buttons">
|
||||
<button @click="addAdult">添加成人</button>
|
||||
<button @click="addChild">添加儿童</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const passengers = ref([
|
||||
{ id: 1, type: 'adult', name: '', phone: '' }
|
||||
])
|
||||
|
||||
let nextId = 2
|
||||
|
||||
const getPassengerTitle = (type, index) => {
|
||||
return type === 'adult' ? `成人${index + 1}` : `儿童${index + 1}`
|
||||
}
|
||||
|
||||
const addAdult = () => {
|
||||
passengers.value.push({
|
||||
id: nextId++,
|
||||
type: 'adult',
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
}
|
||||
|
||||
const addChild = () => {
|
||||
passengers.value.push({
|
||||
id: nextId++,
|
||||
type: 'child',
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
}
|
||||
|
||||
const removePassenger = (index) => {
|
||||
if (passengers.value.length > 1) {
|
||||
passengers.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
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
|
||||
256
components/FormCard/demo.vue
Normal file
256
components/FormCard/demo.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<view class="demo-container">
|
||||
<view class="demo-title">FormCard 表单组件演示</view>
|
||||
|
||||
<view class="demo-description">
|
||||
<text>✨ 支持姓名和手机号输入,自动过滤非数字字符</text>
|
||||
<text>👤 姓名失去焦点时验证,自动去除首尾空格</text>
|
||||
<text>📱 手机号失去焦点时验证,支持中国大陆手机号格式</text>
|
||||
<text>🎨 优化UI设计,支持悬停效果和动画</text>
|
||||
<text>🗑️ 支持删除操作(可配置显示/隐藏)</text>
|
||||
<text>📱 响应式设计,适配小屏幕设备</text>
|
||||
</view>
|
||||
|
||||
<!-- 示例1: 基础用法 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例1: 基础用法</view>
|
||||
<FormCard
|
||||
:form="form1"
|
||||
title="游客1"
|
||||
@update:name="form1.name = $event"
|
||||
@update:phone="form1.phone = $event"
|
||||
@delete="handleDelete1"
|
||||
/>
|
||||
<view class="form-data">
|
||||
<text>姓名: {{ form1.name }}</text>
|
||||
<text>手机号: {{ form1.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 示例2: 自定义标题 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例2: 自定义标题</view>
|
||||
<FormCard
|
||||
:form="form2"
|
||||
title="成人票"
|
||||
@update:name="form2.name = $event"
|
||||
@update:phone="form2.phone = $event"
|
||||
@delete="handleDelete2"
|
||||
/>
|
||||
<view class="form-data">
|
||||
<text>姓名: {{ form2.name }}</text>
|
||||
<text>手机号: {{ form2.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 示例3: 隐藏删除图标 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例3: 隐藏删除图标</view>
|
||||
<FormCard
|
||||
:form="form3"
|
||||
title="联系人信息"
|
||||
:show-delete-icon="false"
|
||||
@update:name="form3.name = $event"
|
||||
@update:phone="form3.phone = $event"
|
||||
/>
|
||||
<view class="form-data">
|
||||
<text>姓名: {{ form3.name }}</text>
|
||||
<text>手机号: {{ form3.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 示例4: 多个表单卡片 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例4: 多个表单卡片</view>
|
||||
<FormCard
|
||||
v-for="(item, index) in formList"
|
||||
:key="index"
|
||||
:form="item"
|
||||
:title="`游客${index + 1}`"
|
||||
@update:name="item.name = $event"
|
||||
@update:phone="item.phone = $event"
|
||||
@delete="handleDeleteForm(index)"
|
||||
/>
|
||||
<button class="add-btn" @click="addForm">添加游客</button>
|
||||
|
||||
<view class="form-list-data">
|
||||
<view class="list-title">表单数据:</view>
|
||||
<view v-for="(item, index) in formList" :key="index" class="list-item">
|
||||
<text>游客{{ index + 1 }}: {{ item.name }} - {{ item.phone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import FormCard from './index.vue'
|
||||
|
||||
// 单个表单数据
|
||||
const form1 = reactive({
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
const form2 = reactive({
|
||||
name: '张三',
|
||||
phone: '13800138000'
|
||||
})
|
||||
|
||||
const form3 = reactive({
|
||||
name: '',
|
||||
phone: ''
|
||||
})
|
||||
|
||||
// 多个表单数据
|
||||
const formList = ref([
|
||||
{ name: '', phone: '' },
|
||||
{ name: '', phone: '' }
|
||||
])
|
||||
|
||||
// 事件处理
|
||||
const handleDelete1 = () => {
|
||||
console.log('删除表单1')
|
||||
uni.showToast({
|
||||
title: '删除表单1',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete2 = () => {
|
||||
console.log('删除表单2')
|
||||
uni.showToast({
|
||||
title: '删除表单2',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteForm = (index) => {
|
||||
console.log(`删除表单${index + 1}`)
|
||||
formList.value.splice(index, 1)
|
||||
uni.showToast({
|
||||
title: `删除游客${index + 1}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
const addForm = () => {
|
||||
formList.value.push({ name: '', phone: '' })
|
||||
uni.showToast({
|
||||
title: '添加游客成功',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.demo-container {
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 20rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.demo-description {
|
||||
margin-bottom: 40rpx;
|
||||
padding: 20rpx;
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid #bae6fd;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #0369a1;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20rpx;
|
||||
color: #333;
|
||||
border-left: 6rpx solid #00a6ff;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
|
||||
.form-data {
|
||||
margin-top: 20rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
border: 1px solid #e5e8ef;
|
||||
|
||||
text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: #00a6ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
margin-top: 20rpx;
|
||||
|
||||
&:active {
|
||||
background: #0056b3;
|
||||
}
|
||||
}
|
||||
|
||||
.form-list-data {
|
||||
margin-top: 30rpx;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
border: 1px solid #e5e8ef;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,44 +1,169 @@
|
||||
<template>
|
||||
<view class="form-wrapper">
|
||||
<view class="form-header">
|
||||
<image class="form-icon" src="./images/icon_minus.png"></image>
|
||||
<text class="form-title">游客1</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">姓 名</text>
|
||||
<input class="form-input" v-model="name" placeholder="请输入姓名" />
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">手机号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
v-model="phone"
|
||||
placeholder="请输入手机号"
|
||||
@blur="validatePhone"
|
||||
<uni-icons class="minus" color="#00A6FF" size="22" type="minus" />
|
||||
<text class="form-title">{{ title }}</text>
|
||||
<uni-icons
|
||||
v-if="showDeleteIcon"
|
||||
class="delete"
|
||||
color="#00A6FF"
|
||||
size="22"
|
||||
type="trash"
|
||||
@click="handleDelete"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-item-wrapper">
|
||||
<view class="form-item">
|
||||
<view class="form-item-row">
|
||||
<text class="form-label">姓 名</text>
|
||||
<input
|
||||
class="form-input"
|
||||
:class="{ 'form-input-error': nameError }"
|
||||
v-model="nameValue"
|
||||
placeholder="请输入姓名"
|
||||
@blur="validateName"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="nameError" class="form-error">{{ nameError }}</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<view class="form-item-row">
|
||||
<text class="form-label">手机号</text>
|
||||
<input
|
||||
class="form-input"
|
||||
:class="{ 'form-input-error': phoneError }"
|
||||
v-model="phoneValue"
|
||||
placeholder="请输入手机号"
|
||||
type="tel"
|
||||
maxlength="11"
|
||||
@blur="validatePhone"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="phoneError" class="form-error">{{ phoneError }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// Local state
|
||||
const name = ref("");
|
||||
const phone = ref("");
|
||||
// 常量定义
|
||||
const PHONE_REGEX = /^1[3-9]\d{9}$/;
|
||||
const ERROR_MESSAGES = {
|
||||
NAME_REQUIRED: "请输入姓名",
|
||||
PHONE_REQUIRED: "手机号不能为空",
|
||||
PHONE_INVALID: "请输入正确的手机号",
|
||||
};
|
||||
|
||||
/**
|
||||
* FormCard 组件 Props
|
||||
* @typedef {Object} FormCardProps
|
||||
* @property {string} title - 表单标题
|
||||
* @property {Object} form - 表单数据对象
|
||||
* @property {string} form.name - 姓名
|
||||
* @property {string} form.phone - 手机号
|
||||
* @property {boolean} showDeleteIcon - 是否显示删除图标
|
||||
*/
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "游客1",
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
name: "",
|
||||
phone: "",
|
||||
}),
|
||||
validator: (value) => {
|
||||
return value && typeof value === 'object' &&
|
||||
'name' in value && 'phone' in value;
|
||||
},
|
||||
},
|
||||
showDeleteIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* FormCard 组件事件
|
||||
* @typedef {Object} FormCardEmits
|
||||
* @property {Function} update:name - 更新姓名事件
|
||||
* @property {Function} update:phone - 更新手机号事件
|
||||
* @property {Function} delete - 删除表单事件
|
||||
*/
|
||||
const emit = defineEmits(["update:name", "update:phone", "delete"]);
|
||||
|
||||
// 响应式状态
|
||||
const nameError = ref("");
|
||||
const phoneError = ref("");
|
||||
|
||||
// Methods
|
||||
const validatePhone = () => {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/;
|
||||
if (!phone.value) {
|
||||
phoneError.value = "手机号不能为空";
|
||||
} else if (!phoneRegex.test(phone.value)) {
|
||||
phoneError.value = "请输入正确的手机号";
|
||||
} else {
|
||||
phoneError.value = "";
|
||||
// 计算属性 - 双向绑定
|
||||
const nameValue = computed({
|
||||
get: () => props.form?.name || "",
|
||||
set: (value) => emit("update:name", value?.trim() || ""),
|
||||
});
|
||||
|
||||
const phoneValue = computed({
|
||||
get: () => props.form?.phone || "",
|
||||
set: (value) => {
|
||||
// 只允许数字输入
|
||||
const numericValue = value.replace(/\D/g, "");
|
||||
emit("update:phone", numericValue);
|
||||
},
|
||||
});
|
||||
|
||||
// 工具函数
|
||||
/**
|
||||
* 验证姓名是否有效
|
||||
* @param {string} name - 姓名
|
||||
* @returns {string} 错误信息,空字符串表示验证通过
|
||||
*/
|
||||
const getNameError = (name) => {
|
||||
if (!name || name.trim() === "") {
|
||||
return ERROR_MESSAGES.NAME_REQUIRED;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* 验证手机号是否有效
|
||||
* @param {string} phone - 手机号
|
||||
* @returns {string} 错误信息,空字符串表示验证通过
|
||||
*/
|
||||
const getPhoneError = (phone) => {
|
||||
if (!phone) {
|
||||
return ERROR_MESSAGES.PHONE_REQUIRED;
|
||||
}
|
||||
if (!PHONE_REGEX.test(phone)) {
|
||||
return ERROR_MESSAGES.PHONE_INVALID;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 验证方法
|
||||
const validateName = () => {
|
||||
nameError.value = getNameError(props.form?.name);
|
||||
};
|
||||
|
||||
const validatePhone = () => {
|
||||
phoneError.value = getPhoneError(props.form?.phone);
|
||||
};
|
||||
|
||||
// 事件处理
|
||||
const handleDelete = () => {
|
||||
emit("delete");
|
||||
};
|
||||
|
||||
// 暴露给模板的方法(用于测试或外部调用)
|
||||
defineExpose({
|
||||
validateName,
|
||||
validatePhone,
|
||||
getNameError,
|
||||
getPhoneError,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,46 +1,146 @@
|
||||
// SASS 变量定义
|
||||
$form-primary-color: #00a6ff;
|
||||
$form-error-color: #ff4d4f;
|
||||
$form-text-color: #333;
|
||||
$form-label-color: #86909c;
|
||||
$form-border-color: #ddd;
|
||||
$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;
|
||||
|
||||
.form-wrapper {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
font-size: 0;
|
||||
border-radius: $form-border-radius;
|
||||
overflow: hidden;
|
||||
width: 280px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
background-color: rgba(25, 144, 255, 0.06);
|
||||
border: 1px solid #e5e8ef;
|
||||
border-radius: 8px 8px 0 0;
|
||||
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;
|
||||
|
||||
.form-icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-right: 8px;
|
||||
.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: 16px;
|
||||
color: #00a6ff;
|
||||
font-weight: 500;
|
||||
color: $form-primary-color;
|
||||
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;
|
||||
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;
|
||||
padding: 12px 24px 12px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 16px;
|
||||
color: #86909c;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: $form-label-color;
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding-bottom: 6px;
|
||||
font-size: 14px;
|
||||
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: $form-primary-color;
|
||||
|
||||
&::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.form-input-error {
|
||||
border-bottom-color: $form-error-color;
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: $form-error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 12px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
## 功能特性
|
||||
|
||||
- 🎨 **可配置圆角**:支持数字(px)或字符串形式的圆角设置
|
||||
- 📏 **可配置高度**:支持数字(px)或字符串形式的高度设置
|
||||
- 🖼️ **缩略图导航**:底部缩略图快速切换,支持左右滑动
|
||||
- 👁️ **缩略图控制**:可配置显示或隐藏缩略图
|
||||
- 📱 **响应式设计**:适配不同屏幕尺寸
|
||||
- 🎯 **自定义数据**:支持传入自定义图片数据
|
||||
- 📊 **进度指示器**:显示当前图片位置
|
||||
@@ -42,6 +44,37 @@ import ImageSwiper from '@/components/ImageSwiper/index.vue'
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义高度
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 数字形式 (px) -->
|
||||
<ImageSwiper :height="300" />
|
||||
|
||||
<!-- 字符串形式 -->
|
||||
<ImageSwiper height="50vh" />
|
||||
|
||||
<!-- 小高度轮播图 -->
|
||||
<ImageSwiper :height="120" />
|
||||
</template>
|
||||
```
|
||||
|
||||
### 隐藏缩略图
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 隐藏缩略图,只显示主轮播图 -->
|
||||
<ImageSwiper :show-thumbnails="false" />
|
||||
|
||||
<!-- 结合其他属性使用 -->
|
||||
<ImageSwiper
|
||||
:height="250"
|
||||
:border-radius="15"
|
||||
:show-thumbnails="false"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 自定义图片数据
|
||||
|
||||
```vue
|
||||
@@ -97,6 +130,8 @@ const manyImages = ref([
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| borderRadius | Number \| String | 8 | 轮播图圆角大小,数字时单位为px |
|
||||
| height | Number \| String | 200 | 轮播图高度,数字时单位为px |
|
||||
| showThumbnails | Boolean | true | 是否显示缩略图 |
|
||||
| images | Array | [] | 图片数据数组,为空时使用默认数据 |
|
||||
|
||||
### images 数组结构
|
||||
@@ -157,21 +192,79 @@ const handleChange = (e) => {
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 响应式圆角
|
||||
### 响应式配置
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<ImageSwiper :border-radius="responsiveRadius" />
|
||||
<ImageSwiper
|
||||
:border-radius="responsiveRadius"
|
||||
:height="responsiveHeight"
|
||||
:show-thumbnails="showThumbnails"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// 根据屏幕宽度动态调整圆角
|
||||
// 根据屏幕宽度动态调整圆角和高度
|
||||
const responsiveRadius = computed(() => {
|
||||
const screenWidth = uni.getSystemInfoSync().screenWidth
|
||||
return screenWidth > 750 ? 16 : 8
|
||||
})
|
||||
|
||||
const responsiveHeight = computed(() => {
|
||||
const screenWidth = uni.getSystemInfoSync().screenWidth
|
||||
return screenWidth > 750 ? 300 : 200
|
||||
})
|
||||
|
||||
// 动态控制缩略图显示
|
||||
const showThumbnails = ref(true)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 动态控制示例
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<!-- 控制面板 -->
|
||||
<view class="control-panel">
|
||||
<text>高度: {{ dynamicHeight }}px</text>
|
||||
<slider
|
||||
:value="dynamicHeight"
|
||||
:min="100"
|
||||
:max="400"
|
||||
@change="handleHeightChange"
|
||||
/>
|
||||
|
||||
<view class="checkbox-wrapper">
|
||||
<checkbox :checked="showThumbnails" @change="handleThumbnailToggle" />
|
||||
<text>显示缩略图</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 轮播图组件 -->
|
||||
<ImageSwiper
|
||||
:height="dynamicHeight"
|
||||
:show-thumbnails="showThumbnails"
|
||||
:border-radius="10"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const dynamicHeight = ref(200)
|
||||
const showThumbnails = ref(true)
|
||||
|
||||
const handleHeightChange = (e) => {
|
||||
dynamicHeight.value = e.detail.value
|
||||
}
|
||||
|
||||
const handleThumbnailToggle = (e) => {
|
||||
showThumbnails.value = e.detail.value
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -196,12 +289,22 @@ const themeRadius = computed(() => {
|
||||
## 注意事项
|
||||
|
||||
1. **圆角单位**:数字类型自动添加px单位,字符串类型直接使用
|
||||
2. **图片比例**:建议使用相同比例的图片以获得最佳显示效果
|
||||
3. **性能优化**:大量图片时建议使用懒加载
|
||||
4. **兼容性**:支持微信小程序、H5、App等平台
|
||||
2. **高度单位**:数字类型自动添加px单位,字符串类型直接使用(支持vh、rem等)
|
||||
3. **缩略图显示**:当设置 `showThumbnails` 为 `false` 时,缩略图完全隐藏
|
||||
4. **图片比例**:建议使用相同比例的图片以获得最佳显示效果
|
||||
5. **性能优化**:大量图片时建议使用懒加载
|
||||
6. **兼容性**:支持微信小程序、H5、App等平台
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.3.0
|
||||
- ✨ 新增 `height` 属性,支持自定义轮播图高度
|
||||
- ✨ 新增 `showThumbnails` 属性,支持隐藏缩略图
|
||||
- 🎨 优化样式系统,移除硬编码高度
|
||||
- 🔧 改进计算属性,支持动态高度和缩略图控制
|
||||
- 📝 更新文档和演示示例,新增多个高级用法示例
|
||||
- 🎯 增强组件灵活性,适应更多使用场景
|
||||
|
||||
### v1.2.0
|
||||
- ✨ 新增缩略图左右滑动功能
|
||||
- ✨ 新增缩略图选中状态高亮显示
|
||||
|
||||
@@ -53,6 +53,54 @@
|
||||
</view>
|
||||
<ImageSwiper :border-radius="dynamicRadius" />
|
||||
</view>
|
||||
|
||||
<!-- 示例8: 自定义高度 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例8: 自定义高度 (300px)</view>
|
||||
<ImageSwiper :height="300" :border-radius="12" />
|
||||
</view>
|
||||
|
||||
<!-- 示例9: 小高度轮播图 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例9: 小高度轮播图 (120px)</view>
|
||||
<ImageSwiper :height="120" :border-radius="8" />
|
||||
</view>
|
||||
|
||||
<!-- 示例10: 隐藏缩略图 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例10: 隐藏缩略图</view>
|
||||
<ImageSwiper :show-thumbnails="false" :border-radius="15" />
|
||||
</view>
|
||||
|
||||
<!-- 示例11: 动态高度和缩略图控制 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例11: 动态高度和缩略图控制</view>
|
||||
<view class="control-panel">
|
||||
<text>高度: {{ dynamicHeight }}px</text>
|
||||
<slider
|
||||
:value="dynamicHeight"
|
||||
:min="100"
|
||||
:max="400"
|
||||
@change="handleHeightChange"
|
||||
activeColor="#007AFF"
|
||||
/>
|
||||
<view class="checkbox-wrapper">
|
||||
<checkbox :checked="showThumbnails" @change="handleThumbnailToggle" />
|
||||
<text class="checkbox-label">显示缩略图</text>
|
||||
</view>
|
||||
</view>
|
||||
<ImageSwiper
|
||||
:height="dynamicHeight"
|
||||
:show-thumbnails="showThumbnails"
|
||||
:border-radius="10"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 示例12: 字符串高度 -->
|
||||
<view class="demo-section">
|
||||
<view class="section-title">示例12: 字符串高度 (50vh)</view>
|
||||
<ImageSwiper height="50vh" :border-radius="20" :show-thumbnails="false" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -63,6 +111,12 @@ import ImageSwiper from './index.vue'
|
||||
// 动态圆角控制
|
||||
const dynamicRadius = ref(8)
|
||||
|
||||
// 动态高度控制
|
||||
const dynamicHeight = ref(200)
|
||||
|
||||
// 缩略图显示控制
|
||||
const showThumbnails = ref(true)
|
||||
|
||||
// 自定义图片数据
|
||||
const customImages = ref([
|
||||
{
|
||||
@@ -97,6 +151,16 @@ const manyImages = ref([
|
||||
const handleRadiusChange = (e) => {
|
||||
dynamicRadius.value = e.detail.value
|
||||
}
|
||||
|
||||
// 处理高度变化
|
||||
const handleHeightChange = (e) => {
|
||||
dynamicHeight.value = e.detail.value
|
||||
}
|
||||
|
||||
// 处理缩略图显示切换
|
||||
const handleThumbnailToggle = (e) => {
|
||||
showThumbnails.value = e.detail.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -148,4 +212,16 @@ const handleRadiusChange = (e) => {
|
||||
slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
margin-left: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<view class="image-swiper">
|
||||
<swiper
|
||||
class="swiper-box"
|
||||
:style="borderRadiusStyle"
|
||||
:style="swiperStyle"
|
||||
:autoplay="false"
|
||||
:interval="3000"
|
||||
:duration="1000"
|
||||
@@ -23,7 +23,7 @@
|
||||
</view>
|
||||
|
||||
<!-- 缩略图部分 -->
|
||||
<view class="thumbnail-box">
|
||||
<view v-if="showThumbnails" class="thumbnail-box">
|
||||
<scroll-view
|
||||
class="thumbnail-scroll"
|
||||
scroll-x="true"
|
||||
@@ -63,6 +63,16 @@ const props = defineProps({
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 轮播图高度,支持数字(px)或字符串
|
||||
height: {
|
||||
type: [Number, String],
|
||||
default: 200,
|
||||
},
|
||||
// 是否显示缩略图
|
||||
showThumbnails: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const active = ref(0);
|
||||
@@ -79,6 +89,20 @@ const borderRadiusStyle = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
// 计算轮播图样式(包含高度和圆角)
|
||||
const swiperStyle = computed(() => {
|
||||
const radius =
|
||||
typeof props.borderRadius === "number"
|
||||
? `${props.borderRadius}px`
|
||||
: props.borderRadius;
|
||||
const swiperHeight =
|
||||
typeof props.height === "number" ? `${props.height}px` : props.height;
|
||||
return {
|
||||
borderRadius: radius,
|
||||
height: swiperHeight,
|
||||
};
|
||||
});
|
||||
|
||||
// 默认图片数据
|
||||
const defaultImages = [
|
||||
{
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
}
|
||||
|
||||
.swiper-box {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
// 圆角通过内联样式动态设置
|
||||
// 高度和圆角通过内联样式动态设置
|
||||
}
|
||||
|
||||
.swiper-item image {
|
||||
|
||||
72
components/Stepper/README.md
Normal file
72
components/Stepper/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Stepper 数字步进器组件
|
||||
|
||||
一个简洁易用的数字步进器组件,支持增减操作和数值范围限制。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✨ **双向数据绑定**:支持 v-model 语法糖
|
||||
- 🔢 **数值范围控制**:可设置最小值和最大值
|
||||
- 🎯 **响应式更新**:实时响应外部数值变化
|
||||
- 🎨 **简洁UI设计**:使用 uni-icons 图标,界面清爽
|
||||
- 📱 **移动端适配**:完美适配各种屏幕尺寸
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Stepper v-model="quantity" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Stepper from '@/components/Stepper/index.vue'
|
||||
|
||||
const quantity = ref(1)
|
||||
</script>
|
||||
```
|
||||
|
||||
### 设置数值范围
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<Stepper
|
||||
v-model="quantity"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 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设计
|
||||
- 📱 移动端适配
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,23 +1,13 @@
|
||||
<template>
|
||||
<view class="stepper-wrapper">
|
||||
<image
|
||||
class="stepper-btn stepper-btn-minus"
|
||||
src="./images/icon_minus.png"
|
||||
mode="aspectFill"
|
||||
@click="decrease"
|
||||
></image>
|
||||
<uni-icons type="minus" size="24" color="#999" @click="decrease" />
|
||||
<text class="stepper-text">{{ value }}</text>
|
||||
<image
|
||||
class="stepper-btn stepper-btn-plus"
|
||||
src="./images/icon_plus.png"
|
||||
mode="aspectFill"
|
||||
@click="increase"
|
||||
></image>
|
||||
<uni-icons type="plus" size="24" color="#999" @click="increase" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, defineEmits } from "vue";
|
||||
import { ref, defineProps, defineEmits, watch } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -41,6 +31,15 @@ const emit = defineEmits(["update:modelValue"]);
|
||||
// Local state
|
||||
const value = ref(props.modelValue);
|
||||
|
||||
// 监听外部值变化,同步更新本地状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
value.value = newValue;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Methods
|
||||
const decrease = () => {
|
||||
if (value.value === 1) return;
|
||||
|
||||
@@ -3,22 +3,9 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stepper-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stepper-btn-minus {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.stepper-btn-plus {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.stepper-text {
|
||||
width: 40px;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
.sum-wrapper {
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
padding: 0 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sum-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 13px 15px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sum-label {
|
||||
|
||||
Reference in New Issue
Block a user