添加地址功能:手机号需要添加正则验证

This commit is contained in:
cbb
2025-12-25 17:50:11 +08:00
parent 334c0800fa
commit 1aab94bbc3
91 changed files with 13903 additions and 24 deletions

167
pages/address/add.vue Normal file
View File

@@ -0,0 +1,167 @@
<script setup>
import { reactive, ref } from 'vue'
const formRef = ref(null)
const formData = reactive({
name: '',
phone: '',
address: '',
/** 省市区选择 */
selectAddress: '',
/** 是否默认地址 */
defaultAddress: false
})
const rules = {
name: {
rules: [
{
required: true,
errorMessage: '收件人不能为空'
}
]
},
phone: {
rules: [
{
required: true,
errorMessage: '手机号不能为空'
},
{
format: 'number',
errorMessage: '手机号只能输入数字'
}
]
},
selectAddress: {
rules: [
{
required: true,
errorMessage: '所在地区不能为空'
}
]
}
}
const submitForm = () => {
formRef.value.validate().then(res => {
console.log(res, '校验通过')
})
}
</script>
<template>
<view class="address-add">
<view class="address-box">
<view class="top-name">
<text class="left-name">地址信息</text>
<view
class="right-box"
@click="formData.defaultAddress = !formData.defaultAddress"
>
<view class="check">
<image
v-show="formData.defaultAddress"
src="/static/images/public/check-to-confirm.png"
mode="aspectFit"
class="check-icon"
></image>
</view>
<text>默认收货地址</text>
</view>
</view>
<!-- 表单 -->
<uni-forms
ref="formRef"
:rules="rules"
:modelValue="formData"
label-width="150rpx"
>
<uni-forms-item label="收件人" required name="name">
<uni-easyinput
v-model="formData.name"
placeholder="请输入收件人"
/>
</uni-forms-item>
<uni-forms-item label="手机号" required name="phone">
<uni-easyinput
v-model="formData.phone"
placeholder="请输入手机号"
/>
</uni-forms-item>
<uni-forms-item label="所在地区" required name="selectAddress">
<uni-easyinput
v-model="formData.selectAddress"
placeholder="请选择地区"
/>
</uni-forms-item>
<uni-forms-item label="详细地址" name="address">
<uni-easyinput
type="textarea"
v-model="formData.address"
placeholder="请输入详细地址"
/>
</uni-forms-item>
</uni-forms>
</view>
<!-- 底部按钮 -->
<bottom-view>
<cb-button @click="submitForm">确认添加</cb-button>
</bottom-view>
</view>
</template>
<style lang="scss" scoped>
page {
background: #f9f9f9;
}
.address-add {
padding: 34rpx 32rpx;
.address-box {
padding: 34rpx 32rpx;
border-radius: 32rpx;
background: #ffffff;
.top-name {
margin-bottom: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-family: PingFang SC, PingFang SC;
text-align: left;
font-style: normal;
text-transform: none;
.left-name {
font-weight: bold;
font-size: 28rpx;
color: #333333;
}
}
.right-box {
display: flex;
align-items: center;
.check {
width: 30rpx;
height: 30rpx;
border-radius: 28rpx;
border: 2rpx solid #d9d9d9;
display: flex;
justify-content: center;
align-items: center;
margin-right: 10rpx;
.check-icon {
width: 34rpx;
height: 34rpx;
}
}
text {
font-weight: 500;
font-size: 26rpx;
color: #333333;
}
}
}
}
</style>

7
pages/address/edit.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup></script>
<template>
<view class="address-edit">修改地址</view>
</template>
<style lang="scss" scoped></style>

7
pages/address/index.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup></script>
<template>
<view class="address-index">地址列表</view>
</template>
<style lang="scss" scoped></style>

View File

