小兔鮮兒-小程序uni-app
- 7.小兔鮮兒 - 用戶模塊
- 會員中心頁(我的)
- 靜態結構
- 參考代碼
- 會員設置頁
- 分包預下載
- 靜態結構
- 退出登錄
- 會員信息頁
- 靜態結構
- 獲取會員信息
- 渲染會員信息
- 更新會員頭像
- 更新表單信息
- 8.小兔鮮兒 - 地址模塊
- 準備工作
- 靜態結構
- 地址管理頁
- 地址表單頁
- 動態設置標題
- 新建地址頁
- 接口封裝
- 參考代碼
- 地址管理頁
- 接口調用
- 參考代碼
- 修改地址頁
- 數據回顯
- 更新地址
- 表單校驗
- 操作步驟
- 參考代碼
- 刪除地址
- 側滑組件用法
- 接口封裝
- 參考代碼
- 9.小兔鮮兒 - SKU 模塊
- 存貨單位(SKU)
- 插件市場
- 下載 SKU 插件
- 使用 SKU 插件
- 插件類型問題
- 類型聲明文件
- 核心業務
- 渲染商品規格
- 打開彈窗交互
- 渲染被選中的值
- 10.小兔鮮兒 - 購物車模塊
- 加入購物車
- 接口相關
- 參考代碼
- 購物車列表
- 靜態結構
- 登錄狀態
- 列表渲染
- 刪除購物車
- 接口相關
- 參考代碼
- 修改商品信息
- 接口相關
- 修改商品數量
- 修改商品選中/全選
- 底部結算信息
- 計算總錢數(總金額)
- 帶返回按鈕的購物車
- 11.小兔鮮兒 - 訂單模塊
- 填寫訂單頁
- 靜態結構
- 購物車結算
- 立即購買
- 頁面傳參
- 選擇收貨地址
- 提交訂單
- 訂單詳情頁
- 靜態結構
- 自定義導航欄交互
- 獲取訂單詳情
- 訂單狀態
- 再次購買
- 支付倒計時
- 訂單支付
- 微信支付說明
- 參考代碼
- 支付成功頁
- 模擬發貨
- 確認收貨
- 訂單物流
- 刪除訂單
- 取消訂單
- 訂單列表頁
- 靜態結構
- Tabs 滑動切換
- 獲取訂單列表
- 訂單列表渲染
- 分頁加載
- 訂單支付
- 12.小兔鮮兒 - 項目打包
- 微信小程序端
- 核心步驟
- 步驟圖示
- 條件編譯
- 條件編譯語法
- 打包為 H5 端
- 核心步驟
- 路由基礎路徑
- 打包為 APP 端
7.小兔鮮兒 - 用戶模塊
在用戶登錄/注冊成功后,展示會員信息,更新會員信息。
會員中心頁(我的)
主要實現兩部分業務:
- 渲染當前登錄會員的昵稱和頭像,從 Store 中獲取。
- 猜你喜歡分頁加載,可封裝成組合式函數實現復用邏輯。
靜態結構
會員中心頁,替換掉原本的練習代碼。
// src/pages/my/my.vue<script setup lang="ts">
// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()
// 訂單選項
const orderTypes = [{ type: 1, text: '待付款', icon: 'icon-currency' },{ type: 2, text: '待發貨', icon: 'icon-gift' },{ type: 3, text: '待收貨', icon: 'icon-check' },{ type: 4, text: '待評價', icon: 'icon-comment' },
]
</script><template><scroll-view class="viewport" scroll-y enable-back-to-top><!-- 個人資料 --><view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }"><!-- 情況1:已登錄 --><view class="overview" v-if="false"><navigator url="/pagesMember/profile/profile" hover-class="none"><imageclass="avatar"mode="aspectFill"src="https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/avatar_3.jpg"></image></navigator><view class="meta"><view class="nickname"> 黑馬程序員 </view><navigator class="extra" url="/pagesMember/profile/profile" hover-class="none"><text class="update">更新頭像昵稱</text></navigator></view></view><!-- 情況2:未登錄 --><view class="overview" v-else><navigator url="/pages/login/login" hover-class="none"><imageclass="avatar gray"mode="aspectFill"src="http://yjy-xiaotuxian-dev.oss-cn-beijing.aliyuncs.com/picture/2021-04-06/db628d42-88a7-46e7-abb8-659448c33081.png"></image></navigator><view class="meta"><navigator url="/pages/login/login" hover-class="none" class="nickname">未登錄</navigator><view class="extra"><text class="tips">點擊登錄賬號</text></view></view></view><navigator class="settings" url="/pagesMember/settings/settings" hover-class="none">設置</navigator></view><!-- 我的訂單 --><view class="orders"><view class="title">我的訂單<navigator class="navigator" url="/pagesOrder/list/list?type=0" hover-class="none">查看全部訂單<text class="icon-right"></text></navigator></view><view class="section"><!-- 訂單 --><navigatorv-for="item in orderTypes":key="item.type":class="item.icon":url="`/pagesOrder/list/list?type=${item.type}`"class="navigator"hover-class="none">{{ item.text }}</navigator><!-- 客服 --><button class="contact icon-handset" open-type="contact">售后</button></view></view><!-- 猜你喜歡 --><view class="guess"><XtxGuess ref="guessRef" /></view></scroll-view>
</template><style lang="scss">
page {height: 100%;overflow: hidden;background-color: #f7f7f8;
}.viewport {height: 100%;background-repeat: no-repeat;background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/center_bg.png);background-size: 100% auto;
}/* 用戶信息 */
.profile {margin-top: 20rpx;position: relative;.overview {display: flex;height: 120rpx;padding: 0 36rpx;color: #fff;}.avatar {width: 120rpx;height: 120rpx;border-radius: 50%;background-color: #eee;}.gray {filter: grayscale(100%);}.meta {display: flex;flex-direction: column;justify-content: center;align-items: flex-start;line-height: 30rpx;padding: 16rpx 0;margin-left: 20rpx;}.nickname {max-width: 350rpx;margin-bottom: 16rpx;font-size: 30rpx;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;}.extra {display: flex;font-size: 20rpx;}.tips {font-size: 22rpx;}.update {padding: 3rpx 10rpx 1rpx;color: rgba(255, 255, 255, 0.8);border: 1rpx solid rgba(255, 255, 255, 0.8);margin-right: 10rpx;border-radius: 30rpx;}.settings {position: absolute;bottom: 0;right: 40rpx;font-size: 30rpx;color: #fff;}
}/* 我的訂單 */
.orders {position: relative;z-index: 99;padding: 30rpx;margin: 50rpx 20rpx 0;background-color: #fff;border-radius: 10rpx;box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);.title {height: 40rpx;line-height: 40rpx;font-size: 28rpx;color: #1e1e1e;.navigator {font-size: 24rpx;color: #939393;float: right;}}.section {width: 100%;display: flex;justify-content: space-between;padding: 40rpx 20rpx 10rpx;.navigator,.contact {text-align: center;font-size: 24rpx;color: #333;&::before {display: block;font-size: 60rpx;color: #ff9545;}}.contact {padding: 0;margin: 0;border: 0;background-color: transparent;line-height: inherit;}}
}/* 猜你喜歡 */
.guess {background-color: #f7f7f8;margin-top: 20rpx;
}
</style>
參考代碼
封裝猜你喜歡組合式函數
// src/composables/index.tsimport type { XtxGuessInstance } from '@/types/components'
import { ref } from 'vue'/*** 猜你喜歡組合式函數*/
export const useGuessList = () => {// 獲取猜你喜歡組件實例const guessRef = ref<XtxGuessInstance>()// 滾動觸底事件const onScrolltolower = () => {guessRef.value?.getMore()}// 返回 ref 和事件處理函數return { guessRef, onScrolltolower }
}
會員中心頁(我的):渲染用戶頭像,昵稱,完善猜你喜歡分頁加載。
// src/pages/my/my.vue<script setup lang="ts">
import { useMemberStore } from '@/stores'// 獲取會員信息
const memberStore = useMemberStore() // [!code ++]// 猜你喜歡組合式函數
const { guessRef, onScrolltolower } = useGuessList() // [!code ++]
</script><template><scroll-view class="viewport" scroll-y enable-back-to-top @scrolltolower="onScrolltolower"><!-- 個人資料 --><view class="profile" :style="{ paddingTop: safeAreaInsets!.top + 'px' }"><!-- 情況1:已登錄 --><view class="overview" v-if="memberStore.profile"><navigator url="/pagesMember/profile/profile" hover-class="none"><image class="avatar" :src="memberStore.profile.avatar" mode="aspectFill"></image></navigator><view class="meta"><view class="nickname">{{ memberStore.profile.nickname || memberStore.profile.account }}</view><navigator class="extra" url="/pagesMember/profile/profile" hover-class="none"><text class="update">更新頭像昵稱</text></navigator></view></view><!-- 情況2:未登錄 --><view class="overview" v-else> ...省略 </view></view><!-- 猜你喜歡 --><view class="guess"><XtxGuess ref="guessRef" /></view></scroll-view>
</template>
會員設置頁
會員模塊的二級頁面,按模塊處理成分包頁面,有以下好處:
- 按模塊管理頁面,方便項目維護。
- 減少主包體積,用到的時候再加載分包,屬于性能優化解決方案。
::: tip 溫馨提示
通過 VS Code 插件 uni-create-view 可以快速新建分包頁面,自動配置分包路由。
:::
分包預下載
當用戶進入【我的】頁面時,由框架自動預下載【會員模塊】的分包,提升進入后續分包頁面時的啟動速度。
// src/pages.json{// ...省略// 分包加載規則"subPackages": [{// 子包的根目錄"root": "pagesMember",// 頁面路徑和窗口表現"pages": [{"path": "settings/settings","style": {"navigationBarTitleText": "設置"}}]}],// 分包預下載規則"preloadRule": {"pages/my/my": {"network": "all", // [!code ++]"packages": ["pagesMember"] // [!code ++]}}
}
靜態結構
設置頁:src/pagesMember/settings/settings.vue
<script setup lang="ts">
//
</script><template><view class="viewport"><!-- 列表1 --><view class="list" v-if="true"><navigator url="/pagesMember/address/address" hover-class="none" class="item arrow">我的收貨地址</navigator></view><!-- 列表2 --><view class="list"><button hover-class="none" class="item arrow" open-type="openSetting">授權管理</button><button hover-class="none" class="item arrow" open-type="feedback">問題反饋</button><button hover-class="none" class="item arrow" open-type="contact">聯系我們</button></view><!-- 列表3 --><view class="list"><navigator hover-class="none" class="item arrow" url=" ">關于小兔鮮兒</navigator></view><!-- 操作按鈕 --><view class="action"><view class="button">退出登錄</view></view></view>
</template><style lang="scss">
page {background-color: #f4f4f4;
}.viewport {padding: 20rpx;
}/* 列表 */
.list {padding: 0 20rpx;background-color: #fff;margin-bottom: 20rpx;border-radius: 10rpx;.item {line-height: 90rpx;padding-left: 10rpx;font-size: 30rpx;color: #333;border-top: 1rpx solid #ddd;position: relative;text-align: left;border-radius: 0;background-color: #fff;&::after {width: auto;height: auto;left: auto;border: none;}&:first-child {border: none;}&::after {right: 5rpx;}}.arrow::after {content: '\e6c2';position: absolute;top: 50%;color: #ccc;font-family: 'erabbit' !important;font-size: 32rpx;transform: translateY(-50%);}
}/* 操作按鈕 */
.action {text-align: center;line-height: 90rpx;margin-top: 40rpx;font-size: 32rpx;color: #333;.button {background-color: #fff;margin-bottom: 20rpx;border-radius: 10rpx;}
}
</style>
退出登錄
設置頁需實現以下業務:
- 退出登錄,清理用戶信息,返回上一頁。
- 根據登錄狀態,按需展示頁面內容。
參考效果
參考代碼
// src/pagesMember/settings/settings.vue<script setup lang="ts">
import { useMemberStore } from '@/stores'const memberStore = useMemberStore()
// 退出登錄
const onLogout = () => {// 模態彈窗uni.showModal({content: '是否退出登錄?',success: (res) => {if (res.confirm) {// 清理用戶信息memberStore.clearProfile()// 返回上一頁uni.navigateBack()}},})
}
</script><template><view class="viewport"><!-- 列表1 --><view class="list" v-if="memberStore.profile"><navigator url="./address/address" hover-class="none" class="item arrow">我的收貨地址</navigator></view><!-- 列表2 --><view class="list"><button hover-class="none" class="item arrow" open-type="openSetting">授權管理</button><button hover-class="none" class="item arrow" open-type="feedback">問題反饋</button><button hover-class="none" class="item arrow" open-type="contact">聯系我們</button></view><!-- 列表3 --><view class="list"><navigator hover-class="none" class="item arrow" url=" ">關于小兔鮮兒</navigator></view><!-- 操作按鈕 --><view class="action" v-if="memberStore.profile"><view @tap="onLogout" class="button">退出登錄</view></view></view>
</template>
會員信息頁
用戶可以對會員信息進行更新操作,涉及到表單數據提交、圖片讀取、文件上傳等知識點。
靜態結構
會員信息頁,處理成分包頁面:src/pagesMember/profile/profile.vue
<script setup lang="ts">
// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()
</script><template><view class="viewport"><!-- 導航欄 --><view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"><navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator><view class="title">個人信息</view></view><!-- 頭像 --><view class="avatar"><view class="avatar-content"><image class="image" src=" " mode="aspectFill" /><text class="text">點擊修改頭像</text></view></view><!-- 表單 --><view class="form"><!-- 表單內容 --><view class="form-content"><view class="form-item"><text class="label">賬號</text><text class="account">賬號名</text></view><view class="form-item"><text class="label">昵稱</text><input class="input" type="text" placeholder="請填寫昵稱" value="" /></view><view class="form-item"><text class="label">性別</text><radio-group><label class="radio"><radio value="男" color="#27ba9b" :checked="true" />男</label><label class="radio"><radio value="女" color="#27ba9b" :checked="false" />女</label></radio-group></view><view class="form-item"><text class="label">生日</text><pickerclass="picker"mode="date"start="1900-01-01":end="new Date()"value="2000-01-01"><view v-if="false">2000-01-01</view><view class="placeholder" v-else>請選擇日期</view></picker></view><view class="form-item"><text class="label">城市</text><picker class="picker" mode="region" :value="['廣東省', '廣州市', '天河區']"><view v-if="false">廣東省廣州市天河區</view><view class="placeholder" v-else>請選擇城市</view></picker></view><view class="form-item"><text class="label">職業</text><input class="input" type="text" placeholder="請填寫職業" value="" /></view></view><!-- 提交按鈕 --><button class="form-button">保 存</button></view></view>
</template><style lang="scss">
page {background-color: #f4f4f4;
}.viewport {display: flex;flex-direction: column;height: 100%;background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);background-size: auto 420rpx;background-repeat: no-repeat;
}// 導航欄
.navbar {position: relative;.title {height: 40px;display: flex;justify-content: center;align-items: center;font-size: 16px;font-weight: 500;color: #fff;}.back {position: absolute;height: 40px;width: 40px;left: 0;font-size: 20px;color: #fff;display: flex;justify-content: center;align-items: center;}
}// 頭像
.avatar {text-align: center;width: 100%;height: 260rpx;display: flex;flex-direction: column;justify-content: center;align-items: center;.image {width: 160rpx;height: 160rpx;border-radius: 50%;background-color: #eee;}.text {display: block;padding-top: 20rpx;line-height: 1;font-size: 26rpx;color: #fff;}
}// 表單
.form {background-color: #f4f4f4;&-content {margin: 20rpx 20rpx 0;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;}&-item {display: flex;height: 96rpx;line-height: 46rpx;padding: 25rpx 10rpx;background-color: #fff;font-size: 28rpx;border-bottom: 1rpx solid #ddd;&:last-child {border: none;}.label {width: 180rpx;color: #333;}.account {color: #666;}.input {flex: 1;display: block;height: 46rpx;}.radio {margin-right: 20rpx;}.picker {flex: 1;}.placeholder {color: #808080;}}&-button {height: 80rpx;text-align: center;line-height: 80rpx;margin: 30rpx 20rpx;color: #fff;border-radius: 80rpx;font-size: 30rpx;background-color: #27ba9b;}
}
</style>
獲取會員信息
需要登錄后才能獲取用戶個人信息,在 項目起步 模塊已封裝請求攔截器,攔截器中自動添加 token
,無需再手動添加。
接口調用
接口地址:/member/profile
請求方式:GET
登錄權限: 是
請求參數:無
接口封裝
src/services/profile.ts
import type { ProfileDetail } from '@/types/member'
import { http } from '@/utils/http'/*** 獲取個人信息*/
export const getMemberProfileAPI = () => {return http<ProfileDetail>({method: 'GET',url: '/member/profile',})
}
類型聲明
src/types/member.d.ts
/** 個人信息 用戶詳情信息 */
export type ProfileDetail = {/** 用戶ID */id: number/** 頭像 */avatar: string/** 賬戶名 */account: string/** 昵稱 */nickname?: string/** 性別 */gender?: Gender/** 生日 */birthday?: string/** 省市區 */fullLocation?: string/** 職業 */profession?: string
}
/** 性別 */
export type Gender = '女' | '男'
類型聲明封裝升級(可選),提取用戶信息通用部分,再復用類型。
/** 封裝通用信息 */
type BaseProfile = {/** 用戶ID */id: number/** 頭像 */avatar: string/** 賬戶名 */account: string/** 昵稱 */nickname?: string
}/** 小程序登錄 登錄用戶信息 */
export type LoginResult = BaseProfile & {/** 用戶ID */id: number/** 頭像 */avatar: string/** 賬戶名 */account: string/** 昵稱 */nickname?: string/** 手機號 */mobile: string/** 登錄憑證 */token: string
}/** 個人信息 用戶詳情信息 */
export type ProfileDetail = BaseProfile & {/** 性別 */gender?: Gender/** 生日 */birthday?: string/** 省市區 */fullLocation?: string/** 職業 */profession?: string
}
/** 性別 */
export type Gender = '女' | '男'
渲染會員信息
會員信息頁
<script setup lang="ts">
import { getMemberProfileAPI } from '@/services/profile'
import type { ProfileDetail } from '@/types/member'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()// 獲取個人信息
const profile = ref<ProfileDetail>()
const getMemberProfileData = async () => {const res = await getMemberProfileAPI()profile.value = res.result
}onLoad(() => {getMemberProfileData()
})
</script><template><view class="viewport"><!-- 導航欄 --><view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"><navigator open-type="navigateBack" class="back icon-left" hover-class="none"></navigator><view class="title">個人信息</view></view><!-- 頭像 --><view class="avatar"><view class="avatar-content"><image class="image" :src="profile?.avatar" mode="aspectFill" /><text class="text">點擊修改頭像</text></view></view><!-- 表單 --><view class="form"><!-- 表單內容 --><view class="form-content"><view class="form-item"><text class="label">賬號</text><text class="account">{{ profile?.account }}</text></view><view class="form-item"><text class="label">昵稱</text><input class="input" type="text" placeholder="請填寫昵稱" :value="profile?.nickname" /></view><view class="form-item"><text class="label">性別</text><radio-group><label class="radio"><radio value="男" color="#27ba9b" :checked="profile?.gender === '男'" />男</label><label class="radio"><radio value="女" color="#27ba9b" :checked="profile?.gender === '女'" />女</label></radio-group></view><view class="form-item"><text class="label">出生日期</text><pickerclass="picker"mode="date":value="profile?.birthday"start="1900-01-01":end="new Date()"><view v-if="profile?.birthday">{{ profile?.birthday }}</view><view class="placeholder" v-else>請選擇日期</view></picker></view><view class="form-item"><text class="label">城市</text><picker class="picker" :value="profile?.fullLocation?.split(' ')" mode="region"><view v-if="profile?.fullLocation">{{ profile.fullLocation }}</view><view class="placeholder" v-else>請選擇城市</view></picker></view><view class="form-item"><text class="label">職業</text><input class="input" type="text" placeholder="請填寫職業" :value="profile?.profession" /></view></view><!-- 提交按鈕 --><button class="form-button">保 存</button></view></view>
</template>
更新會員頭像
-
通過
uni.chooseMedia()
讀取用戶相冊的照片或者拍照。 -
通過
uni.uploadFile()
上傳用戶圖片。
接口信息
接口地址:/member/profile/avatar
請求方式:POST
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
name | 是 | 無 | 后端數據字段名 |
filePath | 是 | 無 | 新頭像 |
參考代碼
更新會員頭像
<script setup lang="ts">
// ...省略// 修改頭像
const onAvatarChange = () => {// 調用拍照/選擇圖片uni.chooseMedia({// 文件個數count: 1,// 文件類型mediaType: ['image'],success: (res) => {// 本地路徑const { tempFilePath } = res.tempFiles[0]// 文件上傳uni.uploadFile({url: '/member/profile/avatar', // [!code ++]name: 'file', // 后端數據字段名 // [!code ++]filePath: tempFilePath, // 新頭像 // [!code ++]success: (res) => {// 判斷狀態碼是否上傳成功if (res.statusCode === 200) {// 提取頭像const { avatar } = JSON.parse(res.data).result// 當前頁面更新頭像profile.value!.avatar = avatar // [!code ++]// 更新 Store 頭像memberStore.profile!.avatar = avatar // [!code ++]uni.showToast({ icon: 'success', title: '更新成功' })} else {uni.showToast({ icon: 'error', title: '出現錯誤' })}},})},})
}
</script>
::: tip 知識回顧
-
網頁端上傳文件用
Axios + FormData
-
小程序端上傳文件用 wx.uploadFile()
-
使用 uni.uploadFile() 能自動多端兼容
-
小程序端需配置上傳文件安全域名
:::
更新表單信息
涉及到 <input>
、<radio>
、<picker>
表單組件的數據收集。
接口信息
接口地址:/member/profile
請求方式:PUT
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
nickname | 是 | 無 | 用戶昵稱 |
gender | 是 | 無 | 用戶性別 |
birthday | 是 | 無 | 用戶生日 |
profession | 是 | 無 | 用戶職業 |
接口封裝
/*** 修改個人信息* @param data 請求體參數*/
export const putMemberProfileAPI = (data: ProfileParams) => {return http<ProfileDetail>({method: 'PUT',url: '/member/profile',data,})
}
類型聲明
/** 個人信息 修改請求體參數 */
export type ProfileParams = Pick<ProfileDetail,'nickname' | 'gender' | 'birthday' | 'profession'
> & {/** 省份編碼 */provinceCode?: string/** 城市編碼 */cityCode?: string/** 區/縣編碼 */countyCode?: string
}
參考代碼
<input>
組件使用 v-model
收集數據,<radio-group>
組件使用 @change
事件收集數據。
<script setup lang="ts">
import type { Gender, ProfileDetail } from '@/types/member'// 獲取個人信息,修改個人信息需提供初始值 // [!code ++]
const profile = ref({} as ProfileDetail) // [!code ++]
const profile = ref<ProfileDetail>() // [!code --]// 修改性別
const onGenderChange: UniHelper.RadioGroupOnChange = (ev) => {profile.value.gender = ev.detail.value as Gender
}
</script><template><view class="viewport"><!-- 表單 --><view class="form"><!-- 表單內容 --><view class="form-content"><view class="form-item"><text class="label">賬號</text><text class="account">{{ profile.account }}</text></view><view class="form-item"><text class="label">昵稱</text><input class="input" type="text" placeholder="請填寫昵稱" v-model="profile.nickname" /></view><view class="form-item"><text class="label">性別</text><radio-group @change="onGenderChange"><label class="radio"><radio value="男" color="#27ba9b" :checked="profile.gender === '男'" />男</label><label class="radio"><radio value="女" color="#27ba9b" :checked="profile.gender === '女'" />女</label></radio-group></view>...省略<view class="form-item"><text class="label">職業</text><input class="input" type="text" placeholder="請填寫職業" v-model="profile.profession" /></view></view></view></view>
</template>
<picker>
組件使用 @change
事件收集數據。
<script setup lang="ts">
// 修改生日
const onBirthdayChange: UniHelper.DatePickerOnChange = (ev) => {profile.value.birthday = ev.detail.value
}// 修改城市
let fullLocationCode: [string, string, string] = ['', '', '']
const onFullLocationChange: UniHelper.RegionPickerOnChange = (ev) => {// 修改前端界面profile.value.fullLocation = ev.detail.value.join(' ')// 提交后端更新fullLocationCode = ev.detail.code!
}
</script><template><view class="form-item"><text class="label">生日</text><pickerclass="picker"mode="date"start="1900-01-01":end="new Date()":value="profile.birthday"@change="onBirthdayChange"><view v-if="profile.birthday">{{ profile.birthday }}</view><view class="placeholder" v-else>請選擇日期</view></picker></view><view class="form-item"><text class="label">城市</text><pickerclass="picker"mode="region":value="profile.fullLocation?.split(' ')"@change="onFullLocationChange"><view v-if="profile.fullLocation">{{ profile.fullLocation }}</view><view class="placeholder" v-else>請選擇城市</view></picker></view>
</template>
提交表單,更新會員信息,Store 昵稱記得修改,用于會員中心頁展示。
<script setup lang="ts">
import { useMemberStore } from '@/stores'// 點擊保存提交表單
const memberStore = useMemberStore()// 點擊保存提交表單
const onSubmit = async () => {const { nickname, gender, birthday, profession } = profile.valueconst res = await putMemberProfileAPI({nickname,gender,birthday,profession,provinceCode: fullLocationCode[0],cityCode: fullLocationCode[1],countyCode: fullLocationCode[2],})// 更新Store昵稱memberStore.profile!.nickname = res.result.nicknameuni.showToast({ icon: 'success', title: '保存成功' })setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><!-- 提交按鈕 --><button @tap="onSubmit" class="form-button">保 存</button>
</template>
8.小兔鮮兒 - 地址模塊
- 能夠獲取不同類型的表單數據
- 能夠動態設置導航欄的標題
- 能夠使用 uni-ui 組件庫的組件
- 能夠完成收貨地址的增刪改查的功能
準備工作
靜態結構
地址模塊共兩個頁面:地址管理頁,地址表單頁 ,劃分到會員分包中。
地址管理頁
src/pagesMember/address/address.vue
<script setup lang="ts">
//
</script><template><view class="viewport"><!-- 地址列表 --><scroll-view class="scroll-view" scroll-y><view v-if="true" class="address"><view class="address-list"><!-- 收貨地址項 --><view class="item"><view class="item-content"><view class="user">黑馬小王子<text class="contact">13111111111</text><text v-if="true" class="badge">默認</text></view><view class="locate">廣東省 廣州市 天河區 黑馬程序員</view><navigatorclass="edit"hover-class="none":url="`/pagesMember/address-form/address-form?id=1`">修改</navigator></view></view><!-- 收貨地址項 --><view class="item"><view class="item-content"><view class="user">黑馬小公主<text class="contact">13222222222</text><text v-if="false" class="badge">默認</text></view><view class="locate">北京市 北京市 順義區 黑馬程序員</view><navigatorclass="edit"hover-class="none":url="`/pagesMember/address-form/address-form?id=2`">修改</navigator></view></view></view></view><view v-else class="blank">暫無收貨地址</view></scroll-view><!-- 添加按鈕 --><view class="add-btn"><navigator hover-class="none" url="/pagesMember/address-form/address-form">新建地址</navigator></view></view>
</template><style lang="scss">
page {height: 100%;overflow: hidden;
}/* 刪除按鈕 */
.delete-button {display: flex;justify-content: center;align-items: center;width: 50px;height: 100%;font-size: 28rpx;color: #fff;border-radius: 0;padding: 0;background-color: #cf4444;
}.viewport {display: flex;flex-direction: column;height: 100%;background-color: #f4f4f4;.scroll-view {padding-top: 20rpx;}
}.address {padding: 0 20rpx;margin: 0 20rpx;border-radius: 10rpx;background-color: #fff;.item-content {line-height: 1;padding: 40rpx 10rpx 38rpx;border-bottom: 1rpx solid #ddd;position: relative;.edit {position: absolute;top: 36rpx;right: 30rpx;padding: 2rpx 0 2rpx 20rpx;border-left: 1rpx solid #666;font-size: 26rpx;color: #666;line-height: 1;}}.item:last-child .item-content {border: none;}.user {font-size: 28rpx;margin-bottom: 20rpx;color: #333;.contact {color: #666;}.badge {display: inline-block;padding: 4rpx 10rpx 2rpx 14rpx;margin: 2rpx 0 0 10rpx;font-size: 26rpx;color: #27ba9b;border-radius: 6rpx;border: 1rpx solid #27ba9b;}}.locate {line-height: 1.6;font-size: 26rpx;color: #333;}
}.blank {margin-top: 300rpx;text-align: center;font-size: 32rpx;color: #888;
}.add-btn {height: 80rpx;text-align: center;line-height: 80rpx;margin: 30rpx 20rpx;color: #fff;border-radius: 80rpx;font-size: 30rpx;background-color: #27ba9b;
}
</style>
地址表單頁
src/pagesMember/address-form/address-form.vue
<script setup lang="ts">
import { ref } from 'vue'// 表單數據
const form = ref({receiver: '', // 收貨人contact: '', // 聯系方式fullLocation: '', // 省市區(前端展示)provinceCode: '', // 省份編碼(后端參數)cityCode: '', // 城市編碼(后端參數)countyCode: '', // 區/縣編碼(后端參數)address: '', // 詳細地址isDefault: 0, // 默認地址,1為是,0為否
})
</script><template><view class="content"><form><!-- 表單內容 --><view class="form-item"><text class="label">收貨人</text><input class="input" placeholder="請填寫收貨人姓名" value="" /></view><view class="form-item"><text class="label">手機號碼</text><input class="input" placeholder="請填寫收貨人手機號碼" value="" /></view><view class="form-item"><text class="label">所在地區</text><picker class="picker" mode="region" value=""><view v-if="false">廣東省 廣州市 天河區</view><view v-else class="placeholder">請選擇省/市/區(縣)</view></picker></view><view class="form-item"><text class="label">詳細地址</text><input class="input" placeholder="街道、樓牌號等信息" value="" /></view><view class="form-item"><label class="label">設為默認地址</label><switch class="switch" color="#27ba9b" :checked="true" /></view></form></view><!-- 提交按鈕 --><button class="button">保存并使用</button>
</template><style lang="scss">
page {background-color: #f4f4f4;
}.content {margin: 20rpx 20rpx 0;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;.form-item,.uni-forms-item {display: flex;align-items: center;min-height: 96rpx;padding: 25rpx 10rpx 40rpx;background-color: #fff;font-size: 28rpx;border-bottom: 1rpx solid #ddd;position: relative;margin-bottom: 0;// 調整 uni-forms 樣式.uni-forms-item__content {display: flex;}.uni-forms-item__error {margin-left: 200rpx;}&:last-child {border: none;}.label {width: 200rpx;color: #333;}.input {flex: 1;display: block;height: 46rpx;}.switch {position: absolute;right: -20rpx;transform: scale(0.8);}.picker {flex: 1;}.placeholder {color: #808080;}}
}.button {height: 80rpx;margin: 30rpx 20rpx;color: #fff;border-radius: 80rpx;font-size: 30rpx;background-color: #27ba9b;
}
</style>
動態設置標題
新建地址 和 修改地址 復用同一個地址表單頁,需要根據頁面參數 id
動態設置頁面標題。
<script setup lang="ts">
// 獲取頁面參數
const query = defineProps<{id?: string
}>()// 動態設置標題
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })
</script>
新建地址頁
新用戶沒有收貨地址,先完成新建地址,新建成功返回地址管理頁。
主要功能:前端收集表單的數據,提交表單給后端。
接口封裝
接口調用
接口地址:/member/address
請求方式:POST
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 類型 | 備注 |
---|---|---|---|
receiver | 是 | string | 收貨人姓名 |
contact | 是 | string | 收貨人聯系方式 |
provinceCode | 是 | string | 省對應的 code |
cityCode | 是 | string | 市對應的 code |
countyCode | 是 | string | 區/縣對應的 code |
address | 是 | string | 收貨人詳細地址 |
isDefault | 是 | number | 是否設置為默認地址(數值類型) |
接口封裝
src/services/address.ts
import type { AddressParams } from '@/types/address'
import { http } from '@/utils/http'/*** 添加收貨地址* @param data 請求參數*/
export const postMemberAddressAPI = (data: AddressParams) => {return http({method: 'POST',url: '/member/address',data,})
}
類型聲明
src/types/address.d.ts
/** 添加收貨地址: 請求參數 */
export type AddressParams = {/** 收貨人姓名 */receiver: string/** 聯系方式 */contact: string/** 省份編碼 */provinceCode: string/** 城市編碼 */cityCode: string/** 區/縣編碼 */countyCode: string/** 詳細地址 */address: string/** 默認地址,1為是,0為否 */isDefault: number
}
參考代碼
地址表單頁,input
組件通過 v-model
獲取數據,其他表單組件結合 @change
事件獲取。
<script setup lang="ts">
import { postMemberAddressAPI } from '@/services/address'
import { ref } from 'vue'// 表單數據
const form = ref({receiver: '', // 收貨人contact: '', // 聯系方式fullLocation: '', // 省市區(前端展示)provinceCode: '', // 省份編碼(后端參數)cityCode: '', // 城市編碼(后端參數)countyCode: '', // 區/縣編碼(后端參數)address: '', // 詳細地址isDefault: 0, // 默認地址,1為是,0為否
})// 收集所在地區
const onRegionChange: UniHelper.RegionPickerOnChange = (ev) => {// 省市區(前端展示)form.value.fullLocation = ev.detail.value.join(' ')// 省市區(后端參數)const [provinceCode, cityCode, countyCode] = ev.detail.code!// 合并數據Object.assign(form.value, { provinceCode, cityCode, countyCode })
}// 收集是否默認收貨地址
const onSwitchChange: UniHelper.SwitchOnChange = (ev) => {form.value.isDefault = ev.detail.value ? 1 : 0
}// 提交表單
const onSubmit = async () => {// 新建地址請求await postMemberAddressAPI(form.value)// 成功提示uni.showToast({ icon: 'success', title: '添加成功' })// 返回上一頁setTimeout(() => {uni.navigateBack()}, 400)
}
</script><template><view class="content"><form><!-- 表單內容 --><view class="form-item"><text class="label">收貨人</text><input class="input" placeholder="請填寫收貨人姓名" v-model="form.receiver" /></view><view class="form-item"><text class="label">手機號碼</text><input class="input" placeholder="請填寫收貨人手機號碼" v-model="form.contact" /></view><view class="form-item"><text class="label">所在地區</text><pickerclass="picker"mode="region":value="form.fullLocation.split(' ')"@change="onRegionChange"><view v-if="form.fullLocation">{{ form.fullLocation }}</view><view v-else class="placeholder">請選擇省/市/區(縣)</view></picker></view><view class="form-item"><text class="label">詳細地址</text><input class="input" placeholder="街道、樓牌號等信息" v-model="form.address" /></view><view class="form-item"><label class="label">設為默認地址</label><switchclass="switch"color="#27ba9b":checked="form.isDefault === 1"@change="onSwitchChange"/></view></form></view><!-- 提交按鈕 --><button @tap="onSubmit" class="button">保存并使用</button>
</template>
地址管理頁
為了能及時看到新建的收貨地址,需在 onShow
生命周期中獲取地址列表數據。
接口調用
接口地址:/member/address
請求方式:GET
登錄權限: 是
請求參數:無
接口封裝
src/types/address.ts
/*** 獲取收貨地址列表*/
export const getMemberAddressAPI = () => {return http<AddressItem[]>({method: 'GET',url: '/member/address',})
}
類型聲明
src/types/address.d.ts
/** 收貨地址項 */
export type AddressItem = {/** 收貨人姓名 */receiver: string/** 聯系方式 */contact: string/** 省份編碼 */provinceCode: string/** 城市編碼 */cityCode: string/** 區/縣編碼 */countyCode: string/** 詳細地址 */address: string/** 默認地址,1為是,0為否 */isDefault: number/** 收貨地址 id */id: string/** 省市區 */fullLocation: string
}
復用地址類型:src/types/goods.d.ts
+ import type { AddressItem } from './global'- /** 地址信息 */
- export type AddressItem = {
- receiver: string
- contact: string
- provinceCode: string
- cityCode: string
- countyCode: string
- address: string
- isDefault: number
- id: string
- fullLocation: string
- }
::: tip 溫馨提示
用戶登錄后再訪問商品詳情,商品詳情字段中包含用戶收貨地址列表,可以復用收貨地址類型。
:::
參考代碼
地址管理頁
<script setup lang="ts">
import { getMemberAddressAPI } from '@/services/address'
import type { AddressItem } from '@/types/address'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'// 獲取收貨地址列表數據
const addressList = ref<AddressItem[]>([])
const getMemberAddressData = async () => {const res = await getMemberAddressAPI()addressList.value = res.result
}// 初始化調用(頁面顯示)
onShow(() => {getMemberAddressData()
})
</script><template><view class="viewport"><!-- 地址列表 --><scroll-view class="scroll-view" scroll-y><view v-if="true" class="address"><view class="address-list"><!-- 收獲地址項 --><view class="item" v-for="item in addressList" :key="item.id"><view class="item-content"><view class="user">{{ item.receiver }}<text class="contact">{{ item.contact }}</text><text v-if="item.isDefault" class="badge">默認</text></view><view class="locate">{{ item.fullLocation }} {{ item.address }}</view><navigatorclass="edit"hover-class="none":url="`/pagesMember/address-form/address-form?id=${item.id}`">修改</navigator></view></view></view></view><view v-else class="blank">暫無收貨地址</view></scroll-view><!-- 添加按鈕 --><view class="add-btn"><navigator hover-class="none" url="/pagesMember/address-form/address-form">新建地址</navigator></view></view>
</template>
修改地址頁
通過頁面參數 id
來區分當前是修改地址還是新建地址。
數據回顯
修改地址之前,需要先實現數據回顯,用戶再進行有針對性的修改。
接口詳情
接口地址:/member/address/:id
請求方式:GET
登錄權限: 是
請求參數:
路徑參數
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
id | 是 | 無 | 收貨地址 ID |
接口封裝
src/services/address.ts
/*** 獲取收貨地址詳情* @param id 地址id(路徑參數)*/
export const getMemberAddressByIdAPI = (id: string) => {return http<AddressItem>({method: 'GET',url: `/member/address/${id}`,})
}
參考代碼
頁面初始化的時候根據 id
獲取地址詳情,把獲取的數據合并到表單數據中,用于數據回顯。
<script setup lang="ts">
// 獲取收貨地址詳情數據
const getMemberAddressByIdData = async () => {// 有 id 才調用接口if (query.id) {// 發送請求const res = await getMemberAddressByIdAPI(query.id)// 把數據合并到表單中Object.assign(form.value, res.result)}
}// 頁面加載
onLoad(() => {getMemberAddressByIdData()
})
</script>
更新地址
將用戶修改后的地址信息重新發送到服務端進行存儲。
接口詳情
接口地址:/member/address/:id
請求方式:PUT
登錄權限: 是
請求參數:
路徑參數
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
id | 是 | 無 | 收貨地址 ID |
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
receiver | 是 | 無 | 收貨人姓名 |
contact | 是 | 無 | 收貨人聯系方式 |
provinceCode | 是 | 無 | 行政省對應的 code |
cityCode | 是 | 無 | 行政市對應的 code |
countyCode | 是 | 無 | 行政區縣對應的 code |
address | 是 | 無 | 收貨人詳細地址 |
isDefault | 是 | 無 | 是否設置為默認地址(數值類型) |
接口封裝
/*** 修改收貨地址* @param id 地址id(路徑參數)* @param data 表單數據(請求體參數)*/
export const putMemberAddressByIdAPI = (id: string, data: AddressParams) => {return http({method: 'PUT',url: `/member/address/${id}`,data,})
}
參考代碼
根據是否有地址 id 來判斷提交表單到底是新建地址還是更新地址。
<script setup lang="ts">
// 提交表單
const onSubmit = async () => {// 判斷當前頁面是否有地址 idif (query.id) {// 修改地址請求await putMemberAddressByIdAPI(query.id, form.value)} else {// 新建地址請求await postMemberAddressAPI(form.value)}// 成功提示uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })// 返回上一頁setTimeout(() => {uni.navigateBack()}, 400)
}
</script>
表單校驗
通過 uni-ui
組件庫的 uni-forms 組件實現表單校驗。
操作步驟
- 定義校驗規則
- 修改表單結構
- 綁定校驗規則
- 提交時校驗表單
參考代碼
<script setup lang="ts">
// 定義校驗規則
const rules: UniHelper.UniFormsRules = {receiver: {rules: [{ required: true, errorMessage: '請輸入收貨人姓名' }],},contact: {rules: [{ required: true, errorMessage: '請輸入聯系方式' },{ pattern: /^1[3-9]\d{9}$/, errorMessage: '手機號格式不正確' },],},fullLocation: {rules: [{ required: true, errorMessage: '請選擇所在地區' }],},address: {rules: [{ required: true, errorMessage: '請選擇詳細地址' }],},
}// 獲取表單組件實例,用于調用表單方法
const formRef = ref<UniHelper.UniFormsInstance>() // [!code ++]// 提交表單
const onSubmit = async () => {try {// 表單校驗await formRef.value?.validate?.() // [!code ++]// 校驗通過后再發送請求if (query.id) {// 修改地址請求await putMemberAddressByIdAPI(query.id, form.value)} else {// 新建地址請求await postMemberAddressAPI(form.value)}// 成功提示uni.showToast({ icon: 'success', title: query.id ? '修改成功' : '添加成功' })// 返回上一頁setTimeout(() => {uni.navigateBack()}, 400)} catch (error) {uni.showToast({ icon: 'error', title: '請填寫完整信息' }) // [!code ++]}
}
</script><template><view class="content"><uni-forms :rules="rules" :model="form" ref="formRef"><!-- 表單內容 --><uni-forms-item name="receiver" class="form-item"><text class="label">收貨人</text><input class="input" placeholder="請填寫收貨人姓名" v-model="form.receiver" /></uni-forms-item><uni-forms-item name="contact" class="form-item"><text class="label">手機號碼</text><inputclass="input"placeholder="請填寫收貨人手機號碼":maxlength="11"v-model="form.contact"/></uni-forms-item><uni-forms-item name="fullLocation" class="form-item"><text class="label">所在地區</text><pickerclass="picker"@change="onRegionChange"mode="region":value="form.fullLocation.split(' ')"><view v-if="form.fullLocation">{{ form.fullLocation }}</view><view v-else class="placeholder">請選擇省/市/區(縣)</view></picker></uni-forms-item><uni-forms-item name="address" class="form-item"><text class="label">詳細地址</text><input class="input" placeholder="街道、樓牌號等信息" v-model="form.address" /></uni-forms-item><view class="form-item"><label class="label">設為默認地址</label><switchclass="switch"color="#27ba9b"@change="onSwitchChange":checked="form.isDefault === 1"/></view></uni-forms></view><!-- 提交按鈕 --><button @tap="onSubmit" class="button">保存并使用</button>
</template>
刪除地址
通過 uni-ui
組件庫的 uni-swipe-action 組件實現側滑刪除。
側滑組件用法
<template><!-- 滑動操作分區 --><uni-swipe-action><!-- 滑動操作項 --><uni-swipe-action-item><!-- 默認插槽 --><view>內容</view><!-- 右側插槽 --><template #right><button class="delete-button">刪除</button></template></uni-swipe-action-item></uni-swipe-action>
</template>
接口封裝
接口詳情
接口地址:/member/address/:id
請求方式:DELETE
登錄權限: 是
請求參數:
路徑參數
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
id | 是 | 無 | ID |
接口封裝
/*** 刪除收貨地址* @param id 地址id(路徑參數)*/
export const deleteMemberAddressByIdAPI = (id: string) => {return http({method: 'DELETE',url: `/member/address/${id}`,})
}
參考代碼
側滑地址列表項,右側顯示刪除按鈕,刪除地址前需二次確認。
<script setup lang="ts">
// 刪除收貨地址
const onDeleteAddress = (id: string) => {// 二次確認uni.showModal({content: '刪除地址?',success: async (res) => {if (res.confirm) {// 根據id刪除收貨地址await deleteMemberAddressByIdAPI(id)// 重新獲取收貨地址列表getMemberAddressData()}},})
}
</script><template><view class="viewport"><!-- 地址列表 --><scroll-view class="scroll-view" scroll-y><view v-if="addressList.length" class="address"><uni-swipe-action class="address-list"><!-- 收貨地址項 --><uni-swipe-action-item class="item" v-for="item in addressList" :key="item.id"><view class="item-content"><view class="user">{{ item.receiver }}<text class="contact">{{ item.contact }}</text><text v-if="item.isDefault" class="badge">默認</text></view><view class="locate">{{ item.fullLocation }} {{ item.address }}</view><navigatorclass="edit"hover-class="none":url="`/pagesMember/address-form/address-form?id=${item.id}`">修改</navigator></view><!-- 右側插槽 --><template #right><button @tap="onDeleteAddress(item.id)" class="delete-button">刪除</button></template></uni-swipe-action-item></uni-swipe-action></view><view v-else class="blank">暫無收貨地址</view></scroll-view><!-- 添加按鈕 --><view class="add-btn"><navigator hover-class="none" url="/pagesMember/address-form/address-form">新建地址</navigator></view></view>
</template>
9.小兔鮮兒 - SKU 模塊
學會使用插件市場,下載并使用 SKU
組件,實現商品詳情頁規格展示和交互。
存貨單位(SKU)
SKU 概念
存貨單位(Stock Keeping Unit),庫存管理的最小可用單元,通常稱為“單品”。
SKU
常見于電商領域,對于前端工程師而言,更多關注 SKU 算法 和 用戶交互體驗。
插件市場
uni-app 插件市場,是 uni-app 官方插件生態集中地。
SKU
屬于電商常見業務,插件市場有現成的 SKU
插件,我們下載并在項目中使用。
下載 SKU 插件
經過綜合評估,我們選擇該SKU 插件,請下載插件到本地。
體驗地址
::: tip 常見問題
Q:如何評估第三方插件的質量?
A:查看插件的評分、評價、下載量、更新頻率以及文檔完整性,以確保插件具有良好的社區口碑、兼容性、性能和維護狀況。
:::
使用 SKU 插件
組件安裝到自己項目
- 復制
vk-data-goods-sku-popup
和vk-data-input-number-box
到項目的根components
目錄下。 - 復制例子代碼并運行體驗。
插件文檔(部分)
Props 參數
Props | 說明 | 類型 | 默認值 | 可選值 |
---|---|---|---|---|
v-model | 雙向綁定,true 為打開組件,false 為關閉組件 | Boolean | false | true、false |
mode | 模式 1:都顯示 2:只顯示購物車 3:只顯示立即購買 | Number | 1 | 1、2、3 |
localdata | 商品信息本地數據源 | Object | - | - |
Event 事件名
Event | 說明 | 回調參數 |
---|---|---|
add-cart | 點擊添加到購物車時(需選擇完 SKU 才會觸發) | selectShop:當前選擇的 sku 數據 |
buy-now | 點擊立即購買時(需選擇完 SKU 才會觸發) | selectShop:當前選擇的 sku 數據 |
open | 打開組件時 | - |
close | 關閉組件時 | - |
::: tip 常見問題
Q:為什么插件使用時無需導入?
A:pages.json
的 easycom
配置中,默認自動掃描 xxx/xxx.vue
格式的組件,實現自動導入。
Q:為什么組件代碼 Git 提交時報錯?
A:插件未采用 eslint
校驗代碼,請在插件源文件中添加 /* eslint-disable */
,禁用 eslint
。
:::
在 vk-data-goods-sku-popup.vue
和 vk-data-input-number-box.vue
組件禁用 eslint
。
<script>
/* eslint-disable */
// 省略組件源代碼
</script>
溫馨提示: 插件的作者已合并 eslint-disable
PR ,現在已無需手動添加該注釋。
插件類型問題
盡管該插件未采用 TS 開發,但作者提供了詳細的插件文檔,我們可以依據文檔為插件添加 TS 類型聲明文件,從而提高項目數據校驗的安全性。
類型聲明文件
vk-data-goods-sku-popup.d.ts
import { Component } from '@uni-helper/uni-app-types'/** SKU 彈出層 */
export type SkuPopup = Component<SkuPopupProps>/** SKU 彈出層實例 */
export type SkuPopupInstanceType = InstanceType<SkuPopup>/** SKU 彈出層屬性 */
export type SkuPopupProps = {/** 雙向綁定,true 為打開組件,false 為關閉組件 */modelValue: boolean/** 商品信息本地數據源 */localdata: SkuPopupLocaldata/** 按鈕模式 1:都顯示 2:只顯示購物車 3:只顯示立即購買 */mode?: 1 | 2 | 3/** 該商品已搶完時的按鈕文字 */noStockText?: string/** 庫存文字 */stockText?: string/** 點擊遮罩是否關閉組件 */maskCloseAble?: boolean/** 頂部圓角值 */borderRadius?: string | number/** 最小購買數量 */minBuyNum?: number/** 最大購買數量 */maxBuyNum?: number/** 每次點擊后的數量 */stepBuyNum?: number/** 是否只能輸入 step 的倍數 */stepStrictly?: boolean/** 是否隱藏庫存的顯示 */hideStock?: false/** 主題風格 */theme?: 'default' | 'red-black' | 'black-white' | 'coffee' | 'green'/** 默認金額會除以100(即100=1元),若設置為0,則不會除以100(即1=1元) */amountType?: 1 | 0/** 自定義獲取商品信息的函數(已知支付寶不支持,支付寶請改用localdata屬性) */customAction?: () => void/** 是否顯示右上角關閉按鈕 */showClose?: boolean/** 關閉按鈕的圖片地址 */closeImage?: string/** 價格的字體顏色 */priceColor?: string/** 立即購買 - 按鈕的文字 */buyNowText?: string/** 立即購買 - 按鈕的字體顏色 */buyNowColor?: string/** 立即購買 - 按鈕的背景顏色 */buyNowBackgroundColor?: string/** 加入購物車 - 按鈕的文字 */addCartText?: string/** 加入購物車 - 按鈕的字體顏色 */addCartColor?: string/** 加入購物車 - 按鈕的背景顏色 */addCartBackgroundColor?: string/** 商品縮略圖背景顏色 */goodsThumbBackgroundColor?: string/** 樣式 - 不可點擊時,按鈕的樣式 */disableStyle?: object/** 樣式 - 按鈕點擊時的樣式 */activedStyle?: object/** 樣式 - 按鈕常態的樣式 */btnStyle?: object/** 字段名 - 商品表id的字段名 */goodsIdName?: string/** 字段名 - sku表id的字段名 */skuIdName?: string/** 字段名 - 商品對應的sku列表的字段名 */skuListName?: string/** 字段名 - 商品規格名稱的字段名 */specListName?: string/** 字段名 - sku庫存的字段名 */stockName?: string/** 字段名 - sku組合路徑的字段名 */skuArrName?: string/** 字段名 - 商品縮略圖字段名(未選擇sku時) */goodsThumbName?: string/** 被選中的值 */selectArr?: string[]/** 打開彈出層 */onOpen: () => void/** 關閉彈出層 */onClose: () => void/** 點擊加入購物車時(需選擇完SKU才會觸發)*/onAddCart: (event: SkuPopupEvent) => void/** 點擊立即購買時(需選擇完SKU才會觸發)*/onBuyNow: (event: SkuPopupEvent) => void
}/** 商品信息本地數據源 */
export type SkuPopupLocaldata = {/** 商品 ID */_id: string/** 商品名稱 */name: string/** 商品圖片 */goods_thumb: string/** 商品規格列表 */spec_list: SkuPopupSpecItem[]/** 商品SKU列表 */sku_list: SkuPopupSkuItem[]
}/** 商品規格名稱的集合 */
export type SkuPopupSpecItem = {/** 規格名稱 */name: string/** 規格集合 */list: { name: string }[]
}/** 商品SKU列表 */
export type SkuPopupSkuItem = {/** SKU ID */_id: string/** 商品 ID */goods_id: string/** 商品名稱 */goods_name: string/** 商品圖片 */image: string/** SKU 價格 * 100, 注意:需要乘以 100 */price: number/** SKU 規格組成, 注意:需要與 spec_list 數組順序對應 */sku_name_arr: string[]/** SKU 庫存 */stock: number
}/** 當前選擇的sku數據 */
export type SkuPopupEvent = SkuPopupSkuItem & {/** 商品購買數量 */buy_num: number
}/** 全局組件類型聲明 */
declare module 'vue' {export interface GlobalComponents {'vk-data-goods-sku-popup': SkuPopup}
}
核心業務
渲染商品規格
使用以下兩個屬性:
localdata
綁定商品SKU
數據來源v-model
雙向綁定,顯示/隱藏組件
注意:后端返回的數據格式和插件所需的格式不一致,我們需要按插件要求進行處理。
<script setup lang="ts">
import type { SkuPopupLocaldata } from '@/components/vk-data-goods-sku-popup/vk-data-goods-sku-popup'// 獲取商品詳情信息
const goods = ref<GoodsResult>()
const getGoodsByIdData = async () => {const res = await getGoodsByIdAPI(query.id)goods.value = res.result// SKU組件所需格式localdata.value = {_id: res.result.id,name: res.result.name,goods_thumb: res.result.mainPictures[0],spec_list: res.result.specs.map((v) => ({ name: v.name, list: v.values })),sku_list: res.result.skus.map((v) => ({_id: v.id,goods_id: res.result.id,goods_name: res.result.name,image: v.picture,price: v.price * 100, // 注意:需要乘以 100stock: v.inventory,sku_name_arr: v.specs.map((vv) => vv.valueName),})),}
}// 是否顯示SKU組件
const isShowSku = ref(false)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
</script><template><!-- SKU彈窗組件 --><vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" /><!-- 彈窗測試 --><button @tap="isShowSku = true">打開 SKU 彈窗</button>
</template>
打開彈窗交互
SKU 彈窗的按鈕有三種形式。
<script setup lang="ts">
// 按鈕模式
enum SkuMode {Both = 1,Cart = 2,Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Cart)
// 打開SKU彈窗修改按鈕模式
const openSkuPopup = (val: SkuMode) => {// 顯示SKU彈窗isShowSku.value = true// 修改按鈕模式mode.value = val
}
</script><template><!-- SKU彈窗組件 --><vk-data-goods-sku-popupv-model="isShowSku":localdata="localdata":mode="mode"add-cart-background-color="#FFA868"buy-now-background-color="#27BA9B"/><!-- 顯示兩個按鈕 --><view @tap="openSkuPopup(SkuMode.Both)" class="item arrow">請選擇商品規格</view><!-- 顯示一個按鈕 --><view @tap="openSkuPopup(SkuMode.Cart)" class="addcart"> 加入購物車 </view><view @tap="openSkuPopup(SkuMode.Buy)" class="payment"> 立即購買 </view>
</template>
渲染被選中的值
-
通過
ref
獲取組件實例。 -
通過
computed
計算出被選中的值,渲染到界面中。
<script setup lang="ts">
// SKU組件實例
const skuPopupRef = ref<SkuPopupInstance>()
// 計算被選中的值
const selectArrText = computed(() => {return skuPopupRef.value?.selectArr?.join(' ').trim() || '請選擇商品規格'
})
</script><template><!-- SKU彈窗組件 --><vk-data-goods-sku-popupv-model="isShowSku":localdata="localdata":mode="mode"add-cart-background-color="#FFA868"buy-now-background-color="#27BA9B"ref="skuPopupRef":actived-style="{color: '#27BA9B',borderColor: '#27BA9B',backgroundColor: '#E9F8F5',}"/><!-- 操作面板 --><view class="action"><view @tap="openSkuPopup(SkuMode.Both)" class="item arrow"><text class="label">選擇</text><text class="text ellipsis"> {{ selectArrText }} </text></view></view>
</template>
至此,已經完成 SKU
組件的交互,接下來進入到購物車模塊,并實現加入購物車功能。
10.小兔鮮兒 - 購物車模塊
完成加入購物車,購物車列表交互,計算結算金額等業務。
加入購物車
在商品詳情頁把 選中規格后的商品(SKU) 加入購物車。
接口相關
接口詳情
接口地址:/member/cart
請求方式:POST
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
skuId | 是 | 無 | 商品庫存單位 |
count | 是 | 無 | 購買商品數量 |
接口封裝
src/services/cart.ts
import { http } from '@/utils/http'
/*** 加入購物車* @param data 請求體參數*/
export const postMemberCartAPI = (data: { skuId: string; count: number }) => {return http({method: 'POST',url: '/member/cart',data,})
}
參考代碼
通過 SKU
組件提供的 add-cart
事件,獲取加入購物車時所需的參數。
<script setup lang="ts">
// 加入購物車事件
const onAddCart = async (ev: SkuPopupEvent) => {await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })uni.showToast({ title: '添加成功' })isShowSku.value = false
}
</script><template><!-- SKU彈窗組件 --><vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" @add-cart="onAddCart" />
</template>
購物車列表
購物車列表需要訪問后才能登錄。
靜態結構
src/pages/cart/cart.vue
<script setup lang="ts">
//
</script><template><scroll-view scroll-y class="scroll-view"><!-- 已登錄: 顯示購物車 --><template v-if="true"><!-- 購物車列表 --><view class="cart-list" v-if="true"><!-- 優惠提示 --><view class="tips"><text class="label">滿減</text><text class="desc">滿1件, 即可享受9折優惠</text></view><!-- 滑動操作分區 --><uni-swipe-action><!-- 滑動操作項 --><uni-swipe-action-item v-for="item in 2" :key="item" class="cart-swipe"><!-- 商品信息 --><view class="goods"><!-- 選中狀態 --><text class="checkbox" :class="{ checked: true }"></text><navigator:url="`/pages/goods/goods?id=1435025`"hover-class="none"class="navigator"><imagemode="aspectFill"class="picture"src="https://yanxuan-item.nosdn.127.net/da7143e0103304f0f3230715003181ee.jpg"></image><view class="meta"><view class="name ellipsis">人手必備,兒童輕薄透氣防蚊褲73-140cm</view><view class="attrsText ellipsis">黃色小象 140cm</view><view class="price">69.00</view></view></navigator><!-- 商品數量 --><view class="count"><text class="text">-</text><input class="input" type="number" value="1" /><text class="text">+</text></view></view><!-- 右側刪除按鈕 --><template #right><view class="cart-swipe-right"><button class="button delete-button">刪除</button></view></template></uni-swipe-action-item></uni-swipe-action></view><!-- 購物車空狀態 --><view class="cart-blank" v-else><image src="/static/images/blank_cart.png" class="image" /><text class="text">購物車還是空的,快來挑選好貨吧</text><navigator open-type="switchTab" url="/pages/index/index" hover-class="none"><button class="button">去首頁看看</button></navigator></view><!-- 吸底工具欄 --><view class="toolbar"><text class="all" :class="{ checked: true }">全選</text><text class="text">合計:</text><text class="amount">100</text><view class="button-grounp"><view class="button payment-button" :class="{ disabled: true }"> 去結算(10) </view></view></view></template><!-- 未登錄: 提示登錄 --><view class="login-blank" v-else><text class="text">登錄后可查看購物車中的商品</text><navigator url="/pages/login/login" hover-class="none"><button class="button">去登錄</button></navigator></view><!-- 猜你喜歡 --><XtxGuess ref="guessRef"></XtxGuess><!-- 底部占位空盒子 --><view class="toolbar-height"></view></scroll-view>
</template><style lang="scss">
// 根元素
:host {height: 100vh;display: flex;flex-direction: column;overflow: hidden;background-color: #f7f7f8;
}// 滾動容器
.scroll-view {flex: 1;
}// 購物車列表
.cart-list {padding: 0 20rpx;// 優惠提示.tips {display: flex;align-items: center;line-height: 1;margin: 30rpx 10rpx;font-size: 26rpx;color: #666;.label {color: #fff;padding: 7rpx 15rpx 5rpx;border-radius: 4rpx;font-size: 24rpx;background-color: #27ba9b;margin-right: 10rpx;}}// 購物車商品.goods {display: flex;padding: 20rpx 20rpx 20rpx 80rpx;border-radius: 10rpx;background-color: #fff;position: relative;.navigator {display: flex;}.checkbox {position: absolute;top: 0;left: 0;display: flex;align-items: center;justify-content: center;width: 80rpx;height: 100%;&::before {content: '\e6cd';font-family: 'erabbit' !important;font-size: 40rpx;color: #444;}&.checked::before {content: '\e6cc';color: #27ba9b;}}.picture {width: 170rpx;height: 170rpx;}.meta {flex: 1;display: flex;flex-direction: column;justify-content: space-between;margin-left: 20rpx;}.name {height: 72rpx;font-size: 26rpx;color: #444;}.attrsText {line-height: 1.8;padding: 0 15rpx;font-size: 24rpx;align-self: flex-start;border-radius: 4rpx;color: #888;background-color: #f7f7f8;}.price {line-height: 1;font-size: 26rpx;color: #444;margin-bottom: 2rpx;color: #cf4444;&::before {content: '¥';font-size: 80%;}}// 商品數量.count {position: absolute;bottom: 20rpx;right: 5rpx;display: flex;justify-content: space-between;align-items: center;width: 220rpx;height: 48rpx;.text {height: 100%;padding: 0 20rpx;font-size: 32rpx;color: #444;}.input {height: 100%;text-align: center;border-radius: 4rpx;font-size: 24rpx;color: #444;background-color: #f6f6f6;}}}.cart-swipe {display: block;margin: 20rpx 0;}.cart-swipe-right {display: flex;height: 100%;.button {display: flex;justify-content: center;align-items: center;width: 50px;padding: 6px;line-height: 1.5;color: #fff;font-size: 26rpx;border-radius: 0;}.delete-button {background-color: #cf4444;}}
}// 空狀態
.cart-blank,
.login-blank {display: flex;justify-content: center;align-items: center;flex-direction: column;height: 60vh;.image {width: 400rpx;height: 281rpx;}.text {color: #444;font-size: 26rpx;margin: 20rpx 0;}.button {width: 240rpx !important;height: 60rpx;line-height: 60rpx;margin-top: 20rpx;font-size: 26rpx;border-radius: 60rpx;color: #fff;background-color: #27ba9b;}
}// 吸底工具欄
.toolbar {position: fixed;left: 0;right: 0;bottom: var(--window-bottom);z-index: 1;height: 100rpx;padding: 0 20rpx;display: flex;align-items: center;border-top: 1rpx solid #ededed;border-bottom: 1rpx solid #ededed;background-color: #fff;box-sizing: content-box;.all {margin-left: 25rpx;font-size: 14px;color: #444;display: flex;align-items: center;}.all::before {font-family: 'erabbit' !important;content: '\e6cd';font-size: 40rpx;margin-right: 8rpx;}.checked::before {content: '\e6cc';color: #27ba9b;}.text {margin-right: 8rpx;margin-left: 32rpx;color: #444;font-size: 14px;}.amount {font-size: 20px;color: #cf4444;.decimal {font-size: 12px;}&::before {content: '¥';font-size: 12px;}}.button-grounp {margin-left: auto;display: flex;justify-content: space-between;text-align: center;line-height: 72rpx;font-size: 13px;color: #fff;.button {width: 240rpx;margin: 0 10rpx;border-radius: 72rpx;}.payment-button {background-color: #27ba9b;&.disabled {opacity: 0.6;}}}
}
// 底部占位空盒子
.toolbar-height {height: 100rpx;
}
</style>
登錄狀態
已登錄顯示購物車列表,否則應引導用戶去登錄。
<script setup lang="ts">
import { useMemberStore } from '@/stores'// 獲取會員Store
const memberStore = useMemberStore()
</script><template><scroll-view scroll-y class="scroll-view"><!-- 已登錄: 顯示購物車 --><template v-if="memberStore.profile"><!-- 購物車列表 --></template><!-- 未登錄: 提示登錄 --><view class="login-blank" v-else><text class="text">登錄后可查看購物車中的商品</text><navigator url="/pages/login/login" hover-class="none"><button class="button">去登錄</button></navigator></view></scroll-view>
</template>
列表渲染
調用接口獲取當前登錄用戶購物車中的商品列表。
接口信息
接口地址:/member/cart
請求方式:GET
登錄權限: 是
請求參數:無
接口封裝
src/services/cart.ts
/*** 獲取購物車列表*/
export const getMemberCartAPI = () => {return http<CartItem[]>({method: 'GET',url: '/member/cart',})
}
類型聲明
src/services/cart.d.ts
/** 購物車類型 */
export type CartItem = {/** 商品 ID */id: string/** SKU ID */skuId: string/** 商品名稱 */name: string/** 圖片 */picture: string/** 數量 */count: number/** 加入時價格 */price: number/** 當前的價格 */nowPrice: number/** 庫存 */stock: number/** 是否選中 */selected: boolean/** 屬性文字 */attrsText: string/** 是否為有效商品 */isEffective: boolean
}
參考代碼
在頁面初始化的時候判斷用戶是否已登錄,已登錄獲取購物車列表。
<script setup lang="ts">
import { getMemberCartAPI } from '@/services/cart'
import { useMemberStore } from '@/stores'
import type { CartItem } from '@/types/cart'
import { onShow } from '@dcloudio/uni-app'
import { ref } from 'vue'// 獲取會員Store
const memberStore = useMemberStore()// 獲取購物車數據
const cartList = ref<CartItem[]>([])
const getMemberCartData = async () => {const res = await getMemberCartAPI()cartList.value = res.result
}// 初始化調用: 頁面顯示觸發
onShow(() => {// 用戶已登錄才允許調用if (memberStore.profile) {getMemberCartData()}
})
</script>
溫馨提示:自行完成猜你喜歡分頁加載。
刪除購物車
通過側滑刪除購物車的商品,使用 uni-swipe-action 組件實現。
接口相關
接口詳情
接口地址:/member/cart
請求方式:DELETE
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 類型 | 備注 |
---|---|---|---|
ids | 是 | string[] | SKUID 集合 |
接口封裝
/*** 刪除/清空購物車單品* @param data 請求體參數 ids SKUID 集合*/
export const deleteMemberCartAPI = (data: { ids: string[] }) => {return http({method: 'DELETE',url: '/member/cart',data,})
}
參考代碼
<script setup lang="ts">
// 點擊刪除按鈕
const onDeleteCart = (skuId: string) => {// 彈窗二次確認uni.showModal({content: '是否刪除',success: async (res) => {if (res.confirm) {// 后端刪除單品await deleteMemberCartAPI({ ids: [skuId] })// 重新獲取列表getMemberCartData()}},})
}
</script><template><!-- 右側刪除按鈕 --><template #right><view class="cart-swipe-right"><button @tap="onDeleteCart(item.skuId)" class="button delete-button">刪除</button></view></template>
</template>
修改商品信息
修改購買數量,修改選中狀態。
接口相關
接口詳情
接口地址:/member/cart/:id
請求方式:PUT
登錄權限: 是
請求參數:
路徑參數
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
id | 是 | 無 | 商品的 skuId |
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
selected | 非必須 | 是否選中 | |
count | 非必須 | 數量 |
接口封裝
/*** 修改購物車單品* @param skuId SKUID* @param data selected 選中狀態 count 商品數量*/
export const putMemberCartBySkuIdAPI = (skuId: string,data: { selected?: boolean; count?: number },
) => {return http({method: 'PUT',url: `/member/cart/${skuId}`,data,})
}
修改商品數量
復用 SKU
插件中的 步進器組件 修改商品數量,補充類型聲明文件讓組件類型更安全。
聲明文件
import { Component } from '@uni-helper/uni-app-types'/** 步進器 */
export type InputNumberBox = Component<InputNumberBoxProps>/** 步進器實例 */
export type InputNumberBoxInstance = InstanceType<InputNumberBox>/** 步進器屬性 */
export type InputNumberBoxProps = {/** 輸入框初始值(默認1) */modelValue: number/** 用戶可輸入的最小值(默認0) */min: number/** 用戶可輸入的最大值(默認99999) */max: number/** 步長,每次加或減的值(默認1) */step: number/** 是否禁用操作,包括輸入框,加減按鈕 */disabled: boolean/** 輸入框寬度,單位rpx(默認80) */inputWidth: string | number/** 輸入框和按鈕的高度,單位rpx(默認50) */inputHeight: string | number/** 輸入框和按鈕的背景顏色(默認#F2F3F5) */bgColor: string/** 步進器標識符 */index: string/** 輸入框內容發生變化時觸發 */onChange: (event: InputNumberBoxEvent) => void/** 輸入框失去焦點時觸發 */onBlur: (event: InputNumberBoxEvent) => void/** 點擊增加按鈕時觸發 */onPlus: (event: InputNumberBoxEvent) => void/** 點擊減少按鈕時觸發 */onMinus: (event: InputNumberBoxEvent) => void
}/** 步進器事件對象 */
export type InputNumberBoxEvent = {/** 輸入框當前值 */value: number/** 步進器標識符 */index: string
}/** 全局組件類型聲明 */
declare module 'vue' {export interface GlobalComponents {'vk-data-input-number-box': InputNumberBox}
}
參考代碼
<script setup lang="ts">
import type { InputNumberBoxEvent } from '@/components/vk-data-input-number-box/vk-data-input-number-box'// 修改商品數量
const onChangeCount = (ev: InputNumberBoxEvent) => {putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
</script><template><!-- 商品數量 --><view class="count"><vk-data-input-number-boxv-model="item.count":min="1":max="item.stock":index="item.skuId"@change="onChangeCount"/></view>
</template>
修改商品選中/全選
修改單個商品選中會影響全選狀態,修改全選狀態同理。
全選商品
接口地址:/member/cart/selected
請求方式:PUT
登錄權限: 是
請求參數:
路徑參數
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
id | 是 | 無 | skuId |
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
selected | 是 | 無 | 是否全選 |
ids | 否 | 無 | 商品集合 |
接口封裝
/*** 購物車全選/取消全選* @param data selected 是否選中*/
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {return http({method: 'PUT',url: '/member/cart/selected',data,})
}
參考代碼
<script setup lang="ts">
// 修改選中狀態-單品修改
const onChangeSelected = (item: CartItem) => {// 前端數據更新-是否選中取反item.selected = !item.selected// 后端數據更新putMemberCartBySkuIdAPI(item.skuId, { selected: item.selected })
}// 計算全選狀態
const isSelectedAll = computed(() => {return cartList.value.length && cartList.value.every((v) => v.selected)
})// 修改選中狀態-全選修改
const onChangeSelectedAll = () => {// 全選狀態取反const _isSelectedAll = !isSelectedAll.value// 前端數據更新cartList.value.forEach((item) => {item.selected = _isSelectedAll})// 后端數據更新putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
</script><template><!-- 商品信息 --><view class="goods"><!-- 選中狀態 --><text @tap="onChangeSelected(item)" class="checkbox" :class="{ checked: item.selected }"></text></view><!-- 吸底工具欄 --><view class="toolbar"><text @tap="onChangeSelectedAll" class="all" :class="{ checked: isSelectedAll }">全選</text></view>
</template>
底部結算信息
計算總錢數(總金額)
計算并展示購物車中選中商品所要支付的總金額,在用戶切換商品選中狀態和改變購數量后總的金額也要相應的進行重新計算,要實現這個功能我們仍然借助計算屬性來實現:
<script setup lang="ts">
// 計算選中單品列表
const selectedCartList = computed(() => {return cartList.value.filter((v) => v.selected)
})// 計算選中總件數
const selectedCartListCount = computed(() => {return selectedCartList.value.reduce((sum, item) => sum + item.count, 0)
})// 計算選中總金額
const selectedCartListMoney = computed(() => {return selectedCartList.value.reduce((sum, item) => sum + item.count * item.nowPrice, 0).toFixed(2)
})// 結算按鈕
const gotoPayment = () => {if (selectedCartListCount.value === 0) {return uni.showToast({icon: 'none',title: '請選擇商品',})}// 跳轉到結算頁uni.navigateTo({ url: '/pagesOrder/create/create' })
}
</script>
帶返回按鈕的購物車
為了解決小程序 tabBar 頁面限制 導致無法返回上一頁的問題,將購物車業務獨立為組件,使其既可從底部 tabBar 訪問,又可在商品詳情頁中跳轉并返回。
這樣就需要 兩個購物車頁面 實現該功能,其中一個頁面為 tabBar 頁,另一個為普通頁。
目錄結構如下:
pages/cart
├── components
│ └── CartMain.vue ...................................... 購物車業務組件
├── cart2.vue ............................................. 普通頁
└── cart.vue ............................................ TabBar頁
把原本的購物車業務獨立封裝成組件,在兩個購物車頁面分別引入即可。
::: tip 溫馨提示
- 小程序 跳轉到 tabBar 頁面 時,會關閉其他所有非 tabBar 頁面。
- 小程序的 tabBar 頁沒有后退按鈕,可通過 getCurrentPages() 驗證,結果僅有一個頁面,意味著歷史記錄被清空了。
- 有歷史記錄的普通頁才顯示后退按鈕。
:::
11.小兔鮮兒 - 訂單模塊
訂單模塊頁面較多,建議用新的分包文件夾獨立管理訂單模塊頁面:填寫訂單頁,支付訂單頁,訂單詳情頁,訂單列表頁。
填寫訂單頁
小兔鮮兒項目有三種方式可以生成訂單信息,分別是:購物車結算、立即購買、再次購買。
靜態結構
<script setup lang="ts">
import { computed, ref } from 'vue'// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()
// 訂單備注
const buyerMessage = ref('')
// 配送時間
const deliveryList = ref([{ type: 1, text: '時間不限 (周一至周日)' },{ type: 2, text: '工作日送 (周一至周五)' },{ type: 3, text: '周末配送 (周六至周日)' },
])
// 當前配送時間下標
const activeIndex = ref(0)
// 當前配送時間
const activeDelivery = computed(() => deliveryList.value[activeIndex.value])
// 修改配送時間
const onChangeDelivery: UniHelper.SelectorPickerOnChange = (ev) => {activeIndex.value = ev.detail.value
}
</script><template><scroll-view scroll-y class="viewport"><!-- 收貨地址 --><navigatorv-if="false"class="shipment"hover-class="none"url="/pagesMember/address/address?from=order"><view class="user"> 張三 13333333333 </view><view class="address"> 廣東省 廣州市 天河區 黑馬程序員3 </view><text class="icon icon-right"></text></navigator><navigatorv-elseclass="shipment"hover-class="none"url="/pagesMember/address/address?from=order"><view class="address"> 請選擇收貨地址 </view><text class="icon icon-right"></text></navigator><!-- 商品信息 --><view class="goods"><navigatorv-for="item in 2":key="item":url="`/pages/goods/goods?id=1`"class="item"hover-class="none"><imageclass="picture"src="https://yanxuan-item.nosdn.127.net/c07edde1047fa1bd0b795bed136c2bb2.jpg"/><view class="meta"><view class="name ellipsis"> ins風小碎花泡泡袖襯110-160cm </view><view class="attrs">藏青小花 130</view><view class="prices"><view class="pay-price symbol">99.00</view><view class="price symbol">99.00</view></view><view class="count">x5</view></view></navigator></view><!-- 配送及支付方式 --><view class="related"><view class="item"><text class="text">配送時間</text><picker :range="deliveryList" range-key="text" @change="onChangeDelivery"><view class="icon-fonts picker">{{ activeDelivery.text }}</view></picker></view><view class="item"><text class="text">訂單備注</text><inputclass="input":cursor-spacing="30"placeholder="選題,建議留言前先與商家溝通確認"v-model="buyerMessage"/></view></view><!-- 支付金額 --><view class="settlement"><view class="item"><text class="text">商品總價: </text><text class="number symbol">495.00</text></view><view class="item"><text class="text">運費: </text><text class="number symbol">5.00</text></view></view></scroll-view><!-- 吸底工具欄 --><view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }"><view class="total-pay symbol"><text class="number">99.00</text></view><view class="button" :class="{ disabled: true }"> 提交訂單 </view></view>
</template><style lang="scss">
page {display: flex;flex-direction: column;height: 100%;overflow: hidden;background-color: #f4f4f4;
}.symbol::before {content: '¥';font-size: 80%;margin-right: 5rpx;
}.shipment {margin: 20rpx;padding: 30rpx 30rpx 30rpx 84rpx;font-size: 26rpx;border-radius: 10rpx;background: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png)20rpx center / 50rpx no-repeat #fff;position: relative;.icon {font-size: 36rpx;color: #333;transform: translateY(-50%);position: absolute;top: 50%;right: 20rpx;}.user {color: #333;margin-bottom: 5rpx;}.address {color: #666;}
}.goods {margin: 20rpx;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;.item {display: flex;padding: 30rpx 0;border-top: 1rpx solid #eee;&:first-child {border-top: none;}.picture {width: 170rpx;height: 170rpx;border-radius: 10rpx;margin-right: 20rpx;}.meta {flex: 1;display: flex;flex-direction: column;justify-content: center;position: relative;}.name {height: 80rpx;font-size: 26rpx;color: #444;}.attrs {line-height: 1.8;padding: 0 15rpx;margin-top: 6rpx;font-size: 24rpx;align-self: flex-start;border-radius: 4rpx;color: #888;background-color: #f7f7f8;}.prices {display: flex;align-items: baseline;margin-top: 6rpx;font-size: 28rpx;.pay-price {margin-right: 10rpx;color: #cf4444;}.price {font-size: 24rpx;color: #999;text-decoration: line-through;}}.count {position: absolute;bottom: 0;right: 0;font-size: 26rpx;color: #444;}}
}.related {margin: 20rpx;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;.item {display: flex;justify-content: space-between;align-items: center;min-height: 80rpx;font-size: 26rpx;color: #333;}.input {flex: 1;text-align: right;margin: 20rpx 0;padding-right: 20rpx;font-size: 26rpx;color: #999;}.item .text {width: 125rpx;}.picker {color: #666;}.picker::after {content: '\e6c2';}
}/* 結算清單 */
.settlement {margin: 20rpx;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;.item {display: flex;align-items: center;justify-content: space-between;height: 80rpx;font-size: 26rpx;color: #333;}.danger {color: #cf4444;}
}/* 吸底工具欄 */
.toolbar {position: fixed;left: 0;right: 0;bottom: calc(var(--window-bottom));z-index: 1;background-color: #fff;height: 100rpx;padding: 0 20rpx;border-top: 1rpx solid #eaeaea;display: flex;justify-content: space-between;align-items: center;box-sizing: content-box;.total-pay {font-size: 40rpx;color: #cf4444;.decimal {font-size: 75%;}}.button {width: 220rpx;text-align: center;line-height: 72rpx;font-size: 26rpx;color: #fff;border-radius: 72rpx;background-color: #27ba9b;}.disabled {opacity: 0.6;}
}
</style>
購物車結算
在購物車點擊去結算后,進入填寫訂單頁,用戶可以選擇訂單的收貨地址或補充訂單信息。
接口詳情
接口地址:/member/order/pre
請求方式:GET
登錄權限: 是
請求參數:無
接口封裝
src/services/order.ts
import type { OrderPreResult } from '@/types/order'
import { http } from '@/utils/http'
/*** 填寫訂單-獲取預付訂單*/
export const getMemberOrderPreAPI = () => {return http<OrderPreResult>({method: 'GET',url: '/member/order/pre',})
}
類型聲明
src/services/order.d.ts
import type { AddressItem } from './address'/** 獲取預付訂單 返回信息 */
export type OrderPreResult = {/** 商品集合 [ 商品信息 ] */goods: OrderPreGoods[]/** 結算信息 */summary: {/** 商品總價 */totalPrice: number/** 郵費 */postFee: number/** 應付金額 */totalPayPrice: number}/** 用戶地址列表 [ 地址信息 ] */userAddresses: AddressItem[]
}/** 商品信息 */
export type OrderPreGoods = {/** 屬性文字,例如“顏色:瓷白色 尺寸:8寸” */attrsText: string/** 數量 */count: number/** id */id: string/** 商品名稱 */name: string/** 實付單價 */payPrice: string/** 圖片 */picture: string/** 原單價 */price: string/** SKUID */skuId: string/** 實付價格小計 */totalPayPrice: string/** 小計總價 */totalPrice: string
}
將后端返回的預付訂單數據,渲染到頁面中。
立即購買
從商品詳情頁的 SKU
組件直接點擊【立即購買按鈕】跳轉到填寫訂單頁,需要傳遞頁面參數。
接口詳情
接口地址:/member/order/pre/now
請求方式:GET
登錄權限: 是
請求參數:
Query
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
skuId | 是 | 無 | 商品庫存 ID |
count | 是 | 無 | 商品數量 |
addressId | 否 | 無 | 收貨地址 ID |
接口封裝
/*** 填寫訂單-獲取立即購買訂單*/
export const getMemberOrderPreNowAPI = (data: {skuId: stringcount: stringaddressId?: string
}) => {return http<OrderPreResult>({method: 'GET',url: '/member/order/pre/now',data,})
}
頁面傳參
從商品詳情頁的【立即購買事件】中收集兩個必要參數,跳轉填寫訂單頁并傳遞頁面參數。
商品詳情頁
<script setup lang="ts">
// 立即購買
const onBuyNow = (ev: SkuPopupEvent) => {uni.navigateTo({ url: `/pagesOrder/create/create?skuId=${ev._id}&count=${ev.buy_num}` })
}
</script><template><!-- SKU彈窗組件 --><vk-data-goods-sku-popupv-model="isShowSku":localdata="localdata"@add-cart="onAddCart"@buy-now="onBuyNow"/>
</template>
填寫訂單頁
<script setup lang="ts">
import { getMemberOrderPreAPI, getMemberOrderPreNowAPI } from '@/services/order'// 頁面參數
const query = defineProps<{skuId?: stringcount?: string
}>()// 獲取訂單信息
const orderPre = ref<OrderPreResult>()
const getMemberOrderPreData = async () => {// 是否有立即購買參數if (query.count && query.skuId) {// 調用立即購買 APIconst res = await getMemberOrderPreNowAPI({count: query.count,skuId: query.skuId,})orderPre.value = res.result} else {// 調用預付訂單 APIconst res = await getMemberOrderPreAPI()orderPre.value = res.result}
}
</script>
選擇收貨地址
收貨地址在地址管理頁面中選擇,為了更好管理選中收貨地址,創建獨立 Store 維護。
地址 Store
src/stores/modules/address.ts
import type { AddressItem } from '@/types/address'
import { defineStore } from 'pinia'
import { ref } from 'vue'export const useAddressStore = defineStore('address', () => {const selectedAddress = ref<AddressItem>()const changeSelectedAddress = (val: AddressItem) => {selectedAddress.value = val}return { selectedAddress, changeSelectedAddress }
})
地址管理頁
修改選中收貨地址,<navigator>
組件需要阻止事件冒泡。
pagesMember/address/address.vue
<script setup lang="ts">
// 修改收貨地址
const onChangeAddress = (item: AddressItem) => {// 修改選中收貨地址const addressStore = useAddressStore()addressStore.changeSelectedAddress(item)// 返回上一頁uni.navigateBack()
}
</script><template><!-- 收貨地址項 --><uni-swipe-action-item class="item" v-for="item in addressList" :key="item.id"><view class="item-content" @tap="onChangeAddress(item)"><view class="user">{{ item.receiver }}<text class="contact">{{ item.contact }}</text><text v-if="item.isDefault" class="badge">默認</text></view><view class="locate">{{ item.fullLocation }} {{ item.address }}</view><navigatorclass="edit"hover-class="none":url="`/pagesMember/address-form/address-form?id=${item.id}`"@tap.stop="() => {}">修改</navigator></view></uni-swipe-action-item>
</template>
訂單填寫頁
<script setup lang="ts">
import { useAddressStore } from '@/stores/modules/address'
import { computed } from 'vue'const addressStore = useAddressStore()
// 收貨地址
const selecteAddress = computed(() => {return addressStore.selectedAddress || orderPre.value?.userAddresses.find((v) => v.isDefault)
})
</script><template><!-- 收貨地址 --><navigatorv-if="selecteAddress"class="shipment"hover-class="none"url="/pagesMember/address/address?from=order"><view class="user"> {{ selecteAddress.receiver }} {{ selecteAddress.contact }} </view><view class="address"> {{ selecteAddress.fullLocation }} {{ selecteAddress.address }} </view><text class="icon icon-right"></text></navigator>
</template>
提交訂單
收集填寫訂單頁的數據,點擊頁面底部的提交訂單按鈕,創建一個新的訂單。
接口詳情
接口地址:/member/order
請求方式:POST
登錄權限: 是
請求參數:
Body
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
addressId | 是 | 無 | 收貨地址 ID |
goods | 是 | 無 | 訂單商品 |
deliveryTimeType | 是 | 無 | 配送時間 |
buyerMessage | 是 | 無 | 買家留言 |
payType | 是 | 無 | 支付方式(小程序中固定在線支付) |
payChannel | 是 | 無 | 支付渠道(小程序中固定微信支付) |
接口封裝
/*** 提交訂單* @param data 請求參數*/
export const postMemberOrderAPI = (data: OrderCreateParams) => {return http<{ id: string }>({method: 'POST',url: '/member/order',data,})
}
類型聲明
/** 提交訂單 請求參數 */
export type OrderCreateParams = {/** 所選地址Id */addressId: string/** 配送時間類型,1為不限,2為工作日,3為雙休或假日 */deliveryTimeType: number/** 訂單備注 */buyerMessage: string/** 商品集合[ 商品信息 ] */goods: {/** 數量 */count: number/** skuId */skuId: string}[]/** 支付渠道:支付渠道,1支付寶、2微信--支付方式為在線支付時,傳值,為貨到付款時,不傳值 */payChannel: 1 | 2/** 支付方式,1為在線支付,2為貨到付款 */payType: 1 | 2
}/** 提交訂單 返回信息 */
export type OrderCreateResult = {/** 訂單Id */id: string
}
參考代碼
點擊提交訂單按鈕實現創建訂單,訂單創建成功后,跳轉到訂單詳情并傳遞訂單 id。
<script setup lang="ts">
// 提交訂單
const onOrderSubmit = async () => {// 沒有收貨地址提醒if (!selecteAddress.value?.id) {return uni.showToast({ icon: 'none', title: '請選擇收貨地址' })}// 發送請求const res = await postMemberOrderAPI({addressId: selecteAddress.value?.id,buyerMessage: buyerMessage.value,deliveryTimeType: activeDelivery.value.type,goods: orderPre.value!.goods.map((v) => ({ count: v.count, skuId: v.skuId })),payChannel: 2,payType: 1,})// 關閉當前頁面,跳轉到訂單詳情,傳遞訂單iduni.redirectTo({ url: `/pagesOrder/detail/detail?id=${res.result.id}` })
}
</script><template><view class="button" :class="{ disabled: !selecteAddress?.id }" @tap="onOrderSubmit">提交訂單</view>
</template>
訂單提交成功后,接下來進入到訂單詳情頁。
訂單詳情頁
需要展示多種訂單狀態 并實現不同訂單狀態對應的業務。
靜態結構
已完成通過頁面參數獲取到訂單 id 等基礎業務。
<script setup lang="ts">
import { useGuessList } from '@/composables'
import { ref } from 'vue'// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()
// 猜你喜歡
const { guessRef, onScrolltolower } = useGuessList()
// 彈出層組件
const popup = ref<UniHelper.UniPopupInstance>()
// 取消原因列表
const reasonList = ref(['商品無貨','不想要了','商品信息填錯了','地址信息填寫錯誤','商品降價','其它',
])
// 訂單取消原因
const reason = ref('')
// 復制內容
const onCopy = (id: string) => {// 設置系統剪貼板的內容uni.setClipboardData({ data: id })
}
// 獲取頁面參數
const query = defineProps<{id: string
}>()
</script><template><!-- 自定義導航欄: 默認透明不可見, scroll-view 滾動到 50 時展示 --><view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"><view class="wrap"><navigator v-if="true" open-type="navigateBack" class="back icon-left"></navigator><navigator v-else url="/pages/index/index" open-type="switchTab" class="back icon-home"></navigator><view class="title">訂單詳情</view></view></view><scroll-view scroll-y class="viewport" id="scroller" @scrolltolower="onScrolltolower"><template v-if="true"><!-- 訂單狀態 --><view class="overview" :style="{ paddingTop: safeAreaInsets!.top + 20 + 'px' }"><!-- 待付款狀態:展示去支付按鈕和倒計時 --><template v-if="true"><view class="status icon-clock">等待付款</view><view class="tips"><text class="money">應付金額: ¥ 99.00</text><text class="time">支付剩余</text>00 時 29 分 59 秒</view><view class="button">去支付</view></template><!-- 其他訂單狀態:展示再次購買按鈕 --><template v-else><!-- 訂單狀態文字 --><view class="status"> 待付款 </view><view class="button-group"><navigatorclass="button":url="`/pagesOrder/create/create?orderId=${query.id}`"hover-class="none">再次購買</navigator><!-- 待發貨狀態:模擬發貨,開發期間使用,用于修改訂單狀態為已發貨 --><view v-if="false" class="button"> 模擬發貨 </view></view></template></view><!-- 配送狀態 --><view class="shipment"><!-- 訂單物流信息 --><view v-for="item in 1" :key="item" class="item"><view class="message">您已在廣州市天河區黑馬程序員完成取件,感謝使用菜鳥驛站,期待再次為您服務。</view><view class="date"> 2023-04-14 13:14:20 </view></view><!-- 用戶收貨地址 --><view class="locate"><view class="user"> 張三 13333333333 </view><view class="address"> 廣東省 廣州市 天河區 黑馬程序員 </view></view></view><!-- 商品信息 --><view class="goods"><view class="item"><navigatorclass="navigator"v-for="item in 2":key="item":url="`/pages/goods/goods?id=${item}`"hover-class="none"><imageclass="cover"src="https://yanxuan-item.nosdn.127.net/c07edde1047fa1bd0b795bed136c2bb2.jpg"></image><view class="meta"><view class="name ellipsis">ins風小碎花泡泡袖襯110-160cm</view><view class="type">藏青小花, 130</view><view class="price"><view class="actual"><text class="symbol">¥</text><text>99.00</text></view></view><view class="quantity">x1</view></view></navigator><!-- 待評價狀態:展示按鈕 --><view class="action" v-if="true"><view class="button primary">申請售后</view><navigator url="" class="button"> 去評價 </navigator></view></view><!-- 合計 --><view class="total"><view class="row"><view class="text">商品總價: </view><view class="symbol">99.00</view></view><view class="row"><view class="text">運費: </view><view class="symbol">10.00</view></view><view class="row"><view class="text">應付金額: </view><view class="symbol primary">109.00</view></view></view></view><!-- 訂單信息 --><view class="detail"><view class="title">訂單信息</view><view class="row"><view class="item">訂單編號: {{ query.id }} <text class="copy" @tap="onCopy(query.id)">復制</text></view><view class="item">下單時間: 2023-04-14 13:14:20</view></view></view><!-- 猜你喜歡 --><XtxGuess ref="guessRef" /><!-- 底部操作欄 --><view class="toolbar-height" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }"></view><view class="toolbar" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }"><!-- 待付款狀態:展示支付按鈕 --><template v-if="true"><view class="button primary"> 去支付 </view><view class="button" @tap="popup?.open?.()"> 取消訂單 </view></template><!-- 其他訂單狀態:按需展示按鈕 --><template v-else><navigatorclass="button secondary":url="`/pagesOrder/create/create?orderId=${query.id}`"hover-class="none">再次購買</navigator><!-- 待收貨狀態: 展示確認收貨 --><view class="button primary"> 確認收貨 </view><!-- 待評價狀態: 展示去評價 --><view class="button"> 去評價 </view><!-- 待評價/已完成/已取消 狀態: 展示刪除訂單 --><view class="button delete"> 刪除訂單 </view></template></view></template><template v-else><!-- 骨架屏組件 --><PageSkeleton /></template></scroll-view><!-- 取消訂單彈窗 --><uni-popup ref="popup" type="bottom" background-color="#fff"><view class="popup-root"><view class="title">訂單取消</view><view class="description"><view class="tips">請選擇取消訂單的原因:</view><view class="cell" v-for="item in reasonList" :key="item" @tap="reason = item"><text class="text">{{ item }}</text><text class="icon" :class="{ checked: item === reason }"></text></view></view><view class="footer"><view class="button" @tap="popup?.close?.()">取消</view><view class="button primary">確認</view></view></view></uni-popup>
</template><style lang="scss">
page {display: flex;flex-direction: column;height: 100%;overflow: hidden;
}.navbar {width: 750rpx;color: #000;position: fixed;top: 0;left: 0;z-index: 9;/* background-color: #f8f8f8; */background-color: transparent;.wrap {position: relative;.title {height: 44px;display: flex;justify-content: center;align-items: center;font-size: 32rpx;/* color: #000; */color: transparent;}.back {position: absolute;left: 0;height: 44px;width: 44px;font-size: 44rpx;display: flex;align-items: center;justify-content: center;/* color: #000; */color: #fff;}}
}.viewport {background-color: #f7f7f8;
}.overview {display: flex;flex-direction: column;align-items: center;line-height: 1;padding-bottom: 30rpx;color: #fff;background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/order_bg.png);background-size: cover;.status {font-size: 36rpx;}.status::before {margin-right: 6rpx;font-weight: 500;}.tips {margin: 30rpx 0;display: flex;font-size: 14px;align-items: center;.money {margin-right: 30rpx;}}.button-group {margin-top: 30rpx;display: flex;justify-content: center;align-items: center;}.button {width: 260rpx;height: 64rpx;margin: 0 10rpx;text-align: center;line-height: 64rpx;font-size: 28rpx;color: #27ba9b;border-radius: 68rpx;background-color: #fff;}
}.shipment {line-height: 1.4;padding: 0 20rpx;margin: 20rpx 20rpx 0;border-radius: 10rpx;background-color: #fff;.locate,.item {min-height: 120rpx;padding: 30rpx 30rpx 25rpx 75rpx;background-size: 50rpx;background-repeat: no-repeat;background-position: 6rpx center;}.locate {background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/locate.png);.user {font-size: 26rpx;color: #444;}.address {font-size: 24rpx;color: #666;}}.item {background-image: url(https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/images/car.png);border-bottom: 1rpx solid #eee;position: relative;.message {font-size: 26rpx;color: #444;}.date {font-size: 24rpx;color: #666;}}
}.goods {margin: 20rpx 20rpx 0;padding: 0 20rpx;border-radius: 10rpx;background-color: #fff;.item {padding: 30rpx 0;border-bottom: 1rpx solid #eee;.navigator {display: flex;margin: 20rpx 0;}.cover {width: 170rpx;height: 170rpx;border-radius: 10rpx;margin-right: 20rpx;}.meta {flex: 1;display: flex;flex-direction: column;justify-content: center;position: relative;}.name {height: 80rpx;font-size: 26rpx;color: #444;}.type {line-height: 1.8;padding: 0 15rpx;margin-top: 6rpx;font-size: 24rpx;align-self: flex-start;border-radius: 4rpx;color: #888;background-color: #f7f7f8;}.price {display: flex;margin-top: 6rpx;font-size: 24rpx;}.symbol {font-size: 20rpx;}.original {color: #999;text-decoration: line-through;}.actual {margin-left: 10rpx;color: #444;}.text {font-size: 22rpx;}.quantity {position: absolute;bottom: 0;right: 0;font-size: 24rpx;color: #444;}.action {display: flex;flex-direction: row-reverse;justify-content: flex-start;padding: 30rpx 0 0;.button {width: 200rpx;height: 60rpx;text-align: center;justify-content: center;line-height: 60rpx;margin-left: 20rpx;border-radius: 60rpx;border: 1rpx solid #ccc;font-size: 26rpx;color: #444;}.primary {color: #27ba9b;border-color: #27ba9b;}}}.total {line-height: 1;font-size: 26rpx;padding: 20rpx 0;color: #666;.row {display: flex;align-items: center;justify-content: space-between;padding: 10rpx 0;}.symbol::before {content: '¥';font-size: 80%;margin-right: 3rpx;}.primary {color: #cf4444;font-size: 36rpx;}}
}.detail {line-height: 1;padding: 30rpx 20rpx 0;margin: 20rpx 20rpx 0;font-size: 26rpx;color: #666;border-radius: 10rpx;background-color: #fff;.title {font-size: 30rpx;color: #444;}.row {padding: 20rpx 0;.item {padding: 10rpx 0;display: flex;align-items: center;}.copy {border-radius: 20rpx;font-size: 20rpx;border: 1px solid #ccc;padding: 5rpx 10rpx;margin-left: 10rpx;}}
}.toolbar-height {height: 100rpx;box-sizing: content-box;
}.toolbar {position: fixed;left: 0;right: 0;bottom: calc(var(--window-bottom));z-index: 1;height: 100rpx;padding: 0 20rpx;display: flex;align-items: center;flex-direction: row-reverse;border-top: 1rpx solid #ededed;border-bottom: 1rpx solid #ededed;background-color: #fff;box-sizing: content-box;.button {display: flex;justify-content: center;align-items: center;width: 200rpx;height: 72rpx;margin-left: 15rpx;font-size: 26rpx;border-radius: 72rpx;border: 1rpx solid #ccc;color: #444;}.delete {order: 4;}.button {order: 3;}.secondary {order: 2;color: #27ba9b;border-color: #27ba9b;}.primary {order: 1;color: #fff;background-color: #27ba9b;}
}.popup-root {padding: 30rpx 30rpx 0;border-radius: 10rpx 10rpx 0 0;overflow: hidden;.title {font-size: 30rpx;text-align: center;margin-bottom: 30rpx;}.description {font-size: 28rpx;padding: 0 20rpx;.tips {color: #444;margin-bottom: 12rpx;}.cell {display: flex;justify-content: space-between;align-items: center;padding: 15rpx 0;color: #666;}.icon::before {content: '\e6cd';font-family: 'erabbit' !important;font-size: 38rpx;color: #999;}.icon.checked::before {content: '\e6cc';font-size: 38rpx;color: #27ba9b;}}.footer {display: flex;justify-content: space-between;padding: 30rpx 0 40rpx;font-size: 28rpx;color: #444;.button {flex: 1;height: 72rpx;text-align: center;line-height: 72rpx;margin: 0 20rpx;color: #444;border-radius: 72rpx;border: 1rpx solid #ccc;}.primary {color: #fff;background-color: #27ba9b;border: none;}}
}
</style>
自定義導航欄交互
- 導航欄左上角按鈕:獲取當前頁面棧,如果不能返回上一頁,按鈕變成返回首頁。
- 導航欄動畫效果:滾動驅動的動畫,根據滾動位置而不斷改變動畫的進度。
::: warning 注意事項
滾動驅動的動畫目前僅支持微信小程序端,暫不支持 H5 端、App 端,多端兼容時添加條件編譯。
:::
參考代碼
<script setup lang="ts">
// 獲取頁面棧
const pages = getCurrentPages()
// 獲取當前頁面實例,數組最后一項
const pageInstance = pages.at(-1) as any// 頁面渲染完畢,綁定動畫效果
onReady(() => {// 動畫效果,導航欄背景色pageInstance.animate('.navbar', // 選擇器[{ backgroundColor: 'transparent' }, { backgroundColor: '#f8f8f8' }], // 關鍵幀信息1000, // 動畫持續時長{scrollSource: '#scroller', // scroll-view 的選擇器startScrollOffset: 0, // 開始滾動偏移量endScrollOffset: 50, // 停止滾動偏移量timeRange: 1000, // 時間長度},)// 動畫效果,導航欄標題pageInstance.animate('.navbar .title', [{ color: 'transparent' }, { color: '#000' }], 1000, {scrollSource: '#scroller',timeRange: 1000,startScrollOffset: 0,endScrollOffset: 50,})// 動畫效果,導航欄返回按鈕pageInstance.animate('.navbar .back', [{ color: '#fff' }, { color: '#000' }], 1000, {scrollSource: '#scroller',timeRange: 1000,startScrollOffset: 0,endScrollOffset: 50,})
})
</script><template><!-- 自定義導航欄: 默認透明不可見, scroll-view 滾動到 50 時展示 --><view class="navbar" :style="{ paddingTop: safeAreaInsets?.top + 'px' }"><view class="wrap"><navigatorv-if="pages.length > 1"open-type="navigateBack"class="back icon-left"></navigator><navigator v-else url="/pages/index/index" open-type="switchTab" class="back icon-home"></navigator><view class="title">訂單詳情</view></view></view><scroll-view class="viewport" scroll-y enable-back-to-top id="scroller">...滾動容器</scroll-view>
</template>
::: warning 版本升級
- uni-app 不支持
animate
類型。 - 原生微信小程序 支持 animate 類型 。
- 當前需求可基于 原生微信小程序 的 Page 實例類型 擴展 uni-app 的 Page 實例,參考代碼 👇
// 基于小程序的 Page 實例類型擴展 uni-app 的 Page
type PageInstance = Page.PageInstance & WechatMiniprogram.Page.InstanceMethods<any>
const pageInstance = pages.at(-1) as PageInstanceconst pageInstance = pages.at(-1) as any // [!code --]
:::
獲取訂單詳情
請求封裝
/*** 獲取訂單詳情* @param id 訂單id*/
export const getMemberOrderByIdAPI = (id: string) => {return http<OrderResult>({method: 'GET',url: `/member/order/${id}`,})
}
類型聲明
/** 訂單詳情 返回信息 */
export type OrderResult = {/** 訂單編號 */id: string/** 訂單狀態,1為待付款、2為待發貨、3為待收貨、4為待評價、5為已完成、6為已取消 */orderState: number/** 倒計時--剩余的秒數 -1 表示已經超時,正數表示倒計時未結束 */countdown: number/** 商品集合 [ 商品信息 ] */skus: OrderSkuItem[]/** 收貨人 */receiverContact: string/** 收貨人手機 */receiverMobile: string/** 收貨人完整地址 */receiverAddress: string/** 下單時間 */createTime: string/** 商品總價 */totalMoney: number/** 運費 */postFee: number/** 應付金額 */payMoney: number
}/** 商品信息 */
export type OrderSkuItem = {/** sku id */id: string/** 商品 id */spuId: string/** 商品名稱 */name: string/** 商品屬性文字 */attrsText: string/** 數量 */quantity: number/** 購買時單價 */curPrice: number/** 圖片地址 */image: string
}
訂單狀態
在訂單詳情中除了展示訂單信息外,還需要根據不同訂單狀態展示不同的內容。
訂單狀態(orderState) | 含義 |
---|---|
1 | 待付款 |
2 | 待發貨 |
3 | 待收貨 |
4 | 待評價 |
5 | 已完成 |
6 | 已取消 |
訂單狀態常量
枚舉的作用:通過枚舉來替代無意義的訂單狀態數字,提高程序的可讀性。
src/services/constants.ts
/** 訂單狀態枚舉 */
export enum OrderState {/** 待付款 */DaiFuKuan = 1,/** 待發貨 */DaiFaHuo = 2,/** 待收貨 */DaiShouHuo = 3,/** 待評價 */DaiPingJia = 4,/** 已完成 */YiWanCheng = 5,/** 已取消 */YiQuXiao = 6,
}/** 訂單狀態列表 */
export const orderStateList = [{ id: 0, text: '' },{ id: 1, text: '待付款' },{ id: 2, text: '待發貨' },{ id: 3, text: '待收貨' },{ id: 4, text: '待評價' },{ id: 5, text: '已完成' },{ id: 6, text: '已取消' },
]
根據后端返回的數據渲染訂單詳情。
<script setup lang="ts">
import { OrderState, orderStateList } from '@/services/constants'// 獲取訂單詳情
const order = ref<OrderResult>()
const getMemberOrderByIdData = async () => {const res = await getMemberOrderByIdAPI(query.id)order.value = res.result
}onLoad(() => {getMemberOrderByIdData()
})
</script><template><!-- 訂單狀態 --><view class="overview"><!-- 待付款狀態:展示去支付按鈕和倒計時 --><template v-if="order.orderState === OrderState.DaiFuKuan"><view class="status icon-clock">等待付款</view><view class="tips"><text class="money">應付金額: ¥ 99.00</text><text class="time">支付剩余</text>00 時 29 分 59 秒</view><view class="button">去支付</view></template><!-- 其他訂單狀態:展示再次購買按鈕 --><template v-else><!-- 訂單狀態文字 --><view class="status"> {{ orderStateList[order.orderState].text }} </view><navigatorclass="button":url="`/pagesOrder/create/create?orderId=${query.id}`"hover-class="none">再次購買</navigator><!-- 待發貨狀態:模擬發貨,開發期間使用,用于修改訂單狀態為已發貨 --><view v-if="false" class="button"> 模擬發貨 </view></template></view>
</template>
再次購買
現在是第三種生成訂單信息,從訂單詳情頁的【再次購買】按鈕跳轉到填寫訂單頁,需要傳遞頁面參數。
接口封裝
/*** 填寫訂單-再次購買* @param id 訂單id*/
export const getMemberOrderRepurchaseByIdAPI = (id: string) => {return http<OrderPreResult>({method: 'GET',url: `/member/order/repurchase/${id}`,})
}
參考代碼
訂單詳情頁
<template><navigatorclass="button"hover-class="none":url="`/pagesOrder/create/create?orderId=${query.id}`">再次購買</navigator>
</template>
填寫訂單頁
<script setup lang="ts">
// 頁面參數
const query = defineProps<{skuId?: stringcount?: stringorderId?: string // [!code ++]
}>()// 獲取訂單信息
const orderPre = ref<OrderPreResult>()
const getMemberOrderPreData = async () => {if (query.count && query.skuId) {// 立即購買const res = await getMemberOrderPreNowAPI({count: query.count,skuId: query.skuId,})orderPre.value = res.result} else if (query.orderId) {// 再次購買const res = await getMemberOrderRepurchaseByIdAPI(query.orderId)orderPre.value = res.result} else {// 預付訂單const res = await getMemberOrderPreAPI()orderPre.value = res.result}
}
</script>
支付倒計時
通過 uni-ui 組件庫的 uni-countdown 實現倒計時。
<script setup lang="ts">
// 倒計時結束事件
const onTimeup = () => {// 修改訂單狀態為已取消order.value!.orderState = OrderState.YiQuXiao
}
</script><template><!-- 待付款狀態:展示去支付按鈕和倒計時 --><template v-if="order.orderState === OrderState.DaiFuKuan"><view class="status icon-clock">等待付款</view><view class="tips"><text class="money">應付金額: ¥ 99.00</text><text class="time">支付剩余</text><!-- 倒計時組件 --><uni-countdown:second="order.countdown"color="#fff"splitor-color="#fff":show-day="false":show-colon="false"@timeup="onTimeup"/></view><view class="button">去支付</view></template>
</template>
訂單支付
訂單支付其實就是根據訂單號查詢到支付信息,在小程序中調用微信支付的 API 而已。
微信支付說明
- 由于微信支付的限制,僅 appid 為
wx26729f20b9efae3a
的開發者才能調用該接口。此外,開發者還需要微信授權登錄。 - 對于其他開發者,可以使用模擬支付接口進行開發測試,調用后,訂單狀態將自動更新為已支付。
調用接口
- 生產環境:調用正式接口,獲取微信支付參數 + 發起微信支付
- 開發環境:調用模擬接口,通過模擬支付,修改訂單狀態為已支付
import { http } from '@/utils/http'/*** 獲取微信支付參數* @param data orderId 訂單id*/
export const getPayWxPayMiniPayAPI = (data: { orderId: string }) => {return http<WechatMiniprogram.RequestPaymentOption>({method: 'GET',url: '/pay/wxPay/miniPay',data,})
}/*** 模擬支付-內測版* @param data orderId 訂單id*/
export const getPayMockAPI = (data: { orderId: string }) => {return http({method: 'GET',url: '/pay/mock',data,})
}
參考代碼
通過環境變量區分開發環境,調用不同接口。
<script setup lang="ts">
import { getPayMockAPI, getPayWxPayMiniPayAPI } from '@/services/pay'// 訂單支付
const onOrderPay = async () => {// 通過環境變量區分開發環境if (import.meta.env.DEV) {// 開發環境:模擬支付,修改訂單狀態為已支付await getPayMockAPI({ orderId: query.id })} else {// 生產環境:獲取支付參數 + 發起微信支付const res = await getPayWxPayMiniPayAPI({ orderId: query.id })await wx.requestPayment(res.result)}// 關閉當前頁,再跳轉支付結果頁uni.redirectTo({ url: `/pagesOrder/payment/payment?id=${query.id}` })
}
</script><template><view class="button" @tap="onOrderPay">去支付</view>
</template>
支付成功頁
主要用于展示支付結果。
src/pagesOrder/payment/payment.vue
<script setup lang="ts">
import { useGuessList } from '@/composables'// 獲取頁面參數
const query = defineProps<{id: string
}>()// 猜你喜歡
const { guessRef, onScrolltolower } = useGuessList()
</script><template><scroll-view class="viewport" scroll-y @scrolltolower="onScrolltolower"><!-- 訂單狀態 --><view class="overview"><view class="status icon-checked">支付成功</view><view class="buttons"><navigatorhover-class="none"class="button navigator"url="/pages/index/index"open-type="switchTab">返回首頁</navigator><navigatorhover-class="none"class="button navigator":url="`/pagesOrder/detail/detail?id=${query.id}`"open-type="redirect">查看訂單</navigator></view></view><!-- 猜你喜歡 --><XtxGuess ref="guessRef" /></scroll-view>
</template><style lang="scss">
page {display: flex;flex-direction: column;height: 100%;overflow: hidden;
}.viewport {background-color: #f7f7f8;
}.overview {line-height: 1;padding: 50rpx 0;color: #fff;background-color: #27ba9b;.status {font-size: 36rpx;font-weight: 500;text-align: center;}.status::before {display: block;font-size: 110rpx;margin-bottom: 20rpx;}.buttons {height: 60rpx;line-height: 60rpx;display: flex;justify-content: center;align-items: center;margin-top: 60rpx;}.button {text-align: center;margin: 0 10rpx;font-size: 28rpx;color: #fff;&:first-child {width: 200rpx;border-radius: 64rpx;border: 1rpx solid #fff;}}
}
</style>
模擬發貨
僅在訂單狀態為待發貨時,可模擬發貨,調用后訂單狀態修改為待收貨,包含模擬物流。
僅在開發期間使用,項目上線后應該是由商家發貨。
接口封裝
/*** 模擬發貨-內測版* @description 在DEV環境下使用,僅在訂單狀態為待發貨時,可模擬發貨,調用后訂單狀態修改為待收貨,包含模擬物流。* @param id 訂單id*/
export const getMemberOrderConsignmentByIdAPI = (id: string) => {return http({method: 'GET',url: `/member/order/consignment/${id}`,})
}
參考代碼
<script setup lang="ts">
// 是否為開發環境
const isDev = import.meta.env.DEV
// 模擬發貨
const onOrderSend = async () => {if (isDev) {await getMemberOrderConsignmentByIdAPI(query.id)uni.showToast({ icon: 'success', title: '模擬發貨完成' })// 主動更新訂單狀態order.value!.orderState = OrderState.DaiShouHuo}
}
</script><template><!-- 待發貨狀態:模擬發貨,開發期間使用,用于修改訂單狀態為已發貨 --><view v-if="isDev && order.orderState == OrderState.DaiFaHuo" @tap="onOrderSend" class="button">模擬發貨</view>
</template>
確認收貨
點擊確認收貨時需二次確認,提示文案:為保障您的權益,請收到貨并確認無誤后,再確認收貨
接口封裝
/*** 確認收貨* @description 僅在訂單狀態為待收貨時,可確認收貨。* @param id 訂單id*/
export const putMemberOrderReceiptByIdAPI = (id: string) => {return http<OrderResult>({method: 'PUT',url: `/member/order/${id}/receipt`,})
}
參考代碼
<script setup lang="ts">
// 確認收貨
const onOrderConfirm = () => {// 二次確認彈窗uni.showModal({content: '為保障您的權益,請收到貨并確認無誤后,再確認收貨',success: async (success) => {if (success.confirm) {const res = await putMemberOrderReceiptByIdAPI(query.id)// 更新訂單狀態order.value = res.result}},})
}
</script><template><!-- 待收貨狀態: 展示確認收貨按鈕 --><view v-if="order.orderState === OrderState.DaiShouHuo" @tap="onOrderConfirm" class="button">確認收貨</view>
</template>
訂單物流
僅在訂單狀態為待收貨,待評價,已完成時,可獲取物流信息。
請求封裝
/*** 獲取訂單物流* @description 僅在訂單狀態為待收貨,待評價,已完成時,可獲取物流信息。* @param id 訂單id*/
export const getMemberOrderLogisticsByIdAPI = (id: string) => {return http<OrderLogisticResult>({method: 'GET',url: `/member/order/${id}/logistics`,})
}/** 物流信息 返回值類型 */
export type OrderLogisticResult = {/** 快遞公司 */company: {/** 公司名稱 */name: string/** 快遞編號 */number: string/** 聯系電話 */tel: string}/** 商品件數 */count: number/** 物流日志 */list: LogisticItem[]
}/** 物流日志 */
export type LogisticItem = {/** 信息ID */id: string/** 信息文字 */text: string/** 時間 */time: string
}
刪除訂單
僅在訂單狀態為待評價,已完成,已取消時,可刪除訂單。
接口封裝
/*** 刪除訂單* @description 僅在訂單狀態為待評價,已完成,已取消時,可刪除訂單。* @param data ids 訂單集合*/
export const deleteMemberOrderAPI = (data: { ids: string[] }) => {return http({method: 'DELETE',url: `/member/order`,data,})
}
取消訂單
僅在訂單狀態為待付款時,可取消訂單。
接口封裝
/*** 取消訂單* @description 僅在訂單狀態為待付款時,可取消訂單。* @param id 訂單id* @param data cancelReason 取消理由*/
export const getMemberOrderCancelByIdAPI = (id: string, data: { cancelReason: string }) => {return http<OrderResult>({method: 'PUT',url: `/member/order/${id}/cancel`,data,})
}
訂單列表頁
根據訂單的不同狀態展示訂單列表,并實現多 Tabs 分頁加載。
靜態結構
<script setup lang="ts">
import { ref } from 'vue'// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()
// tabs 數據
const orderTabs = ref([{ orderState: 0, title: '全部' },{ orderState: 1, title: '待付款' },{ orderState: 2, title: '待發貨' },{ orderState: 3, title: '待收貨' },{ orderState: 4, title: '待評價' },
])
</script><template><view class="viewport"><!-- tabs --><view class="tabs"><text class="item" v-for="item in 5" :key="item"> 待付款 </text><!-- 游標 --><view class="cursor" :style="{ left: 0 * 20 + '%' }"></view></view><!-- 滑動容器 --><swiper class="swiper"><!-- 滑動項 --><swiper-item v-for="item in 5" :key="item"><!-- 訂單列表 --><scroll-view scroll-y class="orders"><view class="card" v-for="item in 2" :key="item"><!-- 訂單信息 --><view class="status"><text class="date">2023-04-14 13:14:20</text><!-- 訂單狀態文字 --><text>待付款</text><!-- 待評價/已完成/已取消 狀態: 展示刪除訂單 --><text class="icon-delete"></text></view><!-- 商品信息,點擊商品跳轉到訂單詳情,不是商品詳情 --><navigatorv-for="sku in 2":key="sku"class="goods":url="`/pagesOrder/detail/detail?id=1`"hover-class="none"><view class="cover"><imagemode="aspectFit"src="https://yanxuan-item.nosdn.127.net/c07edde1047fa1bd0b795bed136c2bb2.jpg"></image></view><view class="meta"><view class="name ellipsis">ins風小碎花泡泡袖襯110-160cm</view><view class="type">藏青小花 130</view></view></navigator><!-- 支付信息 --><view class="payment"><text class="quantity">共5件商品</text><text>實付</text><text class="amount"> <text class="symbol">¥</text>99</text></view><!-- 訂單操作按鈕 --><view class="action"><!-- 待付款狀態:顯示去支付按鈕 --><template v-if="true"><view class="button primary">去支付</view></template><template v-else><navigatorclass="button secondary":url="`/pagesOrder/create/create?orderId=id`"hover-class="none">再次購買</navigator><!-- 待收貨狀態: 展示確認收貨 --><view v-if="false" class="button primary">確認收貨</view></template></view></view><!-- 底部提示文字 --><view class="loading-text" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">{{ true ? '沒有更多數據~' : '正在加載...' }}</view></scroll-view></swiper-item></swiper></view>
</template><style lang="scss">
page {height: 100%;overflow: hidden;
}.viewport {height: 100%;display: flex;flex-direction: column;background-color: #fff;
}// tabs
.tabs {display: flex;justify-content: space-around;line-height: 60rpx;margin: 0 10rpx;background-color: #fff;box-shadow: 0 4rpx 6rpx rgba(240, 240, 240, 0.6);position: relative;z-index: 9;.item {flex: 1;text-align: center;padding: 20rpx;font-size: 28rpx;color: #262626;}.cursor {position: absolute;left: 0;bottom: 0;width: 20%;height: 6rpx;padding: 0 50rpx;background-color: #27ba9b;/* 過渡效果 */transition: all 0.4s;}
}// swiper
.swiper {background-color: #f7f7f8;
}// 訂單列表
.orders {.card {min-height: 100rpx;padding: 20rpx;margin: 20rpx 20rpx 0;border-radius: 10rpx;background-color: #fff;&:last-child {padding-bottom: 40rpx;}}.status {display: flex;align-items: center;justify-content: space-between;font-size: 28rpx;color: #999;margin-bottom: 15rpx;.date {color: #666;flex: 1;}.primary {color: #ff9240;}.icon-delete {line-height: 1;margin-left: 10rpx;padding-left: 10rpx;border-left: 1rpx solid #e3e3e3;}}.goods {display: flex;margin-bottom: 20rpx;.cover {width: 170rpx;height: 170rpx;margin-right: 20rpx;border-radius: 10rpx;overflow: hidden;position: relative;}.quantity {position: absolute;bottom: 0;right: 0;line-height: 1;padding: 6rpx 4rpx 6rpx 8rpx;font-size: 24rpx;color: #fff;border-radius: 10rpx 0 0 0;background-color: rgba(0, 0, 0, 0.6);}.meta {flex: 1;display: flex;flex-direction: column;justify-content: center;}.name {height: 80rpx;font-size: 26rpx;color: #444;}.type {line-height: 1.8;padding: 0 15rpx;margin-top: 10rpx;font-size: 24rpx;align-self: flex-start;border-radius: 4rpx;color: #888;background-color: #f7f7f8;}.more {flex: 1;display: flex;align-items: center;justify-content: center;font-size: 22rpx;color: #333;}}.payment {display: flex;justify-content: flex-end;align-items: center;line-height: 1;padding: 20rpx 0;text-align: right;color: #999;font-size: 28rpx;border-bottom: 1rpx solid #eee;.quantity {font-size: 24rpx;margin-right: 16rpx;}.amount {color: #444;margin-left: 6rpx;}.symbol {font-size: 20rpx;}}.action {display: flex;justify-content: flex-end;align-items: center;padding-top: 20rpx;.button {width: 180rpx;height: 60rpx;display: flex;justify-content: center;align-items: center;margin-left: 20rpx;border-radius: 60rpx;border: 1rpx solid #ccc;font-size: 26rpx;color: #444;}.secondary {color: #27ba9b;border-color: #27ba9b;}.primary {color: #fff;background-color: #27ba9b;}}.loading-text {text-align: center;font-size: 28rpx;color: #666;padding: 20rpx 0;}
}
</style>
Tabs 滑動切換
訂單列表的 Tabs 支持滑動切換,從【我的】進入訂單列表,能高亮對應的下標。
<script setup lang="ts">
// tabs 數據
const orderTabs = ref([{ orderState: 0, title: '全部' },{ orderState: 1, title: '待付款' },{ orderState: 2, title: '待發貨' },{ orderState: 3, title: '待收貨' },{ orderState: 4, title: '待評價' },
])// 獲取頁面參數
const query = defineProps<{type: string
}>()// 高亮下標
const activeIndex = ref(orderTabs.value.findIndex((v) => v.orderState === Number(query.type)))
</script><template><view class="viewport"><!-- tabs --><view class="tabs"><textclass="item"v-for="(item, index) in orderTabs":key="item.title"@tap="activeIndex = index">{{ item.title }}</text><!-- 游標 --><view class="cursor" :style="{ left: activeIndex * 20 + '%' }"></view></view><!-- 滑動容器 --><swiper class="swiper" :current="activeIndex" @change="activeIndex = $event.detail.current"><!-- 滑動項 --><swiper-item v-for="item in orderTabs" :key="item.title"><!-- 訂單列表 --><scroll-view scroll-y class="orders">...省略</scroll-view></swiper-item></swiper></view>
</template>
獲取訂單列表
當前頁面是多 Tabs 列表的情況,每個 Tabs 都是獨立的列表,并支持分頁加載。
接口信息
接口地址:/member/order
請求方式:GET
Query 參數:
字段名稱 | 是否必須 | 默認值 | 備注 |
---|---|---|---|
page | 可選 | 1 | 頁碼 |
pageSize | 可選 | 10 | 頁容量 |
orderState | 可選 | 0 | 訂單狀態 |
接口封裝
/*** 獲取訂單列表* @param data orderState 訂單狀態*/
export const getMemberOrderAPI = (data: OrderListParams) => {return http<OrderListResult>({method: 'GET',url: `/member/order`,data,})
}import type { PageParams } from '@/types/global'
/** 訂單列表參數 */
export type OrderListParams = PageParams & { orderState: number }/** 訂單列表 */
export type OrderListResult = {/** 總記錄數 */counts: number/** 數據集合 [ 訂單信息 ] */items: OrderItem[]/** 當前頁碼 */page: number/** 總頁數 */pages: number/** 頁尺寸 */pageSize: number
}/** 訂單列表項 */
export type OrderItem = OrderResult & {/** 總件數 */totalNum: number
}
訂單列表渲染
為了更好維護多 Tabs 列表,把列表抽離成業務組件,在組件內部獨立維護列表數據,包括分頁,下拉刷新等業務。
參考代碼
訂單列表頁,把訂單狀態傳遞給列表組件(父傳子)。
<script setup lang="ts">
// 導入列表組件
import OrderList from './components/OrderList.vue'
</script><template><!-- 滑動容器 --><swiper class="swiper" :current="activeIndex" @change="activeIndex = $event.detail.current"><!-- 滑動項 --><swiper-item v-for="item in orderTabs" :key="item.title"><!-- 訂單列表 --><OrderList :order-state="item.orderState" /></swiper-item></swiper>
</template>
列表組件
<script setup lang="ts">
import { OrderState } from '@/services/constants'
import { orderStateList } from '@/services/constants'
import { getMemberOrderAPI } from '@/services/order'
import type { OrderItem } from '@/types/order'
import type { OrderListParams } from '@/types/order'
import { onMounted, ref } from 'vue'// 獲取屏幕邊界到安全區域距離
const { safeAreaInsets } = uni.getSystemInfoSync()// 定義 porps
const props = defineProps<{orderState: number
}>()// 請求參數
const queryParams: OrderListParams = {page: 1,pageSize: 5,orderState: props.orderState,
}// 獲取訂單列表
const orderList = ref<OrderItem[]>([])
const getMemberOrderData = async () => {const res = await getMemberOrderAPI(queryParams)orderList.value = res.result.items
}onMounted(() => {getMemberOrderData()
})
</script><template><scroll-view scroll-y class="orders"><view class="card" v-for="order in orderList" :key="order.id"><!-- 訂單信息 --><view class="status"><text class="date">{{ order.createTime }}</text><!-- 訂單狀態文字 --><text>{{ orderStateList[order.orderState].text }}</text><!-- 待評價/已完成/已取消 狀態: 展示刪除訂單 --><text v-if="order.orderState >= OrderState.DaiPingJia" class="icon-delete"></text></view><!-- 商品信息,點擊商品跳轉到訂單詳情,不是商品詳情 --><navigatorv-for="item in order.skus":key="item.id"class="goods":url="`/pagesOrder/detail/detail?id=${order.id}`"hover-class="none"><view class="cover"><image mode="aspectFit" :src="item.image"></image></view><view class="meta"><view class="name ellipsis">{{ item.name }}</view><view class="type">{{ item.attrsText }}</view></view></navigator><!-- 支付信息 --><view class="payment"><text class="quantity">共{{ order.totalNum }}件商品</text><text>實付</text><text class="amount"> <text class="symbol">¥</text>{{ order.payMoney }}</text></view><!-- 訂單操作按鈕 --><view class="action"><!-- 待付款狀態:顯示去支付按鈕 --><template v-if="order.orderState === OrderState.DaiFuKuan"><view class="button primary">去支付</view></template><template v-else><navigatorclass="button secondary":url="`/pagesOrder/create/create?orderId=id`"hover-class="none">再次購買</navigator><!-- 待收貨狀態: 展示確認收貨 --><view v-if="order.orderState === OrderState.DaiShouHuo" class="button primary">確認收貨</view></template></view></view><!-- 底部提示文字 --><view class="loading-text" :style="{ paddingBottom: safeAreaInsets?.bottom + 'px' }">{{ true ? '沒有更多數據~' : '正在加載...' }}</view></scroll-view>
</template>
分頁加載
分頁加載的邏輯在之前我們已經學習過,本節就不再重復講義的內容了,下拉刷新業務同理。
訂單支付
訂單支付功能之前我們已經學習過,也不再重復。
確認收貨,刪除訂單等按鈕的業務同理。
12.小兔鮮兒 - 項目打包
微信小程序端
把當前 uni-app 項目打包成微信小程序端,并發布上線。
核心步驟
- 運行打包命令
pnpm build:mp-weixin
- 預覽和測試,微信開發者工具導入生成的
/dist/build/mp-weixin
目錄 - 上傳小程序代碼
- 提交審核和發布
步驟圖示
項目打包上線需要使用到多個工具,注意工具之間的職責。
VSCode ----> 微信開發者工具 ----> 微信公眾平臺
了解:開發者也可獨立使用 miniprogram-ci 進行小程序代碼的上傳等操作。
::: tip 舉一反三
打包成其他小程序端的步驟類似,只是更換了 打包命令 和 開發者工具 。
:::
條件編譯
::: tip 常見問題
Q:按照 uni-app 規范開發可保證多平臺兼容,但每個平臺有自己的一些特性,該如何處理?
A:通過 條件編譯,讓代碼按條件編譯到指定平臺。
:::
網頁端不支持微信平臺授權登錄等功能,可通過 條件編譯,實現不同端渲染不同的登錄界面。
條件編譯語法
通過特殊注釋,以 #ifdef
或 #ifndef
加 平臺名稱 開頭,以 #endif
結尾。
多平臺編譯:#ifdef H5 || MP-WEIXIN
表示在 H5 端 或 微信小程序端 代碼。
條件編譯支持: 支持 .vue, .ts, .js, .scss, .css, pages.json 等文件。
<script setup lang="ts">
// 微信平臺特有API,需要條件編譯
// #ifdef MP-WEIXIN
wx.login()
wx.requestPayment()
// #endif
</script><template><!-- 微信開發能力按鈕,需要條件編譯 --><!-- #ifdef MP-WEIXIN --><button open-type="openSetting">授權管理</button><button open-type="feedback">問題反饋</button><button open-type="contact">聯系我們</button><!-- #endif -->
</template><style>
/* 如果出現樣式兼容,也可添加條件編譯 */
page {/* #ifdef H5 */background-color: pink;/* #endif */
}
</style>
::: tip 開發技巧
可通過搜索 wx.
和 open-type
等平臺關鍵詞,快速查找需要小程序端需添加編譯模式的代碼。
:::
打包為 H5 端
把當前 uni-app 項目打包成網頁(H5)端,并配置路由基礎路徑。
核心步驟
- 運行打包命令
pnpm build:h5
- 預覽和測試,使用瀏覽器打開
/dist/build/h5
目錄下的index.html
文件 - 由運維部署到服務器
路由基礎路徑
默認的路由基礎路徑為 /
根路徑,部分網站并不是部署到根路徑,需要按運維要求調整。
// manifest.json
{/* 網頁端特有配置 */"h5": {"router": {// 基礎路徑:./ 為相對路徑"base": "./"}} /* 小程序特有相關 */,"mp-weixin": {// …省略},"vueVersion": "3"
}
打包為 APP 端
App 端 的打包,預覽、測試、發行,使用 HBuilderX
工具。