代码提交

This commit is contained in:
黄少君
2023-11-15 19:59:37 +08:00
parent dcab74274f
commit 35b43ffd97
43 changed files with 1265 additions and 387 deletions

47
.gitignore vendored Normal file
View File

@ -0,0 +1,47 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
/unpackage/dist/dev
/unpackage/dist/static
/unpackage/dist/build/mp-alipay/
/unpackage/dist/build/mp-weixin/
/unpackage/dist/build/app-plus/
/unpackage/dist/build/.automator
/unpackage/cache/
/unpackage/cache/apk
/unpackage/release
/unpackage/res
/unpackage/resources
# misc
.DS_Store
.cache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.tmp*
.svn
.tags
*.sublime-*
sftp-config.json
logs
*.log
.idea*
.yo-rc.json
*.swo
*.swp
/deps
yarn.lock
dev-stats.json
.vscode
.idea
*.hbuilderx
.history

View File

@ -144,3 +144,21 @@ const request = ['post', 'put', 'patch'].reduce((request, method) => {
})
export default request
export const upload = (options)=>{
const token = cookie.get('accessToken')
return new Promise((resolve, reject)=>{
uni.showLoading({title:'上传中'})
uni.uploadFile({
...options,
header:{
...options.headers,
Authorization: 'Bearer ' + token.accessToken,
},
url:options.url?`${VUE_APP_API_URL}${options.url}`:VUE_APP_API_URL+'/member/user/update-avatar',
success:(res)=>resolve(res),
fail:(err)=>reject(err),
complete:()=>uni.hideLoading()
})
})
}

View File