@@ -1,7 +1,186 @@
<script setup></script>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { getProductDetail } from '@/api/mall'
import { reactive, ref } from 'vue'
import { formatRMB } from '@/utils'
import { getUserAddress } from '@/api'
import { navigateTo } from '@/utils/router'
const viewData = ref({})
/** 单价 */
const priceData = ref(0)
const formData = reactive({
/** 数量 */
num: 1,
/** 规格 */
spec: 0,
/** 默认支付方式 */
payWay: 1,
/** 合计 */
total: 0,
/** 默认地址 */
address: '',
/** 可选数量 */
maxNum: 1
})
/** 获取用户地址 */
const userRess = async () => {
const res = await getUserAddress()
console.log(res)
}
/** 用户地址跳转 */
const onRess = () => {
navigateTo('/pages/address/add')
}
const getData = async productId => {
const res = await getProductDetail(productId)
viewData.value = res.data
const { id, price, stockQuantity } = res.data.skuList.find(
v => v.isDefault == 1
)
formData.maxNum = stockQuantity
formData.spec = id
priceData.value = price
formData.total = price
}
/** 数量切换 */
const onChange = i => {
formData.total = priceData.value * i
}
/** 规格选择 */
const onSpecChange = item => {
formData.spec = item.id
formData.num = 1
formData.total = item.price
priceData.value = item.price
formData.maxNum = item.stockQuantity
}
// 提交订单
const onConfirm = () => {}
onLoad(async e => {
await userRess()
await getData(e.productId)
})
</script>
<template>
<view class="mall-confirm-order">确认订单</view>
<view class="mall-confirm-order">
<!-- 地址 -->
<view class="address-box" @click="onRess">
<view class="left-name">
<text class="adres">重庆沙坪坝龙湖光年4号楼3009</text>
<view class="bottom-name">
<text>名字</text>
<text>137******</text>
</view>
</view>
<!-- <text class="wu-adres">
暂无收货地址(点击添加)
</text> -->
<image
src="/static/images/public/right-arrow.png"
mode="heightFix"
class="right-box"
></image>
</view>
<!-- 商品展示 -->
<view class="product-box">
<image
:src="viewData.mainImage"
mode="scaleToFill"
class="left-img"
></image>
<view class="right-content">
<text class="product-name">
{{ viewData.productName }}
</text>
<view class="line-box">
<view class="rmb-box">
<text>¥{{ viewData.maxPrice }}</text>
<text>¥{{ viewData.minPrice }}</text>
</view>
<!-- 添加数量 -->
<view class="add-num">
<uni-number-box
v-model="formData.num"
:min="1"
:max="formData.maxNum"
@change="onChange"
></uni-number-box>
</view>
</view>
</view>
</view>
<!-- 规格 -->
<view class="spec-box">
<text class="title">规格</text>
<view class="spec-item">
<text
v-for="(item, index) in viewData.skuList"
:key="index"
:class="{
'on-text': formData.spec === item.id,
disabled: item.stockQuantity <= 0
}"
@click="item.stockQuantity > 0 && onSpecChange(item)"
>
{{ item.specText }}*{{ item.stockQuantity }}
</text>
</view>
</view>
<!-- 合计 -->
<view class="total-box">
<text class="name">合计</text>
<view class="num">
<text>¥</text>
<text>{{ formatRMB(formData.total) }}</text>
</view>
</view>
<!-- 付款方式 -->
<view class="pay-way">
<view class="pay-way-item" @click="formData.payWay = 1">
<view class="icon">
<image
src="/static/images/public/integral.png"
mode="aspectFit"
class="left-icon"
></image>
<text>积分</text>
</view>
<view class="check">
<image
v-show="formData.payWay == 1"
src="/static/images/public/check-to-confirm.png"
mode="aspectFit"
class="check-icon"
></image>
</view>
</view>
</view>
<!-- 底部按钮 -->
<bottom-view>
<cb-button @click="onConfirm">确认支付</cb-button>
</bottom-view>
</view>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
page {
background: #f9f9f9;
}
@import './styles/confirm-order.scss';
</style>

View File

