1.地址模块-准备工作
- 新建分包页面
- 静态结构
- 动态设置标题
// 获取页面参数
const query = defineProps<{
// id ke有可无
id?: string
}>()
// 动态设置标题
uni.setNavigationBarTitle({ title: query.id ? '修改地址' : '新建地址' })
2.新建地址
- 封装API接口
- 定义参数类型
import { http } from '@/utils/http'
import { AddressParams } from '@/types/address'
export const postMemberAddressAPI = (data: AddressParams) => {
return http({
method: 'POST',
url: '/member/address',
data,
})
}
- 收集表单数据
- 点击保存调用
- 成功提示
- 返回上一页
// 收集地、区信息
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 () => {
// 新建地址请求
const res = await postMemberAddressAPI(form.value)
// 成功提示
uni.showToast({ icon: 'none', title: '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 500)
}
<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>
<picker
class="picker"
@change="onRegionChange"
mode="region"
:value="form.fullLocation.split(' ')"
>
<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>
<switch
class="switch"
@change="onSwitchChange"
color="#27ba9b"
:checked="form.isDefault === 1"
/>
</view>
</form>
</view>
<!-- 提交按钮 -->
<button class="button" @tap="onSubmit">保存并使用</button>
</template>
3.列表渲染
- 封装API接口
// 获取收货地址列表
export const getMemberAddressAPI = () => {
return http<AddressItem[]>({
method: 'GET',
url: '/member/address',
})
}
- 初始化调用
// 获取收货地址列表数据
const addressList = ref<AddressItem[]>([])
const getMemberAddressData = async () => {
const res = await getMemberAddressAPI()
addressList.value = res.result
}
// 初始化
onShow(() => {
getMemberAddressData()
})
- 定义类型(复用)
- 渲染列表
<!-- 收货地址项 -->
<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>
<navigator
class="edit"
hover-class="none"
:url="`/pagesMember/address-form/address-form?id=${item.id}`"
>
修改
</navigator>
</view>
</view>
注意事项:列表地址通过onShow初始化调用,因为新建地址成功后,需要显示最新收货地址列表
4.修改地址-数据回显
- 封装API接口
// 获取收货地址详情
// 获取一个收货地址,就不用写数组格式
export const getMemberAddressByIdAPI = (id: string) => {
return http<AddressItem>({
method: 'GET',
url: `/member/address/${id}`,
})
}
- 若有地址id,则初始化调用
- 表单数据回显
// 获取页面参数
const query = defineProps<{
// id ke有可无
id?: string
}>()
// 获取收货地址详情数据
const getMemberAddressByIdData = async () => {
//确保query.id不是undefine
if (query.id) {
// 发送请求
const res = await getMemberAddressByIdAPI(query.id)
//实现数据回显,将数据合并
Object.assign(form.value, res.result)
}
}
// 页面加载
onLoad(() => {
getMemberAddressByIdData()
})
5.修改地址-保存修改
新建API
// 修改收货地址
export const putMemberAddressByIdAPI = (id: string, data: AddressParams) => {
return http({
method: 'PUT',
url: `/member/address/${id}`,
data,
})
}
修改地址
// 提交表单
const onSubmit = async () => {
if (query.id) {
// 修改地址
await putMemberAddressByIdAPI(query.id, form.value)
} else {
// 新建地址请求
await postMemberAddressAPI(form.value)
}
// 成功提示
uni.showToast({ icon: 'none', title: query.id ? '修改成功' : '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 500)
}
6.地址管理-表单校验
- uni-form组件
- 定义校验规则
// 定义校验规则
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: '请选择详细地址' }],
},
}
- 修改表单结构
- 绑定表单数据
<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>
<input class="input" placeholder="请填写收货人手机号码" v-model="form.contact" />
</uni-forms-item>
<uni-forms-item name="fullLocation" class="form-item">
<text class="label">所在地区</text>
<picker
class="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>
<switch
class="switch"
@change="onSwitchChange"
color="#27ba9b"
:checked="form.isDefault === 1"
/>
</view>
</uni-forms>
- 提交时校验
//表单组件实例
const formRef = ref<UniHelper.UniFormsInstance>()
// 提交表单
const onSubmit = async () => {
try {
await formRef.value?.validate?.()
if (query.id) {
// 修改地址
await putMemberAddressByIdAPI(query.id, form.value)
} else {
// 新建地址请求
await postMemberAddressAPI(form.value)
}
// 成功提示
uni.showToast({ icon: 'none', title: query.id ? '修改成功' : '添加成功' })
// 返回上一页
setTimeout(() => {
uni.navigateBack()
}, 500)
} catch (error) {
uni.showToast({ icon: 'error', title: '请填写完整信息' })
}
}
7.删除收货地址
- 用uni-swipe-action组件
<!-- 地址列表 -->
<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>
<navigator
class="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>
- 修改列表结构
- 绑定删除事件
- 二次确认删除
// 删除
const onDeleteAddress = (id: string) => {
// 二次确认
uni.showModal({
content: '确定删除?',
success: async (res) => {
if (res.confirm) {
//删除API
await deleteMemberAddressByIdAPI(id)
// 重新获取收货地址列表
// await async 确保先删除在重新获取
getMemberAddressData()
}
},
})
}
- 删除地址API
// 删除地址
export const deleteMemberAddressByIdAPI = (id: string) => {
return http({
method: 'DELETE',
url: `/member/address/${id}`,
})
}
8.SKU模块-基本概念
SKU概念:存货单位,库存管理的最小可用单元,通常称为‘单品’
SKU常见于电商领域,对于前端工程师而言,更多关注SKU算法,基于后端的SKU数据渲染页面并实现交互。
在uniapp插件市场中: https://ext.dcloud.net.cn/
搜索sku,并用vue3下载
- 将components目录下的vk-data-goods-sku-popup 和 vk-data-input-number-box 复制到你项目根目录下的components目录下 (若你的项目根目录下无components则先新增一个components目录)
- 通过下面的基本使用示例的方式使用组件,API文档 在最下面
<!-- 静态数据演示版本 适合任何后端 -->
<template>
<view class="app">
<button @click="openSkuPopup()">打开SKU组件</button>
<vk-data-goods-sku-popup
ref="skuPopup"
v-model="skuKey"
border-radius="20"
:z-index="990"
:localdata="goodsInfo"
:mode="skuMode"
@open="onOpenSkuPopup"
@close="onCloseSkuPopup"
@add-cart="addCart"
@buy-now="buyNow"
></vk-data-goods-sku-popup>
</view>
</template>
<script>
export default {
data() {
return {
// 是否打开SKU弹窗
skuKey: false,
// SKU弹窗模式
skuMode: 1,
// 后端返回的商品信息
goodsInfo: {}
};
},
// 监听 - 页面每次【加载时】执行(如:前进)
onLoad(options) {
this.init(options);
},
methods: {
// 初始化
init(options = {}) {},
// 获取商品信息,并打开sku弹出
openSkuPopup() {
/**
* 获取商品信息
* 这里可以看到每次打开SKU都会去重新请求商品信息,为的是每次打开SKU组件可以实时看到剩余库存
*/
// 此处写接口请求,并将返回的数据进行处理成goodsInfo的数据格式,
// goodsInfo是后端返回的数据
this.goodsInfo = {
"_id": "001",
"name": "iphone11",
"goods_thumb": "https://img14.360buyimg.com/n0/jfs/t1/59022/28/10293/141808/5d78088fEf6e7862d/68836f52ffaaad96.jpg",
"sku_list": [
{
"_id": "001",
"goods_id": "001",
"goods_name": "iphone11",
"image": "https://img14.360buyimg.com/n0/jfs/t1/79668/22/9987/159271/5d780915Ebf9bf3f4/6a1b2703a9ed8737.jpg",
"price": 19800,
"sku_name_arr": ["红色", "128G", "公开版"],
"stock": 1000
},
{
"_id": "002",
"goods_id": "001",
"goods_name": "iphone11",
"image": "https://img14.360buyimg.com/n0/jfs/t1/52252/35/10516/124064/5d7808e0E46202391/7100f3733a1c1f00.jpg",
"price": 9800,
"sku_name_arr": ["白色", "256G", "公开版"],
"stock": 100
},
{
"_id": "003",
"goods_id": "001",
"goods_name": "iphone11",
"image": "https://img14.360buyimg.com/n0/jfs/t1/79668/22/9987/159271/5d780915Ebf9bf3f4/6a1b2703a9ed8737.jpg",
"price": 19800,
"sku_name_arr": ["红色", "256G", "公开版"],
"stock": 1
}
],
"spec_list": [
{
"name": "颜色",
"list": [
{ "name": "红色" },
{ "name": "黑色" },
{ "name": "白色" }
]
},
{
"name": "内存",
"list": [
{ "name": "128G" },
{ "name": "256G" }
],
},
{
"name": "版本",
"list": [
{ "name": "公开版" },
{ "name": "非公开版" }
]
}
]
};
this.skuKey = true;
},
// sku组件 开始-----------------------------------------------------------
onOpenSkuPopup() {
console.log("监听 - 打开sku组件");
},
onCloseSkuPopup() {
console.log("监听 - 关闭sku组件");
},
// 加入购物车前的判断
addCartFn(obj) {
let { selectShop } = obj;
// 模拟添加到购物车,请替换成你自己的添加到购物车逻辑
let res = {};
let name = selectShop.goods_name;
if (selectShop.sku_name != "默认") {
name += "-" + selectShop.sku_name_arr;
}
res.msg = `${name} 已添加到购物车`;
if (typeof obj.success == "function") obj.success(res);
},
// 加入购物车按钮
addCart(selectShop) {
console.log("监听 - 加入购物车");
this.addCartFn({
selectShop: selectShop,
success: res => {
// 实际业务时,请替换自己的加入购物车逻辑
this.toast(res.msg);
setTimeout(() => {
this.skuKey = false;
}, 300);
}
});
},
// 立即购买
buyNow(selectShop) {
console.log("监听 - 立即购买");
this.addCartFn({
selectShop: selectShop,
success: res => {
// 实际业务时,请替换自己的立即购买逻辑
this.toast("立即购买");
}
});
},
toast(msg) {
uni.showToast({
title: msg,
icon: "none"
});
}
}
};
</script>
<style lang="scss" scoped>
.app {
padding: 30rpx;
font-size: 28rpx;
}
</style>
eslint不报错
/* eslint-disable */
9.渲染商品信息
- SKU弹窗组件
<!-- Sku弹窗组件 -->
<vk-data-goods-sku-popup v-model="isShowSku" :localdata="localdata" />
- SKU组件文档
- 类型声明文件
- 显示SKU弹窗
// 是否显示SKU组件
const isShowSku = ref(true)
// 商品信息
const localdata = ref({} as SkuPopupLocaldata)
- 渲染商品信息
// 获取商品详情信息
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) => {
return {
name: v.name,
list: v.values,
}
}),
sku_list: res.result.skus.map((v) => {
return {
_id: v.id,
goods_id: res.result.id,
goods_name: res.result.name,
image: v.picture,
price: v.price * 100, // 注意:需要乘以 100
stock: v.inventory,
sku_name_arr: v.specs.map((vv) => vv.valueName),
}
}),
}
}
10. 打开弹窗交互
- 设置按钮模式
// 按钮模式
enum SkuMode {
Both = 1,
Cart = 2,
Buy = 3,
}
const mode = ref<SkuMode>(SkuMode.Both)
// 打开Sku弹窗修改按钮模式
const openSkuPopup = (val: SkuMode) => {
// 显示组件
isShowSku.value = true
// 修改按钮模式
mode.value = val
}
- 微调组件样式
<!-- Sku弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-bankground-color="#27BA9B"
/>
<view class="buttons">
<view class="addcart" @tap="openSkuPopup(SkuMode.Cart)"> 加入购物车 </view>
<view class="buynow" @tap="openSkuPopup(SkuMode.Buy)"> 立即购买 </view>
</view>
11.计算被选中的值
- 获取SKU组件实例
- 计算被选中的值
//sku组件实例
const skuPopupRef = ref<SkuPopupInstanceType>()
// 计算被选中的值
const selectArrText = computed(() => {
return skuPopupRef.value?.selectArr?.join(' ').trim() || '请选择获取商品'
})
- 渲染到页面
- 微调组件样式
<!-- Sku弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-bankground-color="#27BA9B"
ref="skuPopupRef"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
/>
return skuPopupRef.value?.selectArr?.join(’ ').trim() || ‘请选择获取商品’, 要取出多余空格,否则短路运算会出问题
12.加入购物车
- 加入购物车事件
<!-- Sku弹窗组件 -->
<vk-data-goods-sku-popup
v-model="isShowSku"
:localdata="localdata"
:mode="mode"
add-cart-background-color="#FFA868"
buy-now-bankground-color="#27BA9B"
ref="skuPopupRef"
:actived-style="{
color: '#27BA9B',
borderColor: '#27BA9B',
backgroundColor: '#E9F8F5',
}"
@add-cart="onAddCart"
/>
- 调用API接口
import { http } from '@/utils/http'
export const postMemberCartAPI = (data: { skuId: string; count: number }) => {
return http({
method: 'POST',
url: '/member/cart',
data,
})
}
- 成功提示
// 加入购物车
const onAddCart = async (ev: SkuPopupEvent) => {
const res = await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
uni.showToast({ title: '添加成功' })
}
- 关闭SKU弹窗
// 加入购物车
const onAddCart = async (ev: SkuPopupEvent) => {
const res = await postMemberCartAPI({ skuId: ev._id, count: ev.buy_num })
uni.showToast({ title: '添加成功' })
// 隐藏弹窗
isShowSku.value = false
}
13.列表渲染
- 页面静态页面
- 获取会员store
import { useMemberStore } from '@/stores'
// 获取store
const MemberStore = useMemberStore()
- 条件渲染
// 小程序多页面应用的更好的解决方案
uni.navigateBack()
- 购物车列表API
export const getMemberCartAPI = () => {
return http({
method: 'GET',
url: '/member/cart',
})
}
// 获取购物车数据
const getMemberCartData = async () => {
const res = await getMemberCartAPI()
}
- 初始化调用
// 初始化调用, 获取最新数据
onShow(() => {
// 在onShow中要先判断
if (MemberStore.profile) {
getMemberCartData()
}
})
- 列表渲染
<!-- 已登录: 显示购物车 -->
<template v-if="MemberStore.profile">
<!-- 购物车列表 -->
<view class="cart-list" v-if="cartList.length">
<!-- 优惠提示 -->
<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 cartList" :key="item.skuId" class="cart-swipe">
<!-- 商品信息 -->
<view class="goods">
<!-- 选中状态 -->
<text class="checkbox" :class="{ checked: item.selected }"></text>
<navigator
:url="`/pages/goods/goods?id=${item.id}`"
hover-class="none"
class="navigator"
>
<image mode="aspectFill" class="picture" :src="item.picture"></image>
<view class="meta">
<view class="name ellipsis">{{ item.name }}</view>
<view class="attrsText ellipsis"> {{ item.attrsText }}</view>
<view class="price">{{ item.nowPrice }}</view>
</view>
</navigator>
<!-- 商品数量 -->
<view class="count">
<text class="text">-</text>
<input class="input" type="number" :value="item.count.toString()" />
<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>
转换成字符串,避免类型报错
:value="item.count.toString()
14.删除单品
- 购物车删除API
export const deleteMemberCartAPI = (data: { ids: string[] }) => {
return http({
method: 'DELETE',
url: '/member/cart',
data,
})
}
- 按钮绑定事件
<!-- 右侧删除按钮 -->
<template #right>
<view class="cart-swipe-right">
<button class="button delete-button" @tap="onDeleteCart(item.skuId)">删除</button>
</view>
</template>
- 弹窗二次确认
- 调用删除API
- 重新获取列表
// 删除购物车
const deleteMemberCartData = async (skuId: string) => {
const res = await deleteMemberCartAPI({ ids: [skuId] })
}
// 删除按钮
const onDeleteCart = (skuId: string) => {
// 二次确认
uni.showModal({
content: '确认删除?',
success: async (res) => {
if (res.confirm) {
// 后端调用删除
await deleteMemberCartAPI({ ids: [skuId] })
// 刷新
deleteMemberCartData(skuId)
}
},
})
}
15.修改单品数量
- 步进器组件
<vk-data-input-number-box
v-model="item.count"
:min="1"
:max="item.stock"
:index="item.skuId"
@change="onChangeCount"
/>
- 类型声明文件
- 属性绑定
- 事件绑定
- 调用单品修改API
export const putMemberCartBySkuIdAPI = (
skuId: string,
data: { selected?: boolean; count?: number },
) => {
return http({
method: 'PUT',
url: `/member/cart/${skuId}`,
data,
})
}
// 修改商品数量
const onChangeCount = (ev: InputNumberBoxEvent) => {
putMemberCartBySkuIdAPI(ev.index, { count: ev.value })
}
16.修改选中状态
- 点击单品选中
- 调用修改单品选中API
// 修改选中状态-单品修改
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
})
// 后端更新
}
- 调用修改全选状态API
export const putMemberCartSelectedAPI = (data: { selected: boolean }) => {
return http({
method: 'PUT',
url: '/member/cart/selected',
data,
})
}
// 修改选中状态-全选修改
const onChangeSelectedAll = () => {
// 全选状态取反
const _isSelectedAll = !isSelectedAll.value
// 前端更新
cartList.value.forEach((item) => {
item.selected = _isSelectedAll
})
// 后端更新
putMemberCartSelectedAPI({ selected: _isSelectedAll })
}
17.底部结算信息
- 计算选中单品列表
// 计算选中单品列表
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 终止后面的程序
return uni.showToast({
icon: 'none',
title: '请选择商品',
})
}
// 跳转到结算页
uni.showToast({ title: '等待完成' })
}
<!-- 吸底工具栏 -->
<view class="toolbar">
<text @tap="onChangeSelectedAll" class="all" :class="{ checked: isSelectedAll }">全选</text>
<text class="text">合计:</text>
<text class="amount">{{ selectedCartListMoney }}</text>
<view class="button-grounp">
<view
@tap="gotoPayment"
class="button payment-button"
:class="{ disabled: selectedCartListCount === 0 }"
>
去结算({{ selectedCartListCount }})
</view>
</view>
</view>
18.两个购物车页面
在pages.json中的list 中指定的是开发页,没有的就不是
<script lang="ts">
import CartMain from './components/CartMain.vue'
</script>
<template>
<CartMain />
</template>