新增营销系统、分销系统、会员功能、门店、提现功能

This commit is contained in:
Shaw
2024-02-08 21:01:37 +08:00
parent 68b3f2dcc3
commit 17c043348a
1398 changed files with 81279 additions and 56269 deletions

View File

@ -0,0 +1,212 @@
<template>
<Header ref="headerRef" :scroll-top="scrollTop"> 分类</Header>
<view
class="goods-category"
:style="computeMainBoxStyle"
>
<uv-vtabs
:list="categoryData"
:hdHeight="`${headerRef&&headerRef.containerHeight || 0}px`"
>
<template
v-for="(item, index) in categoryData"
:key="index"
>
<uv-vtabs-item :index="index">
<view class="category-list">
<view
class="category"
>
<view class="category-title">
<view class="line"></view>
<view class="category-title-text">
{{ item.name }}
</view>
<view class="line"></view>
</view>
<view class="category-content">
<uv-grid
:border="false"
:col="3"
>
<uv-grid-item
v-for="goodCategory in item.children"
@click="toGoodsCategoryList(goodCategory.id)"
:key="goodCategory.id"
>
<view class="category-item">
<view class="category-item-icon">
<image
class="image"
:src="goodCategory.picUrl"
mode="aspectFit"
/>
</view>
<view class="category-item-name">
{{ goodCategory.name }}
</view>
</view>
</uv-grid-item>
</uv-grid>
</view>
</view>
</view>
</uv-vtabs-item>
</template>
<uv-gap
bg-color="#fff"
height="600"
></uv-gap>
</uv-vtabs>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { getCategoryList } from '@/api/product'
import { useRouter } from "@/hooks/useRouter";
import Header from "@/components/Header/index.vue"
import { onLoad } from "@dcloudio/uni-app";
import UvVtabs from "@/uni_modules/uv-vtabs/components/uv-vtabs/uv-vtabs.vue";
import UvVtabsItem from "@/uni_modules/uv-vtabs/components/uv-vtabs-item/uv-vtabs-item.vue";
import UvGrid from "@/uni_modules/uv-grid/components/uv-grid/uv-grid.vue";
import UvGridItem from "@/uni_modules/uv-grid/components/uv-grid-item/uv-grid-item.vue";
import UvGap from "@/uni_modules/uv-gap/components/uv-gap/uv-gap.vue";
import { useScroll } from "@/hooks/useScroll";
const {push} = useRouter()
const {scrollTop} = useScroll()
const headerRef = ref() //导航条
// 中心高度 100bh - 上导航栏 - h5底部高度
const computeMainBoxStyle = computed(() => {
const height = headerRef.value?.containerHeight ?? 0
return {
height: `calc(100vh - ${ height }rpx - var(--window-bottom))`
}
})
const categoryData = ref([]) // 分类列表
/**
* 获取分类
*/
async function doGetCategoryList() {
const category = await getCategoryList()
if (!category) return
// 二级分类,需要处理一下
categoryData.value = arrayToTree(category)
}
/**
* 数组转tree
* @param items
* @returns {*[]}
*/
function arrayToTree(items) {
const rootItems = [];
const itemMap = {};
// 首先将所有项按照id映射到itemMap中并找到根项没有父项的项
items.forEach(item => {
itemMap[item.id] = {...item, children: []};
if (item.parentId === 0) {
rootItems.push(itemMap[item.id]);
}
});
// 然后将子项添加到父项的children属性中
items.forEach(item => {
if (item.parentId !== 0) {
itemMap[item.parentId].children.push(itemMap[item.id]);
}
});
return rootItems;
}
/**
* 去商品列表
* @param categoryId
*/
function toGoodsCategoryList(categoryId) {
push({
url: '/pages/goodsList/goodsList'
}, {
data: {sid: categoryId}
})
}
onLoad(() => {
doGetCategoryList()
})
</script>
<style lang="scss">
.goods-category {
width: 100%;
overflow: hidden;
}
.category-list {
padding: 0 20rpx;
}
.category {
margin-bottom: 20rpx;
&-title {
display: flex;
align-items: center;
justify-content: center;
margin-top: 50rpx;
margin-bottom: 30rpx;
&-text {
line-height: 45rpx;
font-size: 32rpx;
color: #333333;
margin: 0 30rpx;
}
.line {
width: 30rpx;
height: 1rpx;
background: #CCCCCC;
}
}
&-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 30rpx;
.image {
width: 150rpx;
height: 110rpx;
display: block;
background: #efefef;
}
&-name {
margin-top: 20rpx;
line-height: 32rpx;
font-size: 24rpx;
color: #999999;
}
}
}
.goods-category :deep(.uv-vtabs__bar) {
background: #F5F5F5;
.uv-vtabs__bar-item {
text-align: center;
}
}
</style>

103
root/index/index.vue Normal file
View File