@ -149,8 +149,8 @@ export function orderExpress(data) {
export const wechatPay = (data) => api.post(`/order/pay`, data)
/**
* 检查h5支付
* 检查支付
* @param data
* @returns {*}
*/
export const checkH5Pay = (data) => api.post(`/order/pay/orderQuery`, data)
export const checkPay = (data) => api.post(`/order/pay/orderQuery`, data)

View File

@ -41,3 +41,4 @@ export function updateMobile(data) {
* @returns {*}
*/
export const updateUserInfo = (data) => api.put(`/member/user/update-nickname?nickname=${ data.nickname }&birthday=${ data.birthday }&sex=${ data.sex }`)

View File

@ -10,6 +10,7 @@ import { computed, defineProps, ref, toRefs, unref, watch } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { useRouter } from "@/hooks/useRouter";
import { createAnimation } from "@/utils/utils";
import { useScroll } from "@/hooks/useScroll";
const HEADER_HEIGHT = 40 // header高度
@ -66,10 +67,6 @@ const props = defineProps({
type: String,
default: () => '#fff'
},
scrollTop: {
type: Number,
default: () => 0
},
propUp: {
type: Boolean,
default: () => true
@ -94,7 +91,6 @@ const {
textShadow,
bgChangeByScroll,
bgChangeColor,
scrollTop,
propUp,
showRight,
leftWidth
@ -146,7 +142,7 @@ function getMenuInfo() {
// scss全局变量
const scssVarStyle = computed(() => {
return {
'--header-height': `${ HEADER_HEIGHT * 2 }rpx`
'--header-height': `${ HEADER_HEIGHT }px`
}
})
@ -154,7 +150,7 @@ const scssVarStyle = computed(() => {
const systemBarAreaStyle = computed(() => {
return {
width: '100%',
height: `${ unref(heightInfo).safeAreaInsets.top * 2 }rpx`,
height: `${ unref(heightInfo).statusBarHeight * 2 }rpx`,
background: unref(systemBarAreaBg)
}
})
@ -163,7 +159,7 @@ const systemBarAreaStyle = computed(() => {
const headerAreaStyle = computed(() => {
// 计算margin top
// margin-top (导航条高度 - 胶囊高度) / 2 永远确保胶囊在header中央
const marginTop = unref(menuInfo).height > 0 ? `-${ ((HEADER_HEIGHT - (unref(menuInfo).height)) / 2) * 2 }rpx` : 0
const marginTop = unref(menuInfo).height > 0 ? `-${((HEADER_HEIGHT - unref(menuInfo).height))/2}px` : 0
return {
width: '100%',
background: unref(headerAreaBg),
@ -202,7 +198,7 @@ const scrollMaskStyle = computed(() => {
// 总高度
const containerHeight = computed(() => {
return (unref(heightInfo).safeAreaInsets.top + HEADER_HEIGHT) * 2
return (unref(heightInfo).statusBarHeight + HEADER_HEIGHT)
})
let animation
@ -212,6 +208,7 @@ function doCreateAnimation() {
animation = createAnimation(0, scrollEnd, 0, 1)
}
const {scrollTop} = useScroll();
watch(scrollTop, () => {
if (!bgChangeByScroll.value) return
if (!animation) doCreateAnimation()
@ -289,7 +286,7 @@ onLoad(() => {
<!-- 撑起 -->
<view
class="prop-up"
:style="{height:`${containerHeight}rpx`}"
:style="{height:`${containerHeight}px`}"
v-if="propUp"
></view>
</view>

View File

@ -126,7 +126,7 @@ defineExpose({show, close})
text-align: center;
height: 80rpx;
line-height: 80rpx;
border: 1rpx solid #ee6d46;
border: 1px solid #ee6d46;
background: #ee6d46;
color: #fff;
}

View File

@ -52,7 +52,7 @@ async function handleSubmit() {
close()
} catch (e) {
console.error(e)
push({url: '/pages/payStatus/index?type=2'})
push({url: '/pages/payStatus/index'},{data:{type:2}})
toast({title: '支付失败了'})
close()
}

View File

@ -10,7 +10,7 @@ import { nextTick, ref, toRefs } from "vue";
import UniPopup from "@/components/uniComponents/UPopup/uni-popup/uni-popup.vue";
/** some javascript code in here */
const emit = defineEmits(['open', 'close'])
const emit = defineEmits(['open', 'close', 'maskClick'])
/**
* @property {String} title 标题
* @property {String} mode 模式
@ -73,6 +73,10 @@ const handlePopupChange = (e) => {
if (!e.show) emit('close')
}
const handleMaskClick = (e) => {
emit('maskClick')
}
defineExpose({
show,
close
@ -86,6 +90,7 @@ defineExpose({
:is-mask-click="isMaskClick"
background-color="#fff"
@change="handlePopupChange"
@maskClick="handleMaskClick"
class="y-popup"
>
<view class="popup_inner">

View File

@ -0,0 +1,192 @@
<!--
@name: 新品首发
@author: kahu4
@date: 2023-10-27 14:42
@descriptionindex
@update: 2023-10-27 14:42
-->
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { usePage } from "@/hooks";
import { getProductList } from "@/api/product";
import Empty from "@/components/Empty/index.vue"
import ListLoadOver from "@/components/ListLoadOver/index.vue"
import ListLoadLoading from "@/components/ListLoadLoading/index.vue"
import { useRouter } from "@/hooks/useRouter";
import emptyIcon from "@/static/icon/empty/收藏.png";
import Goods from "@/components/goodsComponents/Goods.vue";
import { computed, toRefs } from "vue";
const {refresh, dataList, loadend, loading, listEmpty} = usePage(getProductList)
const {push} = useRouter()
onLoad(() => {
refresh()
})
const props = defineProps({
more:{
type:Boolean,
default:()=>true
}
})
const { more } = toRefs(props)
const colList = computed(() => [
{
name: 'all',
data: dataList.value.filter((item,index)=>index%2===0)
},
{
name:'right',
data: dataList.value.filter((item,index)=>index%2!==0)
}
])
</script>
<template>
<!--push({ url: '/pages/goodsList/goodsList' }-->
<view class="recommend-container">
<slot name="head">
<view class="title-row">
商品推荐
</view>
</slot>
<view
class="product-box"
v-if="!listEmpty"
>
<!-- 分两列 后期瀑布流 -->
<template
v-for="col in colList"
:key="col.name"
>
<view class="goods-col">
<template
v-for="item in col.data"
:key="item.id"
>
<view class="product">
<Goods
:ratio="true"
:goods="item"
infoPadding="30rpx 10rpx"
>
<template #options>
<view class="good-bottom">
<view class="price">
{{ item.price }}
</view>
<view class="sale">
仅剩{{ item.stock }}
</view>
</view>
</template>
</Goods>
</view>
</template>
</view>
</template>
</view>
<Empty
v-else
:iconSrc="emptyIcon"
>
暂时没有商品推荐哦~
</Empty>
<!-- 加载中 -->
<ListLoadLoading v-if="loading" />
<!-- 加载完毕-->
<ListLoadOver v-if="loadend" >
<template v-if="more">
<span @click="push({ url: '/pages/goodsList/goodsList' })">
浏览更多商品
</span>
</template>
<template v-else>
到底了~
</template>
</ListLoadOver>
</view>
</template>
<style
scoped
lang="scss"
>
.recommend-container{
@include usePadding(30, 20);
width: 100%;
.title-row{
width: 100%;
font-size:32rpx;
text-align: center;
position: relative;
color: #333;
font-weight: bold;
&::before,&::after{
content: '';
position: absolute;
width: 10%;
height: 3rpx;
border-radius: 5rpx;
background: #333;
top: 50%;
transform: translate(0,50%);
}
&::before{
left: 25%;
}
&::after{
right: 25%;
}
}
}
.product-box {
margin-top: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
//display: flex;
//gap: 20rpx;
.goods-col {
width: 49%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
flex-grow: 0;
float: left;
&:nth-child(2){
float: right;
}
.product {
flex-grow: 0;
width: 100%;
background: $white-color;
border-radius: 20rpx;
overflow: hidden;
margin-bottom: 20rpx;
.good-bottom {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 20rpx;
.price {
font-size: 28rpx;
}
.sale {
color: #999999;
}
}
}
}
}
</style>

View File

@ -9,6 +9,7 @@
import { toRefs } from "vue";
import { GOODS_ITEM_TYPE } from "@/components/goodsComponents/utils/index.type";
import { useRouter } from "@/hooks/useRouter";
import { getRandom } from "@/utils/utils";
const props = defineProps({
goods: {
@ -21,17 +22,17 @@ const props = defineProps({
},
/** 图片比例 */
ratio: {
type: String,
default: () => "1/1"
type: [String,Boolean],
default: () => '1/1'
},
infoPadding: {
type: String,
default: () => "0 0"
},
/** title是否换行 */
titleNowrap: {
titleWrap: {
type: Boolean,
default: () => true
default: () => false
},
/** title 文字大小 rpx */
titleSize: {
@ -53,7 +54,7 @@ const {
imgWidth,
ratio,
infoPadding,
titleNowrap,
titleWrap,
titleSize,
row,
} = toRefs(props)
@ -63,6 +64,7 @@ const {push} = useRouter()
function toDetail() {
push({url: '/pages/goodsDetail/goodsDetail'}, {data: {id: goods.value.id}})
}
</script>
<template>
@ -75,13 +77,13 @@ function toDetail() {
class="goods-image"
:style="{
'width':imgWidth,
'aspect-ratio':ratio
'aspect-ratio': ratio===true?`${1/getRandom(1,1.3)}`:ratio
}"
>
<image
:src="goods.image"
class="image"
mode="aspectFill"
:mode="ratio===true?'aspectFit':'aspectFill'"
/>
</view>
<!-- good info -->
@ -96,7 +98,7 @@ function toDetail() {
<view
:class="{
'title-row':true,
'nowrap': titleNowrap
'nowrap': !titleWrap
}"
:style="{
'font-size':`${titleSize}rpx`
@ -136,7 +138,7 @@ function toDetail() {
.title-row {
user-select: none;
width: 100%;
//padding: 14rpx 0;
padding-bottom: 15rpx;
font-size: 28rpx;
}

View File

@ -1,7 +1,6 @@
<template>
<view>
<view
:class="['order', className]"
:class="['order']"
v-if="data"
>
@ -119,7 +118,6 @@
</view>
</view>
</view>
</template>
<script setup>

View File

@ -39,10 +39,10 @@
</view>
<view
class="reply-pic flex flex-ai__center"
v-if="data.pics && data.pics.length>0"
v-if="data.pics && data.pics.filter(item=>!!item).length>0"
>
<template
v-for="(pic,index) in data.pics"
v-for="(pic,index) in data.pics.filter(item=>!!item)"
:key="index"
>
<image
@ -154,11 +154,12 @@ function doPreviewImage(current, urls) {
.reply-pic {
gap: 20rpx;
margin-bottom: 20rpx;
margin: 20rpx 0;
flex-wrap: wrap;
padding: 0;
.image {
width: 180rpx;
height: 180rpx;
width: 212rpx;
height: 212rpx;
}
}
</style>

View File

@ -18,6 +18,9 @@ export const usePage = getPage => {
// 分类ID
const sid = ref('')
// 优惠券ID
const couponId = ref('')
// 是否新品,不为空的字符串即可
const news = ref('')
@ -45,6 +48,7 @@ export const usePage = getPage => {
keyword: keyword.value,
type: type.value,
sid: sid.value,
couponId: couponId.value,
news: news.value,
isIntegral: isIntegral.value,
})
@ -92,6 +96,7 @@ export const usePage = getPage => {
listEmpty,
news,
sid,
couponId,
refresh: handleRefresh,
}
}

View File

@ -127,6 +127,9 @@ export const useRouter = () => {
function getParams(options) {
if (typeof options !== 'object') return {}
if (!options[PARAMS_KEY]) return {}
// #ifdef MP-WEIXIN
console.log(typeof options[PARAMS_KEY],options[PARAMS_KEY],decodeURIComponent(options[PARAMS_KEY]))
// #endif
return JSON.parse(decodeURIComponent(options[PARAMS_KEY]));
}

15
hooks/useScroll.js Normal file
View File

@ -0,0 +1,15 @@
import { onPageScroll } from "@dcloudio/uni-app";
import { onBeforeUnmount, ref } from "vue";
const scrollTop = ref(0)
export function useScroll(){
onPageScroll((e)=>{
scrollTop.value = e.scrollTop
})
onBeforeUnmount(()=>{
scrollTop.value = 0
})
return {
scrollTop
}
}

View File

@ -1,11 +1,9 @@
<template>
<layout>
<uv-navbar
:fixed="false"
:title="title"
left-arrow
@leftClick="goBack"
/>
<Header>
{{ title }}
</Header>
<view class="addressList">
<template v-if=" main.address.length>0">
@ -65,16 +63,18 @@
您还没有新增地址~
</Empty>
</view>
<div class="form-buttons">
<uv-button
round
block
type="primary"
@tap="goCreateAddress"
>
新增地址
</uv-button>
</div>
<view class="form-buttons">
<view class="btn">
<uv-button
round
block
type="primary"
@tap="goCreateAddress"
>
新增地址
</uv-button>
</view>
</view>
</layout>
</template>
@ -87,6 +87,7 @@ import Empty from '@/components/Empty/index.vue'
import emptyIcon from '@/static/icon/empty/地址.png'
import { getAddressDel, } from '@/api/address'
import { useRouter } from "@/hooks/useRouter";
import Header from "@/components/Header/index.vue"
const {push, goBack} = useRouter()
@ -156,6 +157,8 @@ onLoad((option) => {
})
const handleAddressClick = async ({index}, data) => {
if (index == 0) {
uni.showModal({
@ -302,12 +305,13 @@ const handleAddressClick = async ({index}, data) => {
padding: 0;
}
.form-buttons :deep(.uv-button) {
.form-buttons .btn {
width: 90%;
margin: 0 auto;
}
.address-default :deep(.uv-tags) {
justify-content: center;
white-space: nowrap;
}
</style>

View File

@ -221,6 +221,7 @@ async function handleUnCollectSingle() {
await unCollectSingle(toDeleteItem)
await refresh()
await toast({title: '删除成功'})
toDeleteItem = undefined
}
/**

View File

@ -13,14 +13,19 @@
<view class="list-label w-158">
收货地址
</view>
<view class="list-content">
<city-select
ref="cityselect"
:defaultValue="defaultAddress"
@callback="result"
:items="main.areaList"
></city-select>
<view
class="list-content"
@click="handleChooseAddress"
>
<template v-if="addressData.address.cityId">
{{ addressData.address.province }} {{ addressData.address.city }} {{ addressData.address.district }}
</template>
<template v-else>
<span class="chooise">
点击选择
<uv-icon name="arrow-right"></uv-icon>
</span>
</template>
</view>
</view>
</view>
@ -34,7 +39,7 @@
type="text"
placeholder="请输入详细地址"
v-model="addressData.detail"
>
/>
</view>
</view>
@ -49,7 +54,7 @@
type="text"
placeholder="请输入姓名"
v-model="addressData.realName"
>
/>
</view>
</view>
@ -64,7 +69,7 @@
type="number"
placeholder="请输入电话"
v-model="addressData.phone"
>
/>
</view>
</view>
</view>
@ -89,37 +94,45 @@
@click="onSave"
></uv-button>
</view>
<uv-picker
ref="addressPickerRef"
:columns="columns"
keyName="name"
@change="handlePickerChange"
@confirm="handlePickerConfirm"
></uv-picker>
</layout>
</template>
<script setup>
import { ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { useMainStore } from '@/store/store'
import { getAddressAddAndEdit, getAddressDel, } from '@/api/address'
import { useRouter } from "@/hooks/useRouter";
import { storeToRefs } from "pinia";
import UvPicker from "@/uni_modules/uv-picker/components/uv-picker/uv-picker.vue";
const main = useMainStore()
const {getParams, push, goBack} = useRouter()
const areaList = ref(main.areaList)
const {areaList, address} = storeToRefs(main)
// const areaList = ref(main.areaList)
const title = ref('')
const editId = ref('')
const defaultAddress = ref('')
const addressData = ref({
"realName": '',
"postCode": '',
"isDefault": false,
"detail": '',
"phone": '',
"cityId": '',
"city": '',
"district": '',
"province": '',
realName: undefined,
phone: undefined,
detail: undefined,
isDefault: undefined,
address: {
cityId: undefined,
city: undefined,
district: undefined,
province: undefined,
}
});
const isDefaultList = ref([])
@ -188,9 +201,9 @@ const onSave = async () => {
uni.hideLoading()
main.restAddress()
if (actionType.value == 'select') {
push({url: '/pages/address/address'}, {data: {type: 'select'}})
push({url: '/pages/address/address'}, {data: {type: 'select'},type:'redirectTo'})
} else {
push({url: '/pages/address/address'})
goBack()
}
} catch (error) {
console.log(error, 'err')
@ -218,38 +231,72 @@ const result = (values) => {
}
}
watch(() => main.areaList, (next) => {
areaList.value = next
const addressPickerRef = ref()
const provinces = ref([])
const citys = ref([])
const areas = ref([])
const pickerValue = ref([0, 0, 0])
const defaultValue = ref([0, 0, 0])
const columns = computed(() => {
return [
provinces.value, citys.value, areas.value
]
})
watch(() => main.address, (next) => {
let data = next.filter(item => item.id == editId.value)[0]
if (!data) return
addressData.value = {
realName: data.realName,
phone: data.phone,
detail: data.detail,
isDefault: data.isDefault ? 1 : 0,
address: {
cityId: data.cityId,
district: data.district,
province: data.province,
city: data.city
}
function handleSetDefaultColumns() {
console.log(addressPickerRef.value)
pickerValue.value[0] = provinces.value.findIndex((item, index) => index === defaultValue.value[0])
citys.value = provinces.value[pickerValue.value[0]].children || []
pickerValue.value[1] = citys.value.findIndex((item, index) => index === defaultValue.value[1])
areas.value = citys.value[pickerValue.value[1]].children || []
pickerValue.value[2] = areas.value.findIndex((item, index) => index === defaultValue.value[2])
// 重置下位置
// addressPickerRef.value.setColumnValues(0,provinces.value)
// addressPickerRef.value.setColumnValues(1,citys.value)
// addressPickerRef.value.setColumnValues(2,areas.value)
nextTick(() => {
addressPickerRef.value.setIndexs([pickerValue.value[0], pickerValue.value[1], pickerValue.value[2]], true);
console.log('设置完毕')
})
}
function handlePickerChange(e) {
const {columnIndex, index, indexs} = e
// 改变了省
if (columnIndex === 0) {
citys.value = provinces.value[index]?.children || []
areas.value = citys.value[0]?.children || []
addressPickerRef.value.setIndexs([index, 0, 0], true)
} else if (columnIndex === 1) {
areas.value = citys.value[index]?.children || []
addressPickerRef.value.setIndexs(indexs, true)
}
isDefaultList.value = data.isDefault ? ['isDefault'] : [],
defaultAddress.value = {
province: {
name: data.province
},
city: {
name: data.city
},
district: {
name: data.district
}
}
})
}
function handlePickerConfirm(e) {
const {indexs, value} = e
defaultValue.value = indexs
addressData.value.address = {
province: value[0].name || '',
city: value[1].name || '',
district: value[2].name || '',
cityId: value[1].id,
}
}
function handleFindDefault(data) {
const provinceIndex = areaList.value.findIndex(item => item.name === data.province);
const cityIndex = areaList.value[provinceIndex].children.findIndex(item => item.name === data.city);
const areasIndex = areaList.value[provinceIndex].children[cityIndex].children.findIndex(item => item.name === data.district);
defaultValue.value = [provinceIndex, cityIndex, areasIndex]
}
function handleChooseAddress() {
provinces.value = areaList.value
handleSetDefaultColumns()
unref(addressPickerRef).open()
}
onLoad(async (options) => {
const params = getParams(options)
@ -260,7 +307,7 @@ onLoad(async (options) => {
if (id) {
editId.value = id
title.value = '编辑地址'
let data = main.address.filter(item => item.id == id)[0]
let data = address.value.filter(item => item.id == id)[0]
if (!data) return
addressData.value = {
realName: data.realName,
@ -274,24 +321,15 @@ onLoad(async (options) => {
province: data.province,
}
}
defaultAddress.value = {
province: {
name: data.province
},
city: {
name: data.city
},
district: {
name: data.district
}
}
// 设置默认选择
handleFindDefault(data)
isDefaultList.value = data.isDefault ? ['isDefault'] : []
} else {
title.value = '新增地址'
}
})
onMounted(() => {
})
</script>
<style lang="scss">
@ -306,4 +344,9 @@ onLoad(async (options) => {
.w-158 {
flex: 0 0 158rpx;
}
.chooise {
@include useFlex(space-between, center);
color: #999;
}
</style>

View File

@ -39,10 +39,8 @@ const getCoupon = async () => {
});
}
const goToProduct = () => {
push({
url: '/pages/goodsList/goodsList'
})
const goToProduct = (coupons) => {
push({url: '/pages/goodsList/goodsList'},{data: {couponId: coupons.id}})
}
</script>
@ -71,7 +69,7 @@ const goToProduct = () => {
<!-- button -->
<view v-if="type === 'select'">
<view class="button" v-if="tabType === 0">
<span @click="goToProduct">去使用</span>
<span @click="goToProduct(coupons)">去使用</span>
</view>
<view class="button disable" v-if="tabType === 1">
<span>已使用</span>
@ -90,8 +88,7 @@ const goToProduct = () => {
scoped
lang="scss"
>
.coupon-item{
margin-bottom: 30rpx;
.coupon-item {
width: 100%;
aspect-ratio: 682/176;
background: url("@/static/background/coupon-bg.png") no-repeat ;

View File

@ -17,6 +17,7 @@
<uv-line color="#E6E6E6"></uv-line>
<view class="evaluate-content">
<uv-textarea
border="none"
v-model="comment"
placeholder="请填写您遇到的问题,这将帮助我们为您提供更好的服务"
></uv-textarea>

View File

@ -223,6 +223,7 @@ async function handleUnCollectSingle() {
await unFootprintSingle(toDeleteItem)
await refresh()
await toast({title: '删除成功'})
toDeleteItem = undefined
}
/**

View File

@ -6,7 +6,7 @@
>
<uv-vtabs
:list="categoryData"
:hdHeight="`${headerRef&&headerRef.containerHeight || 0}rpx`"
:hdHeight="`${headerRef&&headerRef.containerHeight || 0}px`"
>
<template
v-for="(item, index) in categoryData"

View File

@ -1,17 +1,10 @@
<template>
<layout class="goodsDetail">
<!-- <uv-navbar-->
<!-- fixed-->
<!-- :safeAreaInsetTop="true"-->
<!-- title=""-->
<!-- bg-color="transparent"-->
<!-- @leftClick="goBack"-->
<!-- />-->
<view :style="computedHeightStyle"></view>
<Header
ref="headerRef"
:propUp="false"
:scrollTop="scrollTop"
>
</Header>
<view v-if="detailData">
<swiper
@ -28,7 +21,7 @@
<image
class="image"
:src="item"
mode="widthFix"
mode="aspectFill"
/>
</view>
</swiper-item>
@ -44,9 +37,9 @@
</view>
<view
class="goodsDetail-price goodsDetail-price-original"
v-if="(storeAttr&& storeAttr.otPrice) || (storeInfo&&storeInfo.otPrice)"
v-if="storeAttr && storeInfo"
>
¥{{ storeAttr.otPrice || storeInfo.otPrice }}
¥{{ storeAttr&&storeAttr.otPrice || storeInfo&&storeInfo.otPrice }}
</view>
</view>
<view class="goodsDetail-storeName">{{ storeInfo.storeName }}</view>
@ -115,7 +108,10 @@
<view class="card full">
<view class="card-head" :style="{borderBottom:detailData.replyCount<=0?'none':'1rpx solid #e6e6e6'}">
<view
class="card-head"
:style="{borderBottom:detailData.replyCount<=0?'none':'1rpx solid #e6e6e6'}"
>
<view class="card-title">商品评价({{ detailData.replyCount }})</view>
<view
class="card-more"
@ -256,7 +252,7 @@
</template>
<script setup>
import { ref, unref } from 'vue'
import { computed, onMounted, ref, unref } from 'vue'
import { collectSingle, getProductDetail, unCollectSingle } from '@/api/product'
import { getCartAdd, getCartCount } from '@/api/cart'
import { onLoad, onPageScroll } from '@dcloudio/uni-app'
@ -265,6 +261,7 @@ import { useInterface } from "@/hooks/useInterface";
import Header from '@/components/Header/index.vue'
import GoodCouponSelect from "@/components/good-coupon-select/good-coupon-select.vue";
import UvIcon from "@/uni_modules/uv-icon/components/uv-icon/uv-icon.vue";
import { useScroll } from "@/hooks/useScroll";
const {push, getParams, pushToTab, goBack} = useRouter();
const {toast} = useInterface()
@ -279,6 +276,16 @@ const cardCount = ref(null)
const selectAttrPanel = ref(false)
const selectCouponPanel = ref(false)
const selectCoupon = ref(false)
const headerRef = ref()
const computedHeightStyle = computed(() => {
const style = {width: '100 %', height: 0,background:'#f5f5f5'}
if (!headerRef.value || !headerRef.value.heightInfo) return style
return {...style, height: `${ headerRef.value.heightInfo.statusBarHeight }px`}
})
onMounted(() => {
})
const handleGetDetail = async (id) => {
try {
@ -312,7 +319,7 @@ onLoad((options) => {
handleGetDetail(params.id)
handleGetCartCount(params.id)
})
useScroll()
const goToService = () => {
toast({title: '😒敬请期待😒'})
}
@ -407,10 +414,7 @@ const handleGetCartCount = async () => {
cardCount.value = count.count
}
const scrollTop = ref(0)
onPageScroll((e) => {
scrollTop.value = e.scrollTop
})
</script>
@ -743,7 +747,9 @@ onPageScroll((e) => {
// ======================= 👇 kahu ===
.row-context {
margin: 30rpx 0 ;
margin: 30rpx 0;
.label-row {
@include useFlex(space-between, center);
@include usePadding(30, 20);
@ -775,6 +781,20 @@ onPageScroll((e) => {
.value {
@include useFlex(flex-end, center);
flex-shrink: 0;
font-size: 28rpx;
color: #333;
}
}
}
.swiper {
.swiper-item {
width: 100%;
height: 100%;
.image {
width: inherit;
height: inherit;
}
}
}

View File

@ -76,13 +76,14 @@ import ListLoadLoading from "@/components/ListLoadLoading/index.vue"
import emptyIcon from "@/static/icon/empty/收藏.png"
const {getParams, goBack} = useRouter()
const {keyword, refresh, sid, dataList, loadend, loading, listEmpty} = usePage(getProductList)
const {keyword, refresh, sid, couponId, dataList, loadend, loading, listEmpty} = usePage(getProductList)
onLoad((options) => {
const params = getParams(options)
keyword.value = params.keyword || ''
sid.value = params.sid || ''
couponId.value = params.couponId || ''
refresh()
})

View File

@ -6,83 +6,39 @@
@update: 2023-10-27 14:42
-->
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { usePage } from "@/hooks";
import { getProductList } from "@/api/product";
import Empty from "@/components/Empty/index.vue"
import ListLoadOver from "@/components/ListLoadOver/index.vue"
import ListLoadLoading from "@/components/ListLoadLoading/index.vue"
import { useRouter } from "@/hooks/useRouter";
const {refresh, dataList, loadend, loading, listEmpty} = usePage(getProductList)
const {push} = useRouter()
onLoad(() => {
refresh()
})
import Recommend from "@/components/Recommend/index.vue";
import UvIcon from "@/uni_modules/uv-icon/components/uv-icon/uv-icon.vue";
</script>
<template>
<blank size="15"></blank>
<activity
title="商品推荐"
more="查看更多"
@moreClick="push({ url: '/pages/goodsList/goodsList' })"
>
<view class="card goods-row">
<template v-if="!listEmpty">
<uv-grid
:border="false"
:col="2"
:gutter="10"
>
<uv-grid-item
v-for="(item, index) in dataList"
:key="index"
>
<goods
class="item"
link
card
:data="item"
:storeName="item.storeName"
:price="item.price"
:stock="true"
>
</goods>
</uv-grid-item>
</uv-grid>
</template>
<Empty
v-else
:iconSrc="emptyIcon"
>
这里空空如也~
</Empty>
<!-- 加载中 -->
<ListLoadLoading v-if="loading" />
<!-- 加载完毕-->
<ListLoadOver v-if="loadend" />
</view>
</activity>
<Recommend ref="recommendRef" :more="false">
<template #head>
<view class="head flex flex-ai__center flex-jc__sb">
<view class="left">
商品推荐
</view>
<view class="right flex flex-ai__center flex-jc__end">
查看更多
<uv-icon name="arrow-right" color="#999" size="14" />
</view>
</view>
</template>
</Recommend>
</template>
<style
scoped
lang="scss"
>
@import "@/style/main.scss";
.goods-row {
@include usePadding(20, 20);
border-radius: 15rpx;
.item {
width: 100%;
margin: 0 10rpx;
.head{
margin-top: 20rpx;
.left{
font-size: 32rpx;
color: #333333;
}
.right{
font-size: 24rpx;
color: #999999;
}
}
</style>

View File

@ -2,7 +2,6 @@
<view class="home-container">
<view class="header-row">
<Header
:scroll-top="scrollTop"
:left-width="140"
@animation="handleHeaderAnimation"
>
@ -19,19 +18,14 @@
<view
class="search-col"
:style="searchShadow"
@click="toSearch"
@tap="toSearch"
>
<uv-icon
name="search"
size="26"
size="40rpx"
/>
<view class="search-input">
<input
type="text"
disabled
placeholder="搜索商品"
@click="toSearch"
/>
搜索商品
</view>
</view>
</template>
@ -68,7 +62,7 @@
<!-- 热门直播 -->
<Live v-if="false" />
<Recommend ref="recommendRef" />
<Recommend />
</template>
<!-- h5 tabbar 底部 -->
<view class="h5-tabbar-height"></view>
@ -89,8 +83,9 @@ import Group from './components/Group/index.vue'
import Live from './components/Live/index.vue'
import NewProduct from './components/NewProduct/index.vue'
import Seckill from './components/Seckill/index.vue'
import Recommend from './components/Recommend/index.vue'
import Recommend from "./components/Recommend/index.vue";
import UvIcon from "@/uni_modules/uv-icon/components/uv-icon/uv-icon.vue";
import { useScroll } from "@/hooks/useScroll";
const main = useMainStore()
const {push} = useRouter()
@ -118,19 +113,18 @@ function handleHeaderAnimation(numericalValue) {
}
}
const scrollTop = ref(0)
/*const scrollTop = ref(0)
onPageScroll((e) => {
scrollTop.value = e.scrollTop
})
})*/
onLoad(() => {
main.init()
doGetHomeData()
})
onReachBottom(() => {
unref(recommendRef).onReachBottom && unref(recommendRef).onReachBottom();
})
useScroll()
</script>
<style lang="scss">
@ -153,13 +147,16 @@ onReachBottom(() => {
.search-col {
@include useFlex(flex-start, center);
@include usePadding(30, 15);
width: 100%;
width: 320rpx;
height: 60rpx;
border-radius: 50rpx;
background: $white-color;
margin-left: 50rpx;
.search-input {
margin-left: 30rpx;
font-size: 24rpx;
color: #999999;
}
}
}
@ -169,4 +166,6 @@ onReachBottom(() => {
.goods-row {
padding: 0 8rpx;
}
</style>

View File

@ -6,19 +6,96 @@
@update: 2023-11-08 16:20
-->
<script setup>
import wechat from '@/static/icon/login/wechat.png'
import { useRouter } from "@/hooks/useRouter";
import { loginMethods } from "@/pages/login/index.data";
import { wxLogin } from "@/utils/wechatUtils";
import { error } from "@/uni_modules/uv-ui-tools/libs/function";
import { nextTick, ref } from "vue";
import { useInterface } from "@/hooks/useInterface";
import { privacyAgreementUrl, userAgreementUrl, weixinLogin } from "@/api/auth";
import { useMainStore } from "@/store/store";
import { afterLogin } from "@/utils";
const {toast, loading, hideLoading} = useInterface()
const {goBack, push} = useRouter()
const mainStore = useMainStore()
function toLogin(loginMethod) {
if (loginMethod.type === 0) {
// 微信登录
console.log('微信登录')
} else {
async function toLogin(loginMethod) {
push({url: '/pages/login/index', animationType: 'slide-in-right'}, {data: {...loginMethod}})
}
// ============================= 微信登录相关
const code = ref('') // logingCode必须比getPhoneNumber的code先获取
async function getCode() {
loading({
title: '登陆中...'
})
try {
code.value = await wxLogin();
} catch (e) {
console.error(e)
toast('获取code失败')
hideLoading()
}
}
async function getPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
console.error(e)
hideLoading()
toast({title: '登录失败'})
}
try {
const phoneCode = e.detail.code
const res = await weixinLogin({
phoneCode,
loginCode: code.value
});
if (res) {
await mainStore.setAccessToken(res)
afterLogin()
}
} catch (e) {
console.error(e)
toast({title: '登录失败'})
} finally {
hideLoading()
}
}
const privacyPolicy = ref([]) // 协议双向绑定
const isPrivacyError = ref(false) // 未勾选协议
/**
* 检查是否勾选协议
* @returns {boolean}
*/
function checkPrivacy() {
const flag = privacyPolicy.value.length > 0
if (!flag) {
toast({title: '请先阅读并同意用户协议和隐私政策'})
isPrivacyError.value = true
setTimeout(() => {
isPrivacyError.value = false
}, 1000)
}
return flag
}
/**
* 跳转协议
* @param type
*/
function toAgreement(type) {
const urls = [userAgreementUrl, privacyAgreementUrl]
push({url: '/pages/webview/index'}, {data: {src: urls[type]}})
}
</script>
<template>
@ -37,6 +114,32 @@ function toLogin(loginMethod) {
</view>
<view class="button-group">
<!-- #ifdef MP-WEIXIN -->
<view
v-if="privacyPolicy.length<=0"
class="button animation-button disabled"
@click="checkPrivacy"
>
<image
class="icon"
:src="wechat"
/>
微信快捷登录
</view>
<button
v-else
class="button animation-button"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
@click="getCode"
>
<image
class="icon"
:src="wechat"
/>
微信快捷登录
</button>
<!-- #endif -->
<template
v-for="(loginMethod) in loginMethods"
:key="loginMethod.type"
@ -55,8 +158,34 @@ function toLogin(loginMethod) {
</view>
<view class="tips-row">
为了给您提供更好的服务 我们需要您的授权哦~
<view
class="agreement-box"
:class="{'error-animation':isPrivacyError}"
>
<uv-checkbox-group
v-model="privacyPolicy"
shape="circle"
activeColor="#ec6e47"
>
<uv-checkbox :name="1">
<view class="agreement-text">
阅读并同意
<span
class="color"
@click="toAgreement(0)"
>
YSHOP商城用户协议
</span>
<span
class="color"
@click="toAgreement(1)"
>
YSHOP商城隐私协议
</span>
</view>
</uv-checkbox>
</uv-checkbox-group>
</view>
</view>
@ -112,14 +241,54 @@ function toLogin(loginMethod) {
}
}
.tips-row {
position: absolute;
bottom: 5%;
left: 0;
width: 100%;
color: $tips-color;
font-size: 24rpx;
}
.agreement-box {
@include usePadding(30, 0);
position: fixed;
bottom: 5%;
width: 100%;
font-size: 24rpx;
color: $tips-color;
transition: all .3s;
.agreement-text {
text-align: center;
.color {
color: $primary-color;
}
}
:deep(.uv-checkbox ) {
width: 100%;
align-items: flex-start;
.uv-checkbox__icon-wrap {
}
}
}
.error-animation {
animation: error-text 0.8s 1;
}
@keyframes error-text {
0% {
transform: translateX(0);
}
5%, 25%, 45%, 65%, 85% {
transform: translateX(-10rpx);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(10rpx);
}
15%, 35%, 55%, 75%, 95% {
transform: translateX(20rpx);
}
20%, 40%, 60%, 80%, 100% {
transform: translateX(-20rpx);
}
}
</style>

View File

@ -13,12 +13,7 @@ import phone from '@/static/icon/login/phone.png'
* @type {[{icon: *, label: string},{icon: *, label: string}]}
*/
export const loginMethods = [
{
type: 0,
label: '微信快捷登录',
icon: wechat,
classNames: []
},
{
type: 1,
label: '手机号快捷登录',

View File

@ -4,7 +4,7 @@
:fixed="false"
title="订单详情"
left-arrow
@leftClick="goList"
@leftClick="goBack"
/>
<view v-if="orderInfoData">
<view class="orderInfo-header background-warp">
@ -70,6 +70,7 @@
>
<goods
list
link
interval
desc="3"
showAction
@ -407,14 +408,16 @@ const handleOrderTake = async () => {
uni: orderInfoData.value.orderId,
}
const res = await orderTake(option)
toast({
title: '收货成功'
})
uni.showToast({
title: '收货成功',
duration: 2000
});
handleOrderInfo({
key: option.uni
})
} else if (res.cancel) {
}
}
});
@ -434,9 +437,23 @@ const handlePay = () => {
// 返回列表
const goList = ()=>{
let status = 0
switch (orderInfoData.value.status){
case -1:
status = -1
break
case 0:
status = 0
break
case 99:
status = 1
break
default:
status = orderInfoData.value.status +1
}
push({url: '/pages/orderList/orderList'}, {
data: {
type: orderInfoData.value.status === 99?1:orderInfoData.value.status+1,
type: status
}
})
}
@ -494,13 +511,15 @@ const handleCancel = async () => {
success: async (res) => {
if (res.confirm) {
await orderCancel({
id: data.value.orderId
id: orderInfoData.value.orderId
})
data.value = null
uni.showToast({
title: '已取消',
duration: 2000
});
setTimeout(()=>{
goList()
},2000)
} else if (res.cancel) {
}
}

View File

@ -7,7 +7,7 @@
>
<uv-navbar
:fixed="false"
title="全部订单"
:title="title"
@leftClick="goBack"
/>
<uv-tabs
@ -18,6 +18,7 @@
>
</uv-tabs>
</uv-sticky>
<view class="orderList">
<template v-if="!listEmpty">
<order
@ -40,23 +41,26 @@
<ListLoadOver v-if="loadend" />
</view>
</layout>
<!-- 支付弹窗 -->
<PayPopup
ref="payPopupRef"
@confirm="paySuccess"
/>
</view>
<!-- 支付弹窗 -->
<PayPopup
ref="payPopupRef"
@confirm="paySuccess"
/>
</template>
<script setup>
import { ref, unref } from 'vue'
import { computed, ref, unref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { orderList } from '@/api/order'
import { useRouter } from "@/hooks/useRouter";
import ListLoadOver from "@/components/ListLoadOver/index.vue"
import ListLoadLoading from "@/components/ListLoadLoading/index.vue"
import Empty from '@/components/Empty/index.vue'
import emptyIcon from '@/static/icon/empty/订单.png'
import PayPopup from '@/components/PayPopup/index.vue'
import { usePage } from "@/hooks";
import Header from "@/components/Header/index.vue"
import { getProductList } from "@/api/product";
const {type, refresh, dataList, loadend, loading, listEmpty} = usePage(orderList)
@ -78,6 +82,11 @@ const navList = ref([
// { name: "退款", value: 7, },
])
const title = computed(()=>{
const find = navList.value.find(item=>item.value === type.value);
return find?`${find.name}订单`:'订单'
})
// const handleOrderList = async (option) => {
// orderListData.value = []
@ -110,7 +119,7 @@ function openPay(orderId) {
}
function paySuccess() {
// push({url: '/pages/payStatus/index?type=1'})
push({url: '/pages/payStatus/index'},{})
}

View File

@ -8,14 +8,18 @@
<script setup>
import { useRouter } from "@/hooks/useRouter";
import { computed, ref, unref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { checkH5Pay } from "@/api/order";
import { onLoad, onPageScroll, onReachBottom } from "@dcloudio/uni-app";
import { checkPay } from "@/api/order";
import { CacheKey } from "@/utils/config";
import Header from '@/components/Header/index.vue'
import Recommend from '@/components/Recommend/index.vue'
import { useInterface } from "@/hooks/useInterface";
import { useScroll } from "@/hooks/useScroll";
const {getParams, goBack, push, pushToTab} = useRouter()
const {loading,hideLoading} = useInterface()
const type = ref(0) // 支付状态 0支付中 1支付成功 2支付失败
useScroll()
const title = computed(() => {
if (type.value === 0) return '支付中...'
if (type.value === 1) return '支付成功'
@ -36,23 +40,48 @@ function toBackHome() {
pushToTab({url: '/pages/index/index'})
}
const retryTime = ref(3)
/**
* 查询服务端支付状态
*/
async function queryOrderStatus() {
const payInfoStr = uni.getStorageSync(CacheKey.PAY_INFO);
if (payInfoStr) {
const parse = JSON.parse(payInfoStr);
const res = await checkH5Pay(parse);
type.value = res ? 1 : 2
uni.removeStorageSync(CacheKey.PAY_INFO)
} else {
// 没有订单缓存直接跳到订单页面
toOrderList(0)
}
loading({title: '查询中...'})
// 异步去查,有可能接口比微信快
setTimeout(async () => {
try {
const payInfoStr = uni.getStorageSync(CacheKey.PAY_INFO);
// 没有订单缓存直接跳到订单页面
if (!payInfoStr) return toOrderList(0)
const parse = JSON.parse(payInfoStr);
const res = await checkPay(parse);
if (!res) {
// 支付失败重新查询
if (retryTime.value > 0) {
retryTime.value--
await queryOrderStatus()
} else {
type.value = 2
uni.removeStorageSync(CacheKey.PAY_INFO)
}
return
}
type.value = 1
uni.removeStorageSync(CacheKey.PAY_INFO)
} finally {
hideLoading()
}
}, 1000)
}
const recommendRef = ref(null)
onReachBottom(() => {
unref(recommendRef).onReachBottom && unref(recommendRef).onReachBottom();
})
onLoad(async (options) => {
// H5 和 APP 需要弹窗去确认
// #ifdef H5
uni.showModal({
content: '请确认支付是否完成',
@ -61,23 +90,15 @@ onLoad(async (options) => {
}
})
// #endif
const params = getParams(options);
if (params && params.type === 1) {
type.value = 1
} else {
type.value = 2
}
await queryOrderStatus()
})
</script>
<template>
<view class="pay-status">
<uv-navbar
:fixed="false"
:title="title"
left-arrow
@leftClick="goBack"
/>
<Header>
{{ title }}
</Header>
<view class="status-main flex flex-column flex-ai__center">
<image
class="icon"
@ -123,6 +144,8 @@ onLoad(async (options) => {
重新支付
</view>
</view>
<!-- 商品推荐 -->
<Recommend ref="recommendRef" />
</view>
</template>

View File

@ -230,15 +230,17 @@ const handleOrderInfo = async (option) => {
let price = 0
let productParamList = []
let productAttrUnique = []
goodsList.value.forEach(item => {
price += item.cartInfo.truePrice * item.cartInfo.cartNum
productParamList.push({
productId: item.productId
productId: item.productId,
productAttrUnique: item.cartInfo.productAttrUnique
})
})
totalPrice.value = price
totalPrice.value = price.toFixed(2)
data.value.orderId = res[0].orderId
data.value.serviceType = refundType.value
data.value.productParamList = productParamList
@ -281,9 +283,10 @@ const handleApplyForAfterSales = async () => {
title: '申请成功,请等待审核'
})
push({url: '/pages/refundInfo/refundInfo'}, {
type:'redirectTo',
data: {
id: res,
}
},
})
}

View File

@ -115,6 +115,7 @@
model
:purchase="item.cartNum"
:data="item.productInfo"
:price="item.truePrice"
v-for="(item, index) in orderInfoData.cartInfo"
>
</goods>
@ -133,7 +134,7 @@
</view>
<view class="info-cell ">
<view class="info-cell-label">总计</view>
<view class="info-cell-value">¥{{ orderInfoData.refundAmount }}</view>
<view class="info-cell-value">¥{{ orderInfoData.payPrice.toFixed(2) }}</view>
</view>
</view>
</view>
@ -160,7 +161,7 @@
服务类型
</view>
<view class="info-cell-value">
{{ orderInfoData.serviceType }}
{{ orderInfoData.serviceType===0?'仅退款':'退货退款' }}
</view>
</view>
<view
@ -321,8 +322,8 @@ const handleDelete = async () => {
success: async (res) => {
if (res.confirm) {
await afterSalesOrderDelete({
id: orderInfoData.value.id,
orderCode: orderInfoData.value.orderCode
id: orderId.value,
orderCode: orderInfoData.value.orderId
})
uni.showToast({
title: '已删除',

View File

@ -20,21 +20,43 @@
<card class="shopping-checkbox">
<view
v-for="(item, index) in goodsList"
class="shopping-checkbox-cell"
:key="item.id"
class="goods-row"
>
<uv-checkbox
:name="item.cartInfo.productAttrUnique"
:disabled="item.isAfterSales != 1"
/>
<goods
list
interval
showAction
model
:price="item.cartInfo.truePrice"
:data="item.cartInfo.productInfo"
>
</goods>
<view class="goods-col">
<Goods
row
imgWidth="200rpx"
info-padding="10rpx 40rpx"
:goods="item.cartInfo.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"
>
<view class="info">
{{ item.cartInfo.productInfo &&item.cartInfo.productInfo.attrInfo && item.cartInfo.productInfo.attrInfo.sku }}
</view>
</view>
</view>
<!-- bottom -->
<view class="price-row flex flex-ai__center flex-jc__sb">
<!-- price -->
<view class="price-box flex flex-ai__end">
{{item.cartInfo.truePrice }}
</view>
</view>
</view>
</template>
</Goods>
</view>
</view>
</card>
</space>
@ -87,6 +109,7 @@ import { ref, watch } from 'vue'
import { applyForAfterSalesInfo } from '@/api/order'
import { onLoad } from '@dcloudio/uni-app'
import { useRouter } from "@/hooks/useRouter";
import Goods from "@/components/goodsComponents/Goods.vue";
const {getParams, push, goBack} = useRouter()
const goodsList = ref([])
@ -118,7 +141,7 @@ watch(goodsSelect, (goodsSelect) => {
price += item.cartInfo.truePrice * item.cartInfo.cartNum
})
totalPrice.value = price
totalPrice.value = price.toFixed(2)
})
const handleOrderInfo = async (option) => {
@ -140,7 +163,8 @@ const toRefund = (type) => {
goods: goodsSelect.value.toString(),
orderId: orderId.value,
id: id.value
}
},
type:'redirectTo'
})
}
@ -205,4 +229,95 @@ onLoad((options) => {
background: $white-color;
}
}
.goods-row{
@include useFlex(space-between,center);
@include usePadding(20,10);
width: 100%;
.goods-col{
width: 90%;
}
}
// 商品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;
color:$primary-color;
.old-price {
font-size: 20rpx;
color: $tips-color;
text-decoration: line-through;
margin-left: 10rpx;
}
}
.cart-num {
font-size: 24rpx;
.input {
width: 120rpx;
input {
width: 100%;
text-align: center;
color: #333;
}
}
.button {
font-size: 32rpx;
width: 34rpx;
aspect-ratio: 1/1;
border-radius: 5rpx;
border: 2rpx solid #cccccc;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
transition: all .3s;
&:active {
scale: 1.2;
}
}
}
}
}
</style>

View File

@ -2,7 +2,6 @@
<Header
system-bar-area-bg="#fff"
header-area-bg="#fff"
:scroll-top="scrollTop"
bg-change-by-scroll
>
购物车
@ -27,6 +26,7 @@
<!-- 购物车信息 -->
<view
v-for="(item) in cartList"
:key="item.id"
class="shopping-item"
>
<uv-checkbox
@ -164,13 +164,17 @@
</view>
</view>
</view>
<view class="action-height"></view>
<view class="h5-tabbar-height"></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"
@close="handleCloseSkuSelect"
@ -194,7 +198,9 @@ import { settleFields } from "@/pages/shoppingCart/index.data";
import { useCartData, useCartNumber, useCartOption, useSku } from "@/pages/shoppingCart/index.utils";
import CartEmpty from "@/pages/shoppingCart/components/CartEmpty.vue";
import Header from "@/components/Header/index.vue"
import { onHide, onPageScroll } from "@dcloudio/uni-app";
import { onHide, onPageScroll, onReachBottom } from "@dcloudio/uni-app";
import Recommend from "@/components/Recommend/index.vue";
import { useScroll } from "@/hooks/useScroll";
const modalRef = ref() // 删除弹窗
const goodsAttrSelectRef = ref() // 更改sku
@ -233,15 +239,14 @@ const {
} = useCartNumber({afterChange: computeSelectInfoByShoppingSelect})
onHide(() => {
modalRef.value?.close()
goodsAttrSelectRef.value?.close()
})
const scrollTop = ref(0)
onPageScroll((e) => {
scrollTop.value = e.scrollTop
})
useScroll()
</script>
<style lang="scss">

View File

@ -3,6 +3,7 @@
ref="popupRef"
:showCloseable="false"
@close="emit('close')"
@maskClick="clickMask"
>
<template v-if="couponList.length>0">
<view
@ -22,6 +23,7 @@
@change="radioChage"
>
<CouponItem
class="select-coupon"
:coupons="item"
:type="'noType'"
/>
@ -39,6 +41,7 @@
>
暂无可用的优惠券
</Empty>
<view class="action-height"></view>
<view class="button-action">
<view
class="animation-button"
@ -71,6 +74,7 @@ const popupRef = ref(false)
const currentCoupon = ref({})
const visible = ref(false)
const couponItem = ref(props.currentCouponId ? props.currentCouponId : -1)
const oldCoupon = ref(props.currentCouponId ? props.currentCouponId : -1)
const num = ref(0)
const radioValue = ref('')
const selectCouponPanel = ref(false)
@ -86,6 +90,7 @@ const handleSubmit = () => {
close()
return
}
oldCoupon.value = couponItem.value
emit('submitCoupon', {
couponId: couponItem.value
})
@ -100,6 +105,12 @@ const groupChange = (n) => {
}
}
const clickMask = () => {
if (oldCoupon.value !== couponItem.value) {
couponItem.value = oldCoupon.value
}
}
const radioChage = (n) => {
radioValue.value = n
num.value = 0
@ -142,8 +153,7 @@ defineExpose({
.select-box {
width: 100%;
margin-bottom: 20rpx;
margin-bottom: 30rpx;
.select-icon {
width: 20rpx;
height: 20rpx;
@ -153,15 +163,26 @@ defineExpose({
margin-bottom: 0 !important;
padding: 0 60rpx;
}
.select-coupon {
flex: 1;
}
}
}
.action-height{
width: 100%;
height: 80rpx;
}
.button-action {
position: fixed;
bottom: 0;
width: 100%;
left: 0;
.animation-button {
width: 100%;
height: 80rpx;
line-height: 80rpx;
border-radius: 80rpx;
text-align: center;
}
}

View File

@ -14,6 +14,7 @@ import UvRadio from "@/uni_modules/uv-radio/components/uv-radio/uv-radio.vue";
import { doPayment, PayType } from "@/utils/paymentUtils";
import CouponSelect from "@/pages/submitOrder/components/coupon-select.vue";
import Header from "@/components/Header/index.vue"
import { useScroll } from "@/hooks/useScroll";
const {getParams, push, goBack} = useRouter()
const {toast, loading, hideLoading} = useInterface();
@ -117,16 +118,15 @@ async function handleConfirm() {
subLoading.value = true
try {
const payInfo = await doCreateServiceOrder()
debugger
// 去拉取支付
await doPayment({type: payType.value, payInfo})
// #ifndef H5
push({url: '/pages/payStatus/index?type=1'}, {type: 'redirectTo'})
push({url: '/pages/payStatus/index'}, {type: 'redirectTo'})
// #endif
} catch (e) {
console.error(e)
toast({title: '支付失败'})
push({url: '/pages/payStatus/index?type=2'}, {type: 'redirectTo'})
push({url: '/pages/payStatus/index'}, {type: 'redirectTo'})
} finally {
subLoading.value = false
mainStore.cartId = null
@ -159,12 +159,7 @@ async function doCreateServiceOrder() {
}
}
const scrollTop = ref(0)
onPageScroll(e => {
scrollTop.value = e.scrollTop
})
useScroll()
/**
* 检查路由参数
@ -192,7 +187,7 @@ onLoad(async options => {
<template>
<view class="order-confirm">
<!-- header -->
<Header :scroll-top="scrollTop">
<Header>
提交订单
</Header>
<!-- 地址 -->

View File

@ -1,6 +1,7 @@
import { useMainStore } from "@/store/store";
import { useInterface } from "@/hooks/useInterface";
import { updateAvatar, updateUserInfo } from "@/api/user";
import { upload } from "@/api/api";
export function useRequest() {
const {loading, hideLoading, toast} = useInterface()
@ -12,12 +13,15 @@ export function useRequest() {
* @returns {Promise<void>}
*/
async function doUpdateAvatar(file) {
let data = new FormData()
data.append('avatarFile', file)
loading({title: '上传中...'})
await updateAvatar(data)
await userStore.getUserInfo()
hideLoading()
try {
await upload({
filePath: file.url,
name: 'avatarFile'
});
await userStore.getUserInfo()
} catch (e) {
console.error(e)
}
}
async function doUpdateUserInfo() {

View File

@ -44,8 +44,7 @@ const modelRef = ref()
* @returns {Promise<void>}
*/
async function afterChooseFile(event) {
const imgObj = await objectURLToBlob(event.file.url)
await doUpdateAvatar(new File([imgObj], '', {type: imgObj.type}))
await doUpdateAvatar(event.file)
}
function sexChange(event) {

View File

@ -48,8 +48,7 @@ export const useMainStore = defineStore('main', {
// console.log('--> % getUserInfo % res:\n', res)
},
async getAddressCityList() {
let res = await getAddressCityList()
this.areaList = res
this.areaList = await getAddressCityList()
},
init() {
let accessToken = cookie.get('accessToken')

View File

@ -16,12 +16,19 @@ const {loading, hideLoading} = useInterface()
export const WechatProvider = 'wxpay'
// 支付类型(后端用)
export const ServicePayType = {
0: 'weixin_h5', // H5微信内h5、微信外H5
1: 'weixin_applet', // 微信小程序
2: 'weixin_app' // 微信APP
export const ServiceFrom = {
'h5': 'weixin_h5', // H5微信内h5、微信外H5
'weixin': 'weixin_applet', // 微信小程序
'app': 'weixin_app' // 微信APP
}
export const ServicePayType = {
'h5': 'weixin_h5', // H5微信内h5、微信外H5
'weixin': 'weixin_applet', // 微信小程序
'app': 'weixin_app' // 微信APP
}
// 支付类型(前端用)
export const PayType = {
@ -116,12 +123,15 @@ async function _chooseEnvToPayment(options) {
*/
function _appWechatPay(payInfo) {
return new Promise(async (resolve, reject) => {
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest({
from: ServicePayType['2'],
paytype: ServicePayType['2'],
const payData = {
from: ServiceFrom['app'],
paytype: ServicePayType['app'],
uni: payInfo.orderId
})
}
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest(payData)
// 兼容性写法:防止电脑端用户支付后马上关闭支付弹窗导致失败
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(payData))
const orderInfo = {
appid: res.appId, // 微信开放平台审核通过的移动应用AppID 。
prepayid: res.merchant_id, // 请填写商户号mchid对应的值。
@ -148,13 +158,15 @@ function _appWechatPay(payInfo) {
*/
function _miniProgramPay(payInfo) {
return new Promise(async (resolve, reject) => {
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest({
from: ServicePayType['1'],
paytype: ServicePayType['1'],
const payData = {
from: ServiceFrom['weixin'],
paytype: ServiceFrom['weixin'],
uni: payInfo.orderId
})
}
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest(payData)
// 兼容性写法:防止电脑端用户支付后马上关闭支付弹窗导致失败
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(payData))
uni.requestPayment({
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
@ -177,12 +189,13 @@ function _miniProgramPay(payInfo) {
*/
async function _h5InWechatPay(payInfo) {
return new Promise(async (resolve, reject) => {
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest({
from: 'h5',
paytype: ServicePayType['0'],
const payData = {
from: ServiceFrom['h5'],
paytype: ServicePayType['h5'],
uni: payInfo.orderId
})
}
// 请求线上获取 res:{ appId,timeStamp,nonceStr,paySign,package,signType,mwebUrl,codeUrl,merchant_id,out_trade_no }
const res = await _doWechatPayRequest(payData)
/** 注册JS SDK */
jweixin.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来若要查看传入的参数可以在pc端打开参数信息会通过log打出仅在pc端时才会打印。
@ -203,6 +216,7 @@ async function _h5InWechatPay(payInfo) {
return reject(createMessage('微信版本过低,请升级微信版本', error))
}
});
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(payData))
/** 去拉起微信支付 */
jweixin.chooseWXPay({
timestamp: res.timeStamp, // 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
@ -212,16 +226,6 @@ async function _h5InWechatPay(payInfo) {
paySign: res.paySign, // 支付签名
success: (res) => {
// 支付成功后的回调函数
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(
{
from: 'h5',
paytype: ServicePayType['0'],
uni: payInfo.orderId
}
))
setTimeout(() => {
uni.redirectTo({url: '/pages/payStatus/index'})
}, 3000)
return resolve(createMessage('用户支付成功', res))
},
cancel: (r) => {
@ -246,20 +250,15 @@ async function _h5InWechatPay(payInfo) {
* @private
*/
async function _h5OutWechatPay(payInfo) {
const res = await _doWechatPayRequest({
from: 'h5',
paytype: ServicePayType['0'],
const payData = {
from: ServiceFrom['h5'],
paytype: ServicePayType['h5'],
uni: payInfo.orderId
})
}
const res = await _doWechatPayRequest(payData)
if (res && res.mwebUrl) {
// 缓存支付订单数据
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(
{
from: 'h5',
paytype: ServicePayType['0'],
uni: payInfo.orderId
}
))
uni.setStorageSync(CacheKey.PAY_INFO, JSON.stringify(payData))
location.replace(res.mwebUrl)
return Promise.resolve(createMessage('用户支付成功', {type: 'h5'}))
} else {

View File

@ -146,3 +146,12 @@ export function hasNetWork() {
})
})
}
/**
* 获取两数之间的随机数
* @param min
* @param max
*/
export function getRandom(min, max) {
return (Math.random() * (max - min) + min).toFixed(2);
}

206
utils/wechatUtils.js Normal file
View File

@ -0,0 +1,206 @@
/**
* @FileDescription: 微信工具类
* @Author: kahu
* @Date: 2023/6/29
* @LastEditors: kahu
* @LastEditTime: 2023/6/29
*/
/**
* 拍摄或从手机相册中选择图片或视频
* @param options
* @param options.count 数量 最大选取数量 最多可以选择的文件个数基础库2.25.0前最多可支持9个文件2.25.0及以后最多可支持20个文件
* @param options.mediaType 选取类型 image图片 video视频
* @param options.sourceType 选取的方式 album相册 camera相机
* @param options.maxDuration 录取视频的最大秒数,时间范围为 3s 至 60s 之间。不限制相册
* @param options.sizeType 是否压缩所选内容基础库2.25.0前仅对 mediaType 为 image 时有效2.25.0及以后对全量 mediaType 有效
* @param options.camera 拍摄时候的摄像头,仅在 sourceType 为 camera 时生效,使用前置或后置摄像头
*/
export function wxChooseMedia(options={}){
return new Promise((resolve, reject) => {
const mergeOptions = {
count:9,
mediaType:['image','video'],
sourceType:['album','camera'],
maxDuration:10,
sizeType:['original','compressed'],
camera:'back',
...options,
success:(res)=>{
resolve(res)
},
fail:(err)=>{
reject(err)
}
}
wx.chooseMedia(mergeOptions)
})
}
/**
* 调用微信接口编辑图片
* @param src 被编辑图片的临时路径
*/
export function wxEditImage(src){
return new Promise((resolve, reject)=>{
wx.editImage({
src,
success:(res)=>{
resolve(res.tempFilePath)
},
fail:(err)=>{
reject(err)
}
})
})
}
/**
* 调用微信接口裁剪图片
* @param options
* @param options.src 被裁剪图片的临时路径
* @param options.cropScale 裁剪比例
*/
export function wxCropImage(options){
return new Promise((resolve, reject) => {
const mergeOptions = {
cropScale:'1:1',
...options,
success:(res)=>{
resolve(res.tempFilePath)
},
fail:(err)=>{
reject(err)
}
}
wx.cropImage(mergeOptions)
})
}
/**
* 调用微信接口处理视频
* @param options
* @param options.filePath 视频的本地路径
* @param options.minDuration 视频的最小长度
* @param options.maxDuration 视频的最大长度
* @return { Promise<{duration:number,size:number,tempFilePath:string,tempThumbPath:string}> }
*/
export function wxEditVideo(options){
return new Promise((resolve, reject)=>{
if(options.minDuration>=options.maxDuration)return reject('MaxDuration Must Greater Than MinDuration')
wx.openVideoEditor({
...options,
success:(res)=>{
resolve(res)
},
fail:(err)=>{
reject(err)
}
})
})
}
/**
* 获取视频的详情
* @param src
*/
export function wxGetVideoInfo(src){
return new Promise((resolve, reject)=>{
wx.getVideoInfo({
src,
success:(res)=>{
resolve(res)
},
fail:(err)=>{
reject(err)
}
})
})
}
/**
* 获取照片的详情
* @param src
*/
export function wxGetImageInfo(src){
return new Promise((resolve, reject)=>{
wx.getImageInfo({
src,
success:(res)=>{
resolve(res)
},
fail:(err)=>{
reject(err)
}
})
})
}
/**
* 调用微信login获取code
*/
export function wxLogin(){
return new Promise((resolve, reject)=>{
uni.login({
provider: 'weixin',
success: (res) => {
resolve(res.code)
},
fail: (err) => {
reject({
message:'微信login方法出现错误',
data:err
})
}
})
})
}
/**
* 调用微信getUserProfile获取encryptedData、iv
*/
export function wxGetUserProfile(){
return new Promise((resolve, reject)=> {
uni.getUserProfile({
desc: '用于完善用户信息',
success: ({ encryptedData, iv }) => {
resolve({encryptedData,iv})
},
fail:(err)=>{
console.log(err)
reject({
message:'微信getUserProfile方法出现错误',
data:err
})
}
})
})
}
/**
* 调用地图获取位置信息
* @param params
*/
export function wxChooseLocation(params){
return new Promise((resolve, reject)=>{
const obj = {
success(e){
resolve(e)
},
fail(err){
console.log(err)
reject({
message:'微信ChooseLocation方法出现错误',
data:err
})
}
}
Object.keys(params).forEach(key=>{
// @ts-ignore
obj[key] = params[key]
})
uni.chooseLocation(obj)
})
}