@@ -1,7 +1,250 @@
<script setup></script>
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { getProductDetail } from '@/api/mall'
import { ref, computed } from 'vue'
import { navigateTo } from '@/utils/router'
const viewData = ref({})
const productId = ref('')
const getData = async productId => {
const res = await getProductDetail(productId)
viewData.value = res.data
}
/** 拼单人数 */
const getPeople = computed(() => {
if (!viewData.value.groupActivities?.length) return 0
return viewData.value.groupActivities[0].totalPeople
})
/**
* 顶部导航按钮点击事件
* @param index 0 返回 1 分享
*/
const onTopNav = index => {
console.log(index)
}
const onConfirm = () => {
navigateTo('/pages/mall/confirm-order', {
productId: productId.value
})
}
onLoad(e => {
productId.value = e.productId
getData(e.productId)
})
</script>
<template>
<view class="mall-detail">商品详情</view>
<view class="mall-detail">
<nav-bar>
<template #back>
<image
src="/static/images/public/return-icon.png"
mode="heightFix"
class="left-icon"
></image>
</template>
<template #right>
<image
src="/static/images/public/share-icon.png"
mode="heightFix"
class="right-icon"
></image>
</template>
</nav-bar>
<!-- 顶部图片 -->
<view class="top-img">
<image
:src="viewData.mainImage"
mode="scaleToFill"
class="img"
></image>
</view>
<!-- 商品详情 -->
<view class="detail-box">
<text class="title">{{ viewData.productName }}</text>
<view class="price">
<text>¥</text>
<text>{{ viewData.minPrice }}</text>
</view>
<view class="name-box">
<text>拼单数量:12505</text>
<text>好评率:99%</text>
</view>
<!-- 拼单量 -->
<view class="line-box">
<view class="left-img">
<text>拼单</text>
<image
src="/static/images/public/random1.png"
mode="scaleToFill"
class="avatar"
></image>
<image
src="/static/images/public/random2.png"
mode="scaleToFill"
class="avatar"
></image>
<image
src="/static/images/public/random3.png"
mode="scaleToFill"
class="avatar"
></image>
</view>
<text class="right-name">还需{{ getPeople }}人拼单</text>
</view>
<!-- 去拼团 -->
<!-- <view class="bottom-name">
<view class="count-down">
<text>拼单倒计时:</text>
<text>23:53:00</text>
</view>
<button>去拼单</button>
</view>-->
<!-- 商品详情 -->
<view class="detail-content">
<text class="title">商品详情</text>
<mp-html :content="viewData.description" class="rich-box" />
</view>
</view>
<!-- 底部按钮 -->
<bottom-view>
<cb-button @click="onConfirm">拼单购买</cb-button>
</bottom-view>
</view>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.left-icon,
.right-icon {
height: 64rpx;
}
.top-img {
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 56rpx;
background: #fff;
border-radius: 32rpx 32rpx 0 0;
}
.img {
width: 100%;
height: 628rpx;
}
}
.detail-box {
padding: 0 58rpx 150rpx;
font-family: PingFang SC, PingFang SC;
font-style: normal;
text-transform: none;
.title {
font-weight: bold;
font-size: 32rpx;
color: #333333;
}
.price {
display: flex;
align-items: baseline;
text {
font-weight: 500;
font-size: 24rpx;
color: #eb1c26;
&:last-child {
font-weight: bold;
font-size: 48rpx;
margin: 16rpx 0 16rpx 8rpx;
}
}
}
.name-box {
text {
font-weight: 500;
font-size: 24rpx;
color: #999999;
&:last-child {
margin-left: 66rpx;
}
}
}
.line-box {
margin: 48rpx 0;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 500;
font-size: 28rpx;
.left-img {
display: flex;
align-items: center;
.avatar {
width: 64rpx;
height: 64rpx;
border-radius: 64rpx;
margin-left: 16rpx;
}
text {
color: #333333;
}
}
.right-name {
color: #999999;
}
}
.bottom-name {
display: flex;
justify-content: flex-end;
font-weight: 500;
.count-down {
display: flex;
flex-direction: column;
text {
font-size: 28rpx;
color: #eb1c26;
&:last-child {
color: #999999;
}
}
}
button {
margin: 0;
width: 252rpx;
height: 64rpx;
border-radius: 64rpx;
line-height: 64rpx;
font-size: 28rpx;
color: #00d993;
border: 2rpx solid #00d993;
background: #ffffff;
&::after {
border: none;
}
}
}
.detail-content {
border-top: 2rpx solid #f9f9f9;
padding-top: 46rpx;
margin-top: 46rpx;
.title {
margin-bottom: 16rpx;
font-weight: 500;
font-size: 28rpx;
color: #333333;
}
.rich-box {
margin-top: 16rpx;
width: 100%;
}
}
}
</style>

View File