@ -0,0 +1,103 @@
<template>
<view class="home-container" :style="{ 'margin-top': `${statusBarHeight}px` }">
<CanvasPage />
<!-- h5 tabbar 底部 -->
<!-- <view class="h5-tabbar-height"></view> -->
<adWindow></adWindow>
<ReturnTop :scroll-top="scrollTop" />
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { useMainStore } from '@/store/store'
import { useRouter } from "@/hooks/useRouter";
import CanvasPage from '@/components/canvasShow/canvasShowPage.vue'
import ReturnTop from "@/components/ReturnTop/index.vue"
import adWindow from "@/components/adWindow/adWindow.vue"
import UvIcon from "@/uni_modules/uv-icon/components/uv-icon/uv-icon.vue";
import { useScroll } from "@/hooks/useScroll";
import { homeLogoIcon } from "@/utils/images";
import { useShare } from "@/hooks/useShare";
const main = useMainStore()
const {push} = useRouter()
const recommendRef = ref(null)
const statusBarHeight = ref(0)
const searchShadow = ref({
boxShadow: '0 0 0 #000'
})
function handleHeaderAnimation(numericalValue) {
searchShadow.value = {
boxShadow: `0 0 ${ numericalValue * 15 }px #EFEFEF inset`
}
}
onLoad(() => {
main.init()
// 导航栏距顶距离
uni.getSystemInfo({
//获取系统信息
success: res => {
statusBarHeight.value = res.statusBarHeight;
},
fail(err) {
// console.log(err);
}
});
})
const {shareAppMessage,shareTimeline} = useShare();
onShareAppMessage(shareAppMessage)
onShareTimeline(shareTimeline)
const {scrollTop} = useScroll()
</script>
<style lang="scss">
.home-container {
width: 100%;
position: relative;
.header-row {
.logo-col {
@include useFlex(flex-start, center);
width: 126rpx;
aspect-ratio: 126 / 50;
.logo {
width: 100%;
height: 100%;
}
}
.search-col {
@include useFlex(flex-start, center);
@include usePadding(30, 15);
width: 100%;
height: 60rpx;
border-radius: 50rpx;
background: $white-color;
margin-left: 50rpx;
.search-input {
margin-left: 30rpx;
font-size: 24rpx;
color: #999999;
}
}
}
}
.goods-row {
padding: 0 8rpx;
}
</style>

View File

@ -0,0 +1,47 @@
<!--
@name: 空购物车
@author: kahu4
@date: 2023-11-06 15:59
@descriptionCartEmpty
@update: 2023-11-06 15:59
-->
<script setup>
import Empty from '@/components/Empty/index.vue'
import { emptyCartIcon } from "@/utils/images";
import { useRouter } from "@/hooks/useRouter";
const {pushToTab} = useRouter()
</script>
<template>
<Empty
:iconSrc="emptyCartIcon"
:padding="'220rpx 0 0 0'"
>
<template #default>
购物车里空空如也
</template>
<template #bottom>
<view
class="go-on-btn"
@click="pushToTab({url:'/root/index/index'})"
>
继续逛逛
</view>
</template>
</Empty>
</template>
<style
scoped
lang="scss"
>
.go-on-btn {
padding: 15rpx 50rpx;
margin-top: 20rpx;
background: #333333;
color: #fff;
}
</style>

View File

@ -0,0 +1,30 @@
/**
* @name: index.data
* @author: kahu4
* @date: 2023-11-06 14:15
* @descriptionindex.data
* @update: 2023-11-06 14:15
* */
// 购物车统计信息
export const settleFields = [
{
label: '商品总价',
field: 'costPrice',
prefix: '¥'
},
{
label: '优惠',
field: 'couponPrice',
prefix: '-¥'
},
{
label: '运费',
field: 'storePostage',
prefix: '¥'
},
{
label: '总计',
field: 'totalPrice',
prefix: '¥'
}
]

View File

@ -0,0 +1,319 @@
/**
* @name: 购物车相关操作方法
* @author: kahu4
* @date: 2023-11-06 15:03
* @descriptionindex.utils
* @update: 2023-11-06 15:03
* */
import { computed, nextTick, ref, unref } from "vue";
import { changeCartSku, computeSelectInfo, deleteCartByIds, getCartList, updateCartNumber } from "@/api/cart";
import { onShow } from '@dcloudio/uni-app'
import _ from "loadsh";
import { useInterface } from "@/hooks/useInterface";
import { useRouter } from "@/hooks/useRouter";
import { getProductDetail } from "@/api/product";
/**
* 购物车数据
*/
export function useCartData() {
const cartListLoading = ref(false)
const cartList = ref([])
const showEmpty = computed(() => cartList.value.length <= 0)
/**
* 获取购物车列表
* @returns {Promise<void>}
*/
async function doGetCartList() {
try {
cartListLoading.value = true
const res = await getCartList()
cartList.value = res?.valid ?? []
} finally {
cartListLoading.value = false
}
}
onShow(async () => {
await doGetCartList()
})
return {
showEmpty,
cartListLoading,
cartList,
doGetCartList
}
}
/**
* 用户操作相关
*/
export function useCartOption(options) {
const {cartList, doGetCartList} = options
const {toast} = useInterface()
const {push} = useRouter()
const manage = ref(false)
const manageStr = computed(() => {
return manage.value ? '取消' : '管理'
})
const shoppingSelect = ref([]) // 选中数据
const shoppingSelectAll = ref(false) // 是否全选
/**
* 用户单选
* @param value
*/
async function handleSingleSelect(value) {
shoppingSelectAll.value = value.length === cartList.value.length
setTimeout(async () => {
await computeSelectInfoByShoppingSelect()
}, 100)
}
/**
* 用户全选
* @param e
* @returns {Promise<void>}
*/
async function handleSelectAll(e) {
shoppingSelect.value = !!e ? cartList.value.map(item => item.id) : []
shoppingSelectAll.value = e
await computeSelectInfoByShoppingSelect()
}
const statisticsInfo = ref(undefined) // 统计信息
/**
* 根据shoppingSelect去计算选中数据
* @returns {Promise<void>}
*/
async function computeSelectInfoByShoppingSelect() {
if (unref(shoppingSelect).length <= 0) return statisticsInfo.value = void (0)
const res = await computeSelectInfo({
cartId: unref(shoppingSelect).join(','),
orderType: 1,
useIntegral: false
});
statisticsInfo.value = res.priceGroup
}
/**
* 重新设置选中信息
* @returns {Promise<void>}
*/
async function resetUserSelect() {
shoppingSelect.value = []
shoppingSelectAll.value = false
await computeSelectInfoByShoppingSelect()
}
/**
* 打开删除弹窗
* @returns {*}
*/
function openDelModal(modalRef) {
if (unref(shoppingSelect).length <= 0) return toast({title: '请勾选需要删除的商品'})
unref(modalRef).show()
}
/**
* 删除数据
* @returns {Promise<void>}
*/
async function doDelete() {
await deleteCartByIds({
ids: shoppingSelect.value
})
await doGetCartList()
await resetUserSelect()
}
/**
* 提交订单
* @returns {*}
*/
function submitOrder() {
if (unref(shoppingSelect).length <= 0) return toast({title: '请勾选需要购买的商品'})
push({url: '/pages/submitOrder/submitOrder'}, {
data: {cartId: shoppingSelect.value.toString()}
})
}
return {
manage,
manageStr,
shoppingSelect,
shoppingSelectAll,
statisticsInfo,
handleSingleSelect,
handleSelectAll,
computeSelectInfoByShoppingSelect,
openDelModal,
doDelete,
submitOrder
}
}
/**
* 更改sku
*/
export function useSku() {
const {toast} = useInterface()
const openSkuProductId = ref(undefined) // 当前选中的sku商品id
const openSkuSkuId = ref(undefined) // 当前选中sku的sku id
const openSkuCartId = ref(undefined)
const openProductItem = ref(undefined)
/**
* 获取商品详情
* @param data
* @param data.skuId skuId
* @param data.productId productId
* @return {Promise<void>}
*/
const handleGetDetail = async (data) => {
openProductItem.value = await getProductDetail(data);
};
/**
* 打开sku选择器
* @param item
* @param modalRef
*/
async function handleOpenSkuSelect(item, modalRef) {
await handleGetDetail({productId: item.productId})
openSkuSkuId.value = item.productInfo.attrInfo.id
openSkuProductId.value = item.productId
openSkuCartId.value = item.id
await nextTick(() => {
modalRef.open(item.cartNum)
})
}
/**
* 关闭sku选择器
*/
function handleCloseSkuSelect() {
openSkuProductId.value = openSkuCartId.value = void (0)
}
async function handleSubmitSkuSelect(e, modalRef, cartList, func, doGetCartList) {
if (!openSkuProductId.value) return
const {store, num} = e
await changeCartSku({
id: openSkuCartId.value,
productId: openSkuProductId.value,
productAttrUnique: store.unique
})
if (typeof func === 'function') {
const find = cartList.find(item => item.id === openSkuCartId.value);
await func({detail: {value: num}}, find)
}
setTimeout(async () => {
await doGetCartList()
toast({title: '修改成功', icon: 'success'})
handleCloseSkuSelect()
}, 400)
}
return {
openProductItem,
openSkuSkuId,
openSkuProductId,
handleOpenSkuSelect,
handleCloseSkuSelect,
handleSubmitSkuSelect
}
}
/**
* 更改购物车数量
*/
export function useCartNumber(options) {
const {toast} = useInterface()
/**
* 用户手动输入改变数量
* @param e
* @param item
* @returns {*}
*/
function handleCartNumberInputChange(e, item) {
const value = parseInt(e.detail.value.toString().replace(/^0+/, ''))
if (value <= 0) {
item.cartNum = 1
toast({title: '至少选一件哦~'})
return
}
if (value > item.trueStock) {
item.cartNum = item.productInfo.stock
toast({title: '超出库存啦~'})
return
}
nextTick(()=>{
item.cartNum = value
doCartNumberChangeRequest(item)
})
}
/**
* 购买数量验证
*/
function cartNumberInput(e, item){
const pattern = /^0+|[.]*/g;
nextTick(() => {
item.cartNum = e.detail.value.replace(pattern,'');
})
}
/**
* 用户点击购物车+-改变数量
* @param item
* @param type
* @returns {*}
*/
function handleCartNumberChange(item, type = 'plus') {
if (type === 'plus') {
if (item.cartNum + 1 > item.trueStock) {
item.cartNum = item.trueStock
} else {
item.cartNum += 1
}
} else {
if (item.cartNum <= 1) {
item.cartNum = 1
} else {
item.cartNum -= 1
}
}
doCartNumberChangeRequest(item)
}
/**
* 请求改变后台用户购物车数据
* @param item
* @returns {Promise<void>}
*/
const doCartNumberChangeRequest = _.debounce(async (item) => {
await updateCartNumber({
id: item.id,
number: item.cartNum
})
options && options.afterChange && await options.afterChange()
}, 300)
return {
handleCartNumberInputChange,
handleCartNumberChange,
cartNumberInput
}
}