@@ -1,27 +1,40 @@
<script setup>
import { reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getCategory } from '@/api/mall'
import { getCategory, getProductList } from '@/api/mall'
import { navigateTo } from '@/utils/router'
// 顶部分类选项
/** 顶部分类选项 */
const topNavOptions = ref([])
const formData = reactive({
name: '',
type: '0'
})
/** 商品列表 */
const cardList = ref([])
const categoryList = async () => {
const res = await getCategory()
topNavOptions.value = res.data
console.log(res.data, '===22==')
}
const getListData = async () => {
const res = await getProductList()
cardList.value = res.rows
console.log(res.rows)
}
const onTop = value => {
formData.type = value
}
onLoad(() => {
categoryList()
const onGo = item => {
navigateTo('/pages/mall/detail', { productId: item.id })
}
onLoad(async () => {
await categoryList()
await getListData()
})
</script>
@@ -31,8 +44,8 @@
<cb-search v-model="formData.name"></cb-search>
<view class="top-options">
<view
v-for="item in topNavOptions"
:key="item.value"
v-for="(item, index) in topNavOptions"
:key="index"
:class="{ active: item.id === formData.type }"
class="text"
@click="onTop(item.id)"
@@ -44,22 +57,27 @@
<!-- 商品卡片 -->
<view class="card-list">
<view v-for="item in 3" :key="item" class="card-item">
<view
v-for="item in cardList"
:key="item.id"
class="card-item"
@click="onGo(item)"
>
<image
src="https://wx1.sinaimg.cn/mw690/92eeb099gy1i29hl0ne80j21jk2bcash.jpg"
:src="item.mainImage"
mode="scaleToFill"
class="imghead"
></image>
<text class="title">名称</text>
<text class="title">{{ item.productName }}</text>
<view class="price">
<view class="num-box">
<text class="num"></text>
<text class="num">0.00</text>
<text class="num">{{ item.minPrice }}</text>
</view>
<text class="buy">好评率99%</text>
<!-- <text class="buy">好评率99%</text> -->
</view>
<!-- 拼单数量 -->
<text class="bottom-name">拼单数量:12505</text>
<text class="bottom-name">拼单数量:{{ item.salesCount }}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,236 @@
.mall-confirm-order {
padding: 32rpx 24rpx;
// 地址
.address-box {
padding: 34rpx 32rpx;
border-radius: 32rpx;
background: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
.wu-adres {
font-family: PingFang SC, PingFang SC;
font-style: normal;
text-transform: none;
font-weight: 500;
font-size: 28rpx;
color: #333333;
}
.left-name {
display: flex;
flex-direction: column;
font-family: PingFang SC, PingFang SC;
font-style: normal;
text-transform: none;
font-weight: 500;
.adres {
font-size: 28rpx;
color: #666666;
}
.bottom-name {
display: flex;
margin-top: 32rpx;
text {
font-size: 24rpx;
color: #999999;
&:first-child {
margin-right: 16rpx;
padding-right: 16rpx;
border-right: 2rpx solid #999999;
}
}
}
}
.right-box {
height: 32rpx;
}
}
// 商品展示
.product-box {
margin: 16rpx 0;
padding: 32rpx;
border-radius: 32rpx;
background: #ffffff;
display: flex;
align-items: center;
.left-img {
width: 192rpx;
height: 192rpx;
border-radius: 8rpx;
flex-shrink: 0;
margin-right: 32rpx;
}
.right-content {
width: 100%;
height: 192rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
font-family: PingFang SC, PingFang SC;
font-style: normal;
text-transform: none;
.product-name {
font-weight: bold;
font-size: 32rpx;
color: #333333;
display: -webkit-box;
display: box;
-webkit-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2; /* 添加标准属性 */
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
overflow-wrap: break-word;
}
.line-box {
display: flex;
justify-content: space-between;
align-items: flex-end;
.rmb-box {
display: flex;
flex-direction: column;
text {
font-weight: 500;
font-size: 32rpx;
color: #eb3c39;
// 第一个
&:first-child {
font-size: 24rpx;
color: #999999;
text-decoration-line: line-through;
}
}
}
.add-num {
display: flex;
align-items: center;
.uni-number-box {
width: 120rpx;
}
}
}
}
}
// 规格
.spec-box {
padding: 32rpx;
border-radius: 32rpx;
background: #ffffff;
font-family: PingFang SC, PingFang SC;
font-style: normal;
text-transform: none;
font-weight: 500;
.title {
font-size: 28rpx;
color: #333333;
}
.spec-item {
display: flex;
flex-wrap: wrap;
margin-top: 32rpx;
text {
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #333333;
background: #f4f4f4;
border-radius: 4rpx;
border: 2rpx solid #f4f4f4;
margin: 0 32rpx 16rpx 0;
}
.on-text {
background: #eb1c261a;
border-color: #eb1c26;
color: #eb1c26;
}
.disabled {
color: #999999;
}
}
}
// 合计
.total-box {
margin: 16rpx 0;
padding: 32rpx;
border-radius: 32rpx;
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-between;
font-family: PingFang SC, PingFang SC;
text-align: left;
font-style: normal;
text-transform: none;
.name {
font-weight: 500;
font-size: 28rpx;
color: #333333;
}
.num {
display: flex;
align-items: baseline;
text {
font-weight: 500;
font-size: 24rpx;
color: #eb1c26;
// 最后一个
&:last-child {
margin-left: 10rpx;
font-weight: bold;
font-size: 48rpx;
}
}
}
}
// 付款方式
.pay-way {
border-radius: 32rpx;
background: #ffffff;
margin-bottom: 110rpx;
.pay-way-item {
padding: 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
.icon {
display: flex;
align-items: center;
.left-icon {
width: 37rpx;
height: 37rpx;
margin-right: 14rpx;
}
text {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #333333;
text-align: left;
font-style: normal;
text-transform: none;
}
}
.check {
width: 36rpx;
height: 36rpx;
border-radius: 34rpx;
border: 2rpx solid #d9d9d9;
.check-icon {
width: 36rpx;
height: 36rpx;
}
}
}
}
}