View File

@ -0,0 +1,418 @@
<template>
<Header
:scroll-top="scrollTop"
system-bar-area-bg="#fff"
header-area-bg="#fff"
bg-change-by-scroll
>
购物车
</Header>
<!-- have data -->
<view v-if="!showEmpty">
<!-- manage row -->
<view class="cart-manage">
<view @click="manage=!manage">
{{ manageStr }}
</view>
</view>
<!-- cart body box-->
<uv-checkbox-group
v-model="shoppingSelect"
shape="circle"
activeColor="#ee6d46"
@change="handleSingleSelect"
>
<card class="shopping-checkbox">
<!-- 购物车信息 -->
<view
v-for="(item) in cartList"
:key="item.id"
class="shopping-item"
>
<uv-checkbox
:name="item.id"
/>
<view class="good">
<Goods
row
imgWidth="200rpx"
info-padding="0 0 0 40rpx"
:goods="item.productInfo"
>
<template #options>
<view class="goods-options">
<!-- sku select -->
<view class="sku-row flex">
<view
class="sku-info flex flex-jc__sb flex-ai__center"
@click.stop="handleOpenSkuSelect(item,goodsAttrSelectRef)"
>
<view class="info">
{{ item.productInfo && item.productInfo.attrInfo && item.productInfo.attrInfo.sku }}
</view>
<uv-icon
class="icon"
name="arrow-down"
color="#ccc"
size="12"
/>
</view>
</view>
<!-- bottom -->
<view class="price-row flex flex-ai__center flex-jc__sb">
<!-- price -->
<view class="price-box flex flex-ai__end">
{{ item.truePrice }}
<view class="old-price">
{{ item.productInfo.otPrice }}
</view>
</view>
<!-- cart number -->
<view
class="cart-num flex flex-ai__center flex-jc__sb"
@click.stop=""
>
<view
class="button"
:class="item.cartNum <= 1 && 'disabled'"
@click="handleCartNumberChange(item,'minus')"
>
<uv-icon
name="minus"
color="#333"
size="24rpx"
></uv-icon>
</view>
<view class="input">
<input
type="number"
inputmode="numeric"
v-model="item.cartNum"
@blur="(e)=>handleCartNumberInputChange(e,item)"
@input="(e)=>cartNumberInput(e,item)"
>
</view>
<view
class="button"
:class="item.cartNum >= item.trueStock && 'disabled'"
@click="handleCartNumberChange(item,'plus')"
>
<uv-icon
name="plus"
color="#333"
size="24rpx"
></uv-icon>
</view>
</view>
</view>
</view>
</template>
</Goods>
</view>
</view>
<!-- 选中了的结算统计信息 -->
<view :class="{'select-product-settle-info':true,show:shoppingSelect.length>0}">
<view
class="row flex flex-ai__center flex-jc__sb"
v-for="(item,index) in settleFields"
:key="index"
>
<view class="label">
{{ item.label }}
</view>
<view v-if="statisticsInfo">
{{ item.prefix }} {{ statisticsInfo[item.field].toFixed(2) }}
</view>
</view>
</view>
</card>
</uv-checkbox-group>
<!-- bottom action row -->
<view class="screen action-bar ">
<view class="action-info">
<view class="action-checkbox">
<uv-checkbox-group
shape="circle"
activeColor="#ee6d46"
>
<uv-checkbox
name="all"
:checked="shoppingSelectAll"
@change="handleSelectAll"
>
全选
</uv-checkbox>
</uv-checkbox-group>
</view>
</view>
<view class="action-btns">
<view
class="action-total"
v-show="!manage"
>
总计{{ statisticsInfo ? statisticsInfo.totalPrice.toFixed(2) : "0.00" }}
</view>
<view
class="button"
v-if="!manage"
@click="submitOrder"
>结算
</view>
<view
v-else
class="button"
@click="openDelModal(modalRef)"
>
删除
</view>
</view>
</view>
</view>
<!-- null data -->
<CartEmpty v-else />
<!-- 商品推荐 -->
<Recommend />
<view class="action-height"></view>
<view class="h5-tabbar-height"></view>
<!-- sku select -->
<GoodAttrSelect
style="z-index: 999"
:id="openSkuProductId"
ref="goodsAttrSelectRef"
:goods-detail="openProductItem"
:sku-id="openSkuSkuId"
@select="(e)=>handleSubmitSkuSelect(e, goodsAttrSelectRef,cartList,handleCartNumberInputChange,doGetCartList)
"
/>
<!-- delete modal -->
<Modal
ref="modalRef"
content="确认要删除这些购物车数据吗?"
@confirm="doDelete"
/>
<ReturnTop :scroll-top="scrollTop" />
</template>
<script setup>
import { ref } from 'vue'
import Goods from "@/components/goodsComponents/Goods.vue";
import GoodAttrSelect from "@/components/good-attr-select/good-attr-select.vue";
import Modal from "@/components/Modal/index.vue"
import { settleFields } from "@/root/shoppingCart/index.data";
import { useCartData, useCartNumber, useCartOption, useSku } from "@/root/shoppingCart/index.utils";
import CartEmpty from "@/root/shoppingCart/components/CartEmpty.vue";
import Header from "@/components/Header/index.vue"
import { onHide } from "@dcloudio/uni-app";
import Recommend from "@/components/Recommend/index.vue";
import { useScroll } from "@/hooks/useScroll";
import ReturnTop from "@/components/ReturnTop/index.vue"
const modalRef = ref() // 删除弹窗
const goodsAttrSelectRef = ref() // 更改sku
const {
showEmpty,
cartList,
doGetCartList
} = useCartData();
const {
manage,
manageStr,
shoppingSelect,
shoppingSelectAll,
handleSingleSelect,
handleSelectAll,
statisticsInfo,
computeSelectInfoByShoppingSelect,
openDelModal,
doDelete,
submitOrder,
} = useCartOption({cartList, doGetCartList})
const {
openSkuSkuId,
openProductItem,
openSkuProductId,
handleOpenSkuSelect,
handleCloseSkuSelect,
handleSubmitSkuSelect
} = useSku()
const {
handleCartNumberInputChange,
handleCartNumberChange,
cartNumberInput
} = useCartNumber({afterChange: computeSelectInfoByShoppingSelect})
onHide(() => {
modalRef.value?.close()
goodsAttrSelectRef.value?.close()
})
const {scrollTop} = useScroll()
</script>
<style lang="scss">
.cart-manage {
padding: 20rpx 34rpx;
font-size: 28rpx;
color: #333333;
background-color: #fff;
display: flex;
justify-content: flex-end;
border-bottom: 1rpx solid #f9f9f9;
}
// 商品SKU 数量等操作条
.goods-options {
width: 100%;
.sku-row {
margin-bottom: 30rpx;
.sku-info {
@include usePadding(10, 4);
border: 1rpx solid #ccc;
border-radius: 5rpx;
font-size: 24rpx;
transition: all .3s;
max-width: 100%;
.info {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
&:active {
scale: 1.1;
}
.icon {
margin-left: 15rpx;
}
}
}
.price-row {
width: 100%;
.price-box {
font-size: 30rpx;
.old-price {
font-size: 20rpx;
color: $tips-color;
text-decoration: line-through;
margin-left: 10rpx;
}
}
.cart-num {
font-size: 24rpx;
.input {
width: 70rpx;
input {
width: 100%;
text-align: center;
color: #333;
}
}
.button {
font-size: 32rpx;
width: 40rpx;
height: 40rpx;
aspect-ratio: 1/1;
border-radius: 5rpx;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
transition: all .3s;
border: 1px solid #999999;
&:active {
scale: 1.2;
}
&.disabled {
border-color: #dddddd;
:deep(.uv-icon__icon) {
color: #dddddd !important;
}
&:active {
scale: 1;
}
}
}
}
}
}
.action-bar {
z-index: 89;
height: 100rpx;
.action-btns {
height: 100%;
padding: 0 0;
.action-total {
padding-right: 30rpx;
font-size: 34rpx;
}
.button {
margin: 0 0;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 0 50rpx;
background: $primary-color;
color: $white-color;
}
}
}
// #ifndef H5
.action-height {
width: 100%;
height: 100rpx;
background: #fff;
}
// #endif
.shopping-checkbox {
width: 100%;
.shopping-item {
display: flex;
align-items: center;
padding: 0 20rpx;
box-sizing: border-box;
margin-bottom: 50rpx;
.good {
width: 93%;
position: relative;
}
}
}
</style>

View File

@ -0,0 +1,171 @@
<!--
@name: GridCard
@author: kahu4
@date: 2023-11-09 15:09
@descriptionGridCard
@update: 2023-11-09 15:09
-->
<script setup>
import { toRefs } from "vue";
import { useRouter } from "@/hooks/useRouter";
import { useInterface } from "@/hooks/useInterface";
import { storeToRefs } from "pinia";
import { useMainStore } from "@/store/store";
import { useService } from "@/hooks/useService";
const props = defineProps({
list: {
type: Array,
default: () => ([])
},
dotInfo: {
type: Object
},
title: {
type: String,
default: () => ''
},
buttonText: {
type: String,
default: ''
}
})
const {list, title, buttonText, dotInfo} = toRefs(props)
const emits = defineEmits(['buttonClick'])
const {push} = useRouter();
const {toast} = useInterface();
const mainStore = useMainStore();
const {user} = storeToRefs(mainStore);
async function toLink(listItem) {
if (!user.value) return toast({title: '请先登录'})
if (!listItem.path) return toast({title: ' 暂未开放 '})
if (listItem.path === 'kf') {
const {getServiceData,openService} = useService();
await getServiceData()
await openService()
return
}
push({url: listItem.path}, listItem?.params ?? {})
}
</script>
<template>
<view class="grid-container">
<view
class="title-row"
v-if="title || buttonText"
>
<view>
{{ title }}
</view>
<view
class="right"
@click="emits('buttonClick')"
>
{{ buttonText }}
<uv-icon
v-if="buttonText"
name="arrow-right"
color="#ccc"
size="12"
/>
</view>
</view>
<view class="icon-box">
<view
class="icon-item"
v-for="item in list"
:key="item"
@click="toLink(item)"
>
<template v-if="item&&item.rightTopDot">
<view
class="dot"
v-if="dotInfo && dotInfo[item.dotField] && dotInfo[item.dotField]>0"
>
{{ dotInfo[item.dotField] < 100 ? dotInfo[item.dotField] : `${ dotInfo[item.dotField] }+` }}
</view>
</template>
<image
class="icon"
:src="item.icon"
/>
<view class="text">
{{ item.label }}
</view>
</view>
</view>
</view>
</template>
<style
scoped
lang="scss"
>
.grid-container {
width: 100%;
background: $white-color;
border-radius: 15rpx;
margin: 20rpx 0;
.title-row {
@include useFlex(space-between, center);
@include usePadding(30, 30);
font-size: 32rpx;
color: #333;
font-weight: 500;
border-bottom: 1rpx solid #f5f5f5;
.right {
@include useFlex(space-between, center);
font-size: 24rpx;
color: $tips-color;
font-weight: normal;
}
}
.icon-box {
@include usePadding(30, 20);
display: grid;
grid-template-columns: repeat(4, 1fr);
.icon-item {
@include useFlex(space-between, center, column);
width: 100%;
font-size: 24rpx;
color: #333333;
position: relative;
.dot {
z-index: 99;
position: absolute;
background: #EE6D46;
color: #fff;
right: 20%;
top: 0;
transform: translateY(-20%);
font-size: 18rpx;
width: 38rpx;
height: 38rpx;
text-align: center;
line-height: 38rpx;
box-sizing: border-box;
border-radius: 50%;
border: 2rpx solid #ffffff;
}
.icon {
width: 60rpx;
height: 60rpx;
}
.text {
margin: 14rpx 0;
}
}
}
}
</style>

172
root/user/index.data.js Normal file
View File

@ -0,0 +1,172 @@
/**
* @name: index.data
* @author: kahu4
* @date: 2023-11-09 15:21
* @descriptionindex.data
* @update: 2023-11-09 15:21
* */
import {
myScan,
toDZIcon,
toFHIcon,
toKFIcon,
toKJIcon,
toPayIcon,
toPJIcon,
toSCIcon,
toSHIcon,
toSHOIcon,
toTGIcon,
toYHQIcon,
toZBIcon,
toZHIcon,
toZJIcon
} from "@/utils/images";
import { useJump } from "@/hooks/useJump";
const {goIntegral, goBalance, goCoupon} = useJump()
export const orderIconList = [
{
id: 1,
label: '待付款',
icon: toPayIcon,
rightTopDot: true,
dotField: 'unpaidCount',
path: '/pages/orderList/orderList',
params: {data: {type: 0}}
},
{
id: 2,
label: '待发货',
icon: toFHIcon,
rightTopDot: true,
dotField: 'unshippedCount',
path: '/pages/orderList/orderList',
params: {data: {type: 1}}
},
{
id: 3,
label: '待收货',
icon: toSHIcon,
rightTopDot: true,
dotField: 'receivedCount',
path: '/pages/orderList/orderList',
params: {data: {type: 2}}
},
{
id: 4,
label: '待评价',
icon: toPJIcon,
rightTopDot: true,
dotField: 'evaluatedCount',
path: '/pages/orderList/orderList',
params: {data: {type: 3}}
},
]
export const cardOneList = [
{
id: 1,
label: '我的足迹',
icon: toZJIcon,
path: '/pages/footprint/footprint',
},
{
id: 2,
label: '优惠券',
icon: toYHQIcon,
path: '/pages/discountCoupon/index',
},
{
id: 3,
label: '我的收藏',
icon: toSCIcon,
rightTopDot: false,
dotField: 'receivedCount',
path: '/pages/collect/collect',
},
{
id: 4,
label: '开启直播',
icon: toZBIcon,
rightTopDot: false,
dotField: 'evaluatedCount',
path: '',
params: {data: {type: 3}}
},
{
id: 5,
label: '我的推广',
icon: toTGIcon,
path: '/views/distribution/center/index',
params: {}
},
{
id: 6,
label: '砍价记录',
icon: toKJIcon,
path: '',
params: {data: {type: 1}}
},
{
id: 7,
label: '售后记录',
icon: toSHOIcon,
rightTopDot: false,
dotField: 'receivedCount',
path: '/pages/refundList/refundList',
params: {data: {type: -1}}
},
{
id: 8,
label: '联系客服',
icon: toKFIcon,
rightTopDot: false,
dotField: 'evaluatedCount',
path: 'kf',
params: {data: {type: 3}}
},
]
export const cardTwoList = [
{
id: 1,
label: '地址管理',
icon: toDZIcon,
path: '/pages/address/address',
},
{
id: 2,
label: '账号管理',
icon: toZHIcon,
path: '/pages/userInfo/index',
},
{
id: 3,
label: '订单核销',
icon: myScan,
path: '/views/activity/afterVerification/index',
},
]
export const accountList = [
{
id: 1,
label: '我的余额',
field: 'nowMoney',
path: goBalance
},
{
id: 2,
label: '我的积分',
field: 'integral',
path: goIntegral
},
{
id: 3,
label: '优惠券',
field: 'couponNumber',
path: goCoupon
}
]

388
root/user/user.vue Normal file
View File

@ -0,0 +1,388 @@
<template>
<view class="mine-container">
<Header :show-return="false">我的</Header>
<!-- user info -->
<view class="userinfo-box">
<!-- 未登录 -->
<view
class="userinfo-box__inner no-login"
v-if="!(user && user.id)"
@click="toLogin"
>
<view class="flex flex-ai__center">
<image
class="head"
:src="defaultAvatarIcon"
/>
<view class="user-info">
点击登录
</view>
</view>
</view>
<!-- 已登录 -->
<view
class="userinfo-box__inner"
@click="toUserCenter"
v-else
>
<view class="flex flex-ai__center">
<image
class="head"
:src="user.avatar"
/>
<view class="user-info">
{{ user.nickname }}
</view>
</view>
<view
class="sign-box"
@click.stop="goSignIn">
<image :src="mySignIn" />
签到
</view>
</view>
</view>
<!-- 账户信息 -->
<view class="account-box">
<template
v-for="item in accountList"
:key="item.id">
<view
v-if="user"
class="account-item"
@click.stop="handleJump(item)">
<view class="count">
{{ user[item.field] || 0 }}
</view>
<view class="title">
{{ item.label }}
</view>
</view>
</template>
</view>
<!-- VIP 信息 未激活 -->
<view
class="vip-box vip-none"
@click="goMemberCenter"
v-if="!memberLeverInfo.currentLevel">
<view
class="vip-box__inner flex flex-ai__center flex-jc__sb"
:style="{backgroundImage:`url(${myVip1})`}">
<image
class="icon"
:src="noneVip" />
<view class="vip-text">
开通享更多特权省钱又省心
</view>
<view class="vip-button">
立即激活
</view>
</view>
</view>
<!-- VIP 信息 激活 -->
<view
class="vip-box"
@click="goMemberCenter"
v-else>
<view
class="vip-box__inner "
:style="{backgroundImage:`url(${myVip1})`}">
<view class="flex flex-ai__center flex-jc__sb">
<image
class="icon"
:src="memberLeverInfo.currentLevel.iconUrl" />
<view class="vip-text flex flex-ai__center">
{{ memberLeverInfo.currentLevel.levelName }}
<view class="process">
<view
class="schedule"
:style="{width: `${memberLeverInfo.needGrowthValue/memberLeverInfo.nextLevel.growthValue}%`}"></view>
</view>
</view>
<view class="vip-button">
查看权益 >
</view>
</view>
<view class="tips">
再获取{{ memberLeverInfo.needGrowthValue }}经验可升级为{{ memberLeverInfo.nextLevel.levelName }}会员
</view>
</view>
</view>
<!-- 大卡片 -->
<view class="big-card">
<!-- order card -->
<GridCard
:list="orderIconList"
:dot-info="orderUserCountData"
title="我的订单"
button-text="查看所有订单"
@button-click="toAllOrder"
/>
<!-- footprint card -->
<GridCard
:list="cardOneList"
:dot-info="orderUserCountData"
/>
<GridCard
:list="filterCardTwo"
:dot-info="orderUserCountData"
/>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import Header from '@/components/Header/index.vue'
import { orderUserCount } from '@/api/order'
import { onShow } from '@dcloudio/uni-app'
import { useMainStore } from '@/store/store'
import { useRouter } from "@/hooks/useRouter";
import { storeToRefs } from "pinia";
import GridCard from "@/root/user/components/GridCard.vue";
import { accountList, cardOneList, cardTwoList, orderIconList } from "@/root/user/index.data";
import { defaultAvatarIcon, mySignIn, myVip1, myVipNone, noneVip } from "@/utils/images";
import { useInterface } from "@/hooks/useInterface";
import { useJump } from "@/hooks/useJump";
import { getUserMemberLevel } from "@/api/member";
const mainStore = useMainStore()
const {user} = storeToRefs(mainStore);
const {push} = useRouter()
const {toast} = useInterface();
const {goSignIn, goMemberCenter} = useJump()
const orderUserCountData = ref(null)
const filterCardTwo = computed(() => {
if (!user.value) return []
// 判断是否又核销权限
if (user.value.writeOffAuthority) {
return cardTwoList
} else {
return cardTwoList.filter(item => item.label !== '订单核销')
}
})
function toAllOrder() {
if (!user.value) return toast({title: '请先登录'})
push({url: '/pages/orderList/orderList'}, {data: {type: -1}})
}
function toUserCenter() {
if (!user.value) return toast({title: '请先登录'})
push({url: '/pages/userInfo/index'})
}
function toLogin() {
push({url: '/pages/login/guid'})
}
const handleOrderUserCount = async () => {
orderUserCountData.value = await orderUserCount()
}
// =============================== 会员信息 ====================================
const memberLeverInfo = ref({
currentGrowthValue: 0,
currentLevel: null,
needGrowthValue: 0,
nextLevel: null
})
async function doGetUserMemberLevel() {
memberLeverInfo.value = await getUserMemberLevel()
}
function handleJump(item) {
if (!item.path) return toast({title: '暂未开放~'})
if (typeof item.path === 'function') {
item.path()
}
}
onShow(() => {
mainStore.getUserInfo()
handleOrderUserCount()
doGetUserMemberLevel()
})
</script>
<style
lang="scss"
scoped
>
.mine-container {
background: linear-gradient(to bottom, #fff 0%, #fff 28%, #b0b0b0 40%, #fff 41%);
.userinfo-box {
@include usePadding(32, 0);
width: 100%;
margin: 50rpx 0 0 0;
&__inner {
@include useFlex(space-between, center);
.head {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
border: 5rpx solid #fff;
}
.user-info {
@include useFlex(space-between, center);
padding-left: 30rpx;
font-size: 34rpx;
}
.sign-box {
@include useFlex(center, center);
@include usePadding(16, 12);
border: 1rpx solid #E5E5E5;
font-weight: bold;
font-size: 24rpx;
border-radius: 50rpx;
image {
width: 40rpx;
height: 40rpx;
}
}
&.no-login {
}
}
}
.account-box {
width: 100%;
display: grid;
grid-template-columns: repeat(3, 1fr);
margin: 32rpx 0;
.account-item {
@include useFlex(center, center, column);
font-weight: bold;
font-size: 32rpx;
color: #333;
position: relative;
&:after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 1rpx;
height: 50%;
background: $tips-color;
}
&:last-child {
&:after {
width: 0;
}
}
.title {
color: $tips-color;
font-size: 24rpx;
font-weight: normal;
}
}
}
.vip-box {
width: 100%;
height: 132rpx;
@include usePadding(32, 0);
&__inner {
@include usePadding(32, 25);
width: 100%;
height: 100%;
background-repeat: no-repeat;
background-size: 100% 100%;
.icon {
width: 56rpx;
height: 56rpx;
}
.vip-text {
width: 60%;
color: #FFF8E8;
font-weight: bold;
font-size: 28rpx;
.process {
flex-grow: 1;
height: 10rpx;
margin-left: 12rpx;
border-radius: 10rpx;
overflow: hidden;
background: rgba(255, 255, 255, 0.1);
position: relative;
.schedule {
height: 100%;
background: #FFF8E8;
border-radius: 10rpx;
left: 0;
top: 0;
}
}
}
.vip-button {
@include usePadding(0, 10);
color: #FFF8E8;
font-size: 24rpx;
border-radius: 8rpx;
}
.tips {
margin-top: 10rpx;
color: #FFF8E8;
font-size: 20rpx
}
}
}
.vip-none {
.vip-button {
@include usePadding(24, 10);
background: linear-gradient(45deg, #FAEECB, #F2D7A9);
color: #272A3F;
font-size: 24rpx;
border-radius: 8rpx;
}
}
.big-card {
width: 100%;
background: #f6f6f6;
border-radius: 30rpx;
@include usePadding(32, 32);
}
}
</style>