代码提交

This commit is contained in:
黄少君
2023-11-14 17:21:03 +08:00
parent d0b337c596
commit dcab74274f
567 changed files with 22414 additions and 7375 deletions

View File

@ -0,0 +1,66 @@
<!--
@name: 空状态组件
@author: kahu4
@date: 2023-10-30 16:47
@descriptionindex
@update: 2023-10-30 16:47
-->
<script setup>
import { toRefs } from "vue";
const props = defineProps({
iconSrc: {},
padding: {
type: String,
default: () => '260rpx 0 0 0'
}
})
const {iconSrc, padding} = toRefs(props)
</script>
<template>
<view
class="empty-container"
:style="{padding}"
>
<view class="icon-box">
<slot name="icon">
<image :src="iconSrc" />
</slot>
</view>
<view class="info-row">
<slot>空空如也~</slot>
</view>
<view class="bottom-row">
<slot name="bottom"></slot>
</view>
</view>
</template>
<style
scoped
lang="scss"
>
.empty-container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding-top: 260rpx;
.icon-box {
image {
width: 170rpx;
height: 170rpx;
}
}
.info-row {
padding: 20rpx 0 30rpx 0;
font-size: 28rpx;
color: #333333;
}
}
</style>

368
components/Header/index.vue Normal file
View File

@ -0,0 +1,368 @@
<!--
@name: index
@author: kahu4
@date: 2023-11-09 10:56
@descriptionindex
@update: 2023-11-09 10:56
-->
<script setup>
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";
const HEADER_HEIGHT = 40 // header高度
const {goBack} = useRouter()
/**
* @property {String} systemBarAreaBg 系统导航条区域背景颜色
* @property {String} headerAreaBg header区域背景颜色
* @property {String} headerAreaTextColor header区域字体
* @property {Boolean} showReturn 是否展示返回按钮
* @property {String} returnColor 返回按钮的颜色
* @property {Number} returnSize 返回按钮的大小
* @property {Boolean||String} textShadow 字体阴影
* @property {Boolean} bgChangeByScroll 是否随着页面滚动增加header背景包括system-bar-area
* @property {String} bgChangeColor 滚动时候变换的颜色
* @property {Number} scrollTop 当前页面高度
* @property {Boolean} showRight 是否需要右边
* @property {number} leftWidth 左侧返回宽度设置了此属性title将不再居中
*
*/
const props = defineProps({
systemBarAreaBg: {
type: String,
default: () => '#FFFFFF00' // 透明 #FFFFFF00
},
headerAreaBg: {
type: String,
default: () => '#FFFFFF00' // 透明 #FFFFFF00
},
headerAreaTextColor: {
type: String,
default: () => '#000000'
},
showReturn: {
type: Boolean,
default: () => true
},
returnColor: {
type: String,
default: () => '#000'
},
returnSize: {
type: Number,
default: () => 22
},
textShadow: {
type: [Boolean, String],
default: () => false
},
bgChangeByScroll: {
type: Boolean,
default: () => true
},
bgChangeColor: {
type: String,
default: () => '#fff'
},
scrollTop: {
type: Number,
default: () => 0
},
propUp: {
type: Boolean,
default: () => true
},
showRight: {
type: Boolean,
default: () => true
},
leftWidth: {
type: Number,
default: () => 0
}
})
const {
systemBarAreaBg,
headerAreaBg,
headerAreaTextColor,
showReturn,
returnColor,
returnSize,
textShadow,
bgChangeByScroll,
bgChangeColor,
scrollTop,
propUp,
showRight,
leftWidth
} = toRefs(props)
const emits = defineEmits(['getSystemInfo', 'animation'])
// 高度信息 单位px
const heightInfo = ref({
safeAreaInsets: {bottom: 0, top: 0, left: 0, right: 0}, // 安全区信息
statusBarHeight: 0, // 状态栏高度
screenWidth: 0, // 屏幕内部宽度
screenHeight: 0, // 屏幕内部高度
})
/**
* 获取系统信息,设置导航条
*/
function getSystemInfo() {
const res = uni.getSystemInfoSync();
const heightObj = unref(heightInfo)
heightObj.safeAreaInsets = res.safeAreaInsets
heightObj.statusBarHeight = res.statusBarHeight
heightObj.screenWidth = res.screenWidth || res.windowWidth
heightObj.screenHeight = res.screenHeight || res.windowHeight
}
// 胶囊信息 单位px
const menuInfo = ref({
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0
})
/**
* 获取小程序右上角信息
*/
function getMenuInfo() {
const menuButtonBoundingClientRect = uni.getMenuButtonBoundingClientRect();
if (menuButtonBoundingClientRect) {
menuInfo.value = {...menuButtonBoundingClientRect}
}
}
// scss全局变量
const scssVarStyle = computed(() => {
return {
'--header-height': `${ HEADER_HEIGHT * 2 }rpx`
}
})
// 系统导航条区域样式
const systemBarAreaStyle = computed(() => {
return {
width: '100%',
height: `${ unref(heightInfo).safeAreaInsets.top * 2 }rpx`,
background: unref(systemBarAreaBg)
}
})
// header区域样式
const headerAreaStyle = computed(() => {
// 计算margin top
// margin-top (导航条高度 - 胶囊高度) / 2 永远确保胶囊在header中央
const marginTop = unref(menuInfo).height > 0 ? `-${ ((HEADER_HEIGHT - (unref(menuInfo).height)) / 2) * 2 }rpx` : 0
return {
width: '100%',
background: unref(headerAreaBg),
color: unref(headerAreaTextColor),
marginTop
}
})
// 文本样式
const textShadowStyle = computed(() => {
return {
textShadow: unref(textShadow) ? unref(textShadow) : 'none',
}
})
const titleStyle = computed(() => {
let width = unref(leftWidth) <= 0 ? '' : `calc( 100vw - var(--side-distance) - var(--side-distance) - ${ leftWidth.value }rpx )`
// #ifdef MP-WEIXIN
width = unref(leftWidth) <= 0 ? '' : `calc( 100vw - var(--side-distance) - var(--side-distance) - ${ leftWidth.value }rpx - ${ menuInfo.value.width }px )`
// #endif
return {
width,
left: unref(leftWidth) <= 0 ? '50%' : `calc( var(--side-distance) + ${ leftWidth.value }rpx )`,
textShadow: unref(textShadow) ? unref(textShadow) : 'none',
transform: unref(leftWidth) <= 0 ? 'translateX(-50%) translateY(-50%)' : 'translateX(0) translateY(-50%)'
}
})
// 滚动后背景样式
const scrollMaskStyle = computed(() => {
return {
background: unref(bgChangeColor),
opacity: unref(scrollTransparency)
}
})
// 总高度
const containerHeight = computed(() => {
return (unref(heightInfo).safeAreaInsets.top + HEADER_HEIGHT) * 2
})
let animation
const scrollTransparency = ref(0) // 滚动透明度
function doCreateAnimation() {
const scrollEnd = heightInfo.value.safeAreaInsets.bottom + HEADER_HEIGHT + 100
animation = createAnimation(0, scrollEnd, 0, 1)
}
watch(scrollTop, () => {
if (!bgChangeByScroll.value) return
if (!animation) doCreateAnimation()
scrollTransparency.value = animation(unref(scrollTop));
emits('animation', scrollTransparency.value)
})
defineExpose({containerHeight, heightInfo})
onLoad(() => {
getSystemInfo()
doCreateAnimation()
// #ifdef MP-WEIXIN
getMenuInfo()
// #endif
})
</script>
<template>
<view
class="container"
:style="scssVarStyle"
>
<view class="header-container">
<!-- 头部系统导航条区域 -->
<view
class="system-bar-area"
:style="systemBarAreaStyle"
>
</view>
<!-- header -->
<view
class="header-row"
:style="headerAreaStyle"
>
<view
class="left"
:style="textShadowStyle"
v-if="showReturn"
>
<slot name="left">
<uv-icon
name="arrow-left"
:color="returnColor"
:size="returnSize"
@click="goBack"
/>
</slot>
</view>
<view
class="title"
:style="titleStyle"
>
<slot>
</slot>
</view>
<view
class="right"
v-if="showRight"
>
<slot name="right">
</slot>
</view>
</view>
<!-- 背景 mask -->
<view
class="bg-mask"
:style="scrollMaskStyle"
></view>
</view>
<!-- 撑起 -->
<view
class="prop-up"
:style="{height:`${containerHeight}rpx`}"
v-if="propUp"
></view>
</view>
</template>
<style
scoped
lang="scss"
>
.container {
--header-height: 0rpx;
--side-distance: 30rpx;
$side-distance: var(--side-distance); // 侧边距
$header-height: var(--header-height); // header高度
.header-container {
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 99;
.system-bar-area {
width: 100%;
z-index: 1;
}
.header-row {
z-index: 1;
width: 100%;
height: $header-height;
position: relative;
.left, .right, .title {
position: absolute;
top: 50%;
height: 100%;
transform: translateY(-50%);
display: flex;
align-items: center;
}
.left {
left: $side-distance;
}
.right {
right: $side-distance;
}
.title {
left: 50%;
transform: translateX(-50%) translateY(-50%);
color: #333;
}
}
.bg-mask {
z-index: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
box-shadow: 0 0 15rpx rgba(162, 162, 162, 0.5);
}
}
.prop-up {
width: 100%;
height: $header-height;
}
}
</style>

View File

@ -0,0 +1,74 @@
<!--
@name: index
@author: kahu4
@date: 2023-11-06 18:08
@descriptionindex
@update: 2023-11-06 18:08
-->
<script setup>
import { toRefs } from "vue";
const props = defineProps({
text: {
type: String,
default: () => '加载中...'
}
})
const {text} = toRefs(props)
</script>
<template>
<view class="load-over flex flex-ai__center flex-jc__center">
<view class="line"></view>
<view class="text-box flex flex-ai__center flex-jc__center">
<view
:class="{text:true}"
:style="{animationDelay: `${index*0.2}s`}"
v-for="(item,index) in text"
:key="index"
>
{{ item }}
</view>
</view>
<view class="line"></view>
</view>
</template>
<style
scoped
lang="scss"
>
.load-over {
font-size: 28rpx;
position: relative;
color: $tips-color;
.line {
width: 130rpx;
height: 2rpx;
border-radius: 2rpx;
background: $tips-color;
}
.text-box {
@include usePadding(30, 20);
.text {
@include usePadding(5, 0);
animation: jump 3s ease-in-out infinite;
}
}
}
@keyframes jump {
0%, 60% {
transform: translateY(0);
}
20% {
transform: translateY(-15rpx);
}
40% {
transform: translateY(15rpx);
}
}
</style>

View File

@ -0,0 +1,44 @@
<!--
@name: index
@author: kahu4
@date: 2023-11-06 18:08
@descriptionindex
@update: 2023-11-06 18:08
-->
<script setup>
</script>
<template>
<view class="load-over flex flex-ai__center flex-jc__center">
<view class="line"></view>
<view class="text">
<slot>
到底了
</slot>
</view>
<view class="line"></view>
</view>
</template>
<style
scoped
lang="scss"
>
.load-over {
font-size: 28rpx;
position: relative;
color: $tips-color;
.line {
width: 130rpx;
height: 2rpx;
border-radius: 2rpx;
background: $tips-color;
}
.text {
@include usePadding(30, 20)
}
}
</style>

140
components/Modal/index.vue Normal file
View File

@ -0,0 +1,140 @@
<!--
@name: index
@author: kahu4
@date: 2023-10-31 15:14
@descriptionindex
@update: 2023-10-31 15:14
-->
<script setup>
import Popup from "@/components/Popup/index.vue";
import { ref, toRefs } from "vue";
const props = defineProps({
// 内容
content: {
type: String,
default: () => ''
},
// 确认文字
confirmText: {
type: String,
default: () => '确认'
},
// 取消文字
cancelText: {
type: String,
default: () => '取消'
},
// 是否展示取消
showCancel: {
type: Boolean,
default: () => true
},
// 是否展示提交
showConfirm: {
type: Boolean,
default: () => true
}
})
const {content, confirmText, cancelText, showCancel, showConfirm} = toRefs(props)
const emits = defineEmits(['confirm', 'cancel'])
const popupRef = ref()
function show() {
popupRef.value.show()
}
function close() {
// something before close todo
popupRef.value.close()
}
function clickBtn(type = 'confirm') {
emits(type)
close()
}
defineExpose({show, close})
</script>
<template>
<Popup
ref="popupRef"
:showCloseable="false"
mode="center"
>
<view class="modal-inner">
<view class="content">
<slot>
{{ content }}
</slot>
</view>
<view
class="btn-group"
v-if="showCancel||showConfirm"
>
<view
class="btn cancel"
v-if="showCancel"
@click="clickBtn('cancel')"
>
{{ cancelText }}
</view>
<view
class="btn"
v-if="showConfirm"
@click="clickBtn('confirm')"
>
{{ confirmText }}
</view>
</view>
</view>
</Popup>
</template>
<style
scoped
lang="scss"
>
.modal-inner {
width: 70vw;
padding: 50rpx 40rpx 20rpx 50rpx;
box-sizing: border-box;
.content {
width: 100%;
text-align: center;
font-size: 34rpx;
margin-bottom: 40rpx;
}
.btn-group {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
position: relative;
gap: 30rpx;
.btn {
box-sizing: border-box;
flex: 1 0 40%;
text-align: center;
height: 80rpx;
line-height: 80rpx;
border: 1rpx solid #ee6d46;
background: #ee6d46;
color: #fff;
}
.cancel {
background: #fff;
color: #ee6d46;
}
}
}
</style>

View File

@ -0,0 +1,28 @@
/**
* @name: index.data
* @author: kahu4
* @date: 2023-11-08 11:08
* @descriptionindex.data
* @update: 2023-11-08 11:08
* */
import wechatIcon from "@/static/icon/pay/weixin.png";
import { PayType } from "@/utils/paymentUtils";
import aliIcon from "@/static/icon/pay/zhifubao.png";
export const payRows = [
{
label: '微信支付',
eLabel: 'Wechat Pay',
icon: wechatIcon,
type: PayType["0"],
disabled: false
},
{
label: '支付宝支付',
eLabel: 'ALi Pay',
icon: aliIcon,
type: PayType["2"],
disabled: true
}
]

View File

@ -0,0 +1,165 @@
<!--
@name: 付款弹窗
@author: kahu
@date: 2023-11-08 10:53
@descriptionindex
@update: 2023-11-08 10:53
-->
<script setup>
import Popup from '@/components/Popup/index.vue';
import { ref, unref } from "vue";
import { payRows } from "./index.data";
import UvRadioGroup from "@/uni_modules/uv-radio/components/uv-radio-group/uv-radio-group.vue";
import UvRadio from "@/uni_modules/uv-radio/components/uv-radio/uv-radio.vue";
import { doPayment, PayType } from "@/utils/paymentUtils";
import { orderInfo as orderInfoRequest } from "@/api/order";
import { useInterface } from "@/hooks/useInterface";
import { useRouter } from "@/hooks/useRouter";
const {toast} = useInterface()
const {push} = useRouter()
const emits = defineEmits(['confirm', 'close']);
const popupRef = ref()
async function show(orderId) {
await doGetOrderDetail(orderId)
unref(popupRef).show()
}
function close() {
emits('close')
}
defineExpose({show})
const payType = ref(PayType["0"]) // 支付方式
const orderInfo = ref({}) // 订单信息
async function doGetOrderDetail(orderId) {
orderInfo.value = await orderInfoRequest({key: orderId});
}
const subLoading = ref(false)
async function handleSubmit() {
try {
subLoading.value = true
await doPayment({type: payType.value, payInfo: orderInfo.value})
subLoading.value = false
emits('confirm')
close()
} catch (e) {
console.error(e)
push({url: '/pages/payStatus/index?type=2'})
toast({title: '支付失败了'})
close()
}
}
</script>
<template>
<Popup
ref="popupRef"
title="支付"
@close="close"
>
<view class="pay-container">
<uv-radio-group
placement="column"
iconPlacement="right"
v-model="payType"
class="pay-box__inner flex flex-ai__center flex-jc__center flex-wrap"
shape="circle"
activeColor="#ec6e47"
>
<template
v-for="pay in payRows"
:key="pay.type"
>
<uv-radio
:name="pay.type"
:disabled="pay.disabled"
>
<view class="pay-row flex flex-ai__center">
<image
class="icon"
:src="pay.icon"
/>
<view class="info">
<view class="label">
{{ pay.label }}
</view>
<view>
{{ pay.eLabel }}
</view>
</view>
</view>
</uv-radio>
</template>
</uv-radio-group>
<view
class="animation-button sub-button"
:class="{active:subLoading}"
@click="handleSubmit"
>
立即支付
</view>
</view>
</Popup>
</template>
<style
scoped
lang="scss"
>
.pay-container {
@include usePadding(35, 0);
width: 100%;
.pay-row {
@include usePadding(0, 20);
font-size: 20rpx;
color: $tips-color;
border-bottom: 1rpx solid #e5e5e5;
&:last-child {
border-bottom: none;
}
.icon {
width: 80rpx;
height: 80rpx;
margin-right: 30rpx;
}
.info {
.label {
font-size: 28rpx;
color: #333;
}
}
}
:deep(.uv-radio) {
width: 100%;
transition: all .3s;
&:active {
scale: 1.1;
}
}
.sub-button {
$button-height: 80rpx;
margin: 20rpx auto;
height: $button-height;
line-height: $button-height;
border-radius: $button-height;
text-align: center;
}
}
</style>

169
components/Popup/index.vue Normal file
View File

@ -0,0 +1,169 @@
<!--
@name: index
@author: kahu4
@date: 2023-10-31 15:06
@descriptionindex
@update: 2023-10-31 15:06
-->
<script setup>
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'])
/**
* @property {String} title 标题
* @property {String} mode 模式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} showCloseable 是否展示关闭按钮
* @property {Boolean} isMaskClick 是否点击masker关闭弹窗
* @event {Function} open 打开弹窗
* @event {Function} close 关闭回调
*/
const props = defineProps({
title: {
type: String,
default: () => ''
},
mode: {
type: String,
default: () => 'bottom'
},
showCloseable: {
type: Boolean,
default: () => true
},
isMaskClick: {
type: Boolean,
default: () => true
},
})
const {title, mode, showCloseable, isMaskClick} = toRefs(props)
const popup = ref()
const isShow = ref(false)
/**
* 打开
*/
const show = () => {
nextTick(() => {
isShow.value = true
popup.value.open()
emit('open')
})
}
/**
* 关闭回调
*/
const close = () => {
isShow.value = false
popup.value.close()
}
const handlePopupChange = (e) => {
if (!e.show) emit('close')
}
defineExpose({
show,
close
})
</script>
<template>
<UniPopup
ref="popup"
:type="mode"
:is-mask-click="isMaskClick"
background-color="#fff"
@change="handlePopupChange"
class="y-popup"
>
<view class="popup_inner">
<view
class="head"
v-if="title||showCloseable"
>
<view></view>
<view>{{ title }}</view>
<slot name="rightOption">
<view
@click="close"
v-if="showCloseable"
>
<uv-icon
name="close"
color="#000"
size="16"
/>
</view>
</slot>
</view>
<slot></slot>
</view>
<!-- 如果是h5 增加tabbar高度 -->
<!-- <view class="h5-tabbar-height"></view>-->
</UniPopup>
</template>
<style
scoped
lang="scss"
>
.popup_inner {
padding: 20rpx 20rpx;
box-sizing: border-box;
.head {
padding: 20rpx 0;
font-weight: bolder;
font-size: 36rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.content {
.tips {
width: 100%;
text-align: center;
font-size: 22rpx;
color: #000;
}
.option_list_box {
.option_item {
margin: 15rpx 0;
.icon {
margin-right: 30rpx !important;
}
}
}
}
.button_content {
margin-top: 50rpx;
width: 100%;
height: 80rpx;
background: #ee6d46;
font-size: 40rpx;
color: #fff;
text-align: center;
line-height: 80rpx;
border-radius: 80rpx;
font-weight: bolder;
}
}
</style>

View File

@ -7,18 +7,20 @@
{{ title }}
</view>
<view class="activity-header-subtitle">
{{ subtitle }}
<slot name="subTitle">
{{ subtitle }}
</slot>
</view>
</view>
<view
class="activity-header-more"
@tap="handleMoreClick"
class="activity-header-more"
@tap="handleMoreClick"
>
<view class="activity-header-more-info">{{ more }}</view>
<img
class="image"
src="@/static/images/next.png"
alt=""
class="image"
src="@/static/images/next.png"
alt=""
>
</view>
</view>
@ -31,6 +33,7 @@
<script setup>
import { ref } from 'vue';
const props = defineProps(["title", "subtitle", 'more'])
const title = ref(props.title)
@ -46,7 +49,7 @@ const handleMoreClick = () => {
</script>
<style lang="less">
<style lang="scss">
.activity {
&-header {
@ -94,7 +97,8 @@ const handleMoreClick = () => {
}
}
&-body {}
&-body {
}
}
}

View File

@ -11,5 +11,5 @@ const props = defineProps(['size'])
const size = ref(props.size)
</script>
<style lang="less">
<style lang="scss">
</style>

View File

@ -33,7 +33,7 @@ export default {
}
</script>
<style lang="less">
<style lang="scss">
.buy-progress {
&-info {
flex: 1;

View File

@ -10,12 +10,11 @@
<script setup>
import { ref } from 'vue';
const props = defineProps(['class', 'width'])
console.log("gxs --> % props:\n", props)
const className = ref(props.class)
const width = ref(props.width)
</script>
<style lang="less">
<style lang="scss">
.card {
width: 100%;
background-color: #fff;

View File

@ -133,7 +133,6 @@ const emit = defineEmits(['callback'])
const items = ref(props.items || [])
// const defaultValue = ref(props.defaultValue)
// console.log("--> % defaultValue:\n", defaultValue)
const value = ref(props.value || '请选择')
const province = ref(props.items)
@ -151,7 +150,6 @@ watch(() => props.items, (next) => {
})
watch(() => props.defaultValue, (next) => {
console.log("--> % defaultValue % next:\n", next)
value.value = `${next.province.name} ${next.city.name} ${next.district.name}`
setDefaultValue(items.value, next)
})
@ -179,7 +177,6 @@ const setDefaultValue = (items, value) => {
}
province.value = items
items.map(prov => {
console.log("--> % setDefaultValue % prov:\n", prov)
if (prov.name == value.province.name) {
city.value = prov.id
provinceActive.value = {
@ -205,7 +202,6 @@ const setDefaultValue = (items, value) => {
})
}
})
console.log(provinceActive.value, cityActive.value, districtActive.value)
}
const open = () => {
@ -248,23 +244,8 @@ const selectCity = (selectItem, index) => {
}
const selectDistrict = (selectItem, index) => {
console.log("--> % selectDistrict % selectItem:\n", selectItem)
districtActive.value = selectItem
value.value = `${provinceActive.value?.name} ${cityActive.value?.name} ${districtActive.value?.name}`
console.log({
province: {
id: provinceActive.value?.id,
name: provinceActive.value?.name,
},
city: {
id: cityActive.value?.id,
name: cityActive.value?.name,
},
district: {
id: districtActive.value?.id,
name: districtActive.value?.name,
},
})
emit('callback', {
province: {
id: provinceActive.value?.id,
@ -283,7 +264,7 @@ const selectDistrict = (selectItem, index) => {
}
</script>
<style lang="less">
<style lang="scss">
.cityselect {
width: 100%;
height: 75%;

View File

@ -10,7 +10,7 @@ const props = defineProps(['min'])
const min = ref(props?.min != undefined)
</script>
<style lang="less">
<style lang="scss">
.container {
padding: 0 34rpx;

View File

@ -0,0 +1,106 @@
<template>
<!-- <uv-scroll-list>-->
<view class="reply">
<view class="reply-cont">
<view class="reply-user">
<view class="reply-user-pic">
<uv-image
:src="data.avatar"
width="50rpx"
height="50rpx"
></uv-image>
</view>
<view class="reply-user-name">
{{ data.nickname }}
</view>
</view>
<view class="reply-text">
{{ data.comment }}
</view>
</view>
<view
class="reply-pic flex flex-ai__center"
v-if="data.pics && data.pics.length>0"
>
<template
v-for="(pic,index) in data.pics.slice(0,1)"
:key="index"
>
<image
class="image"
:src="pic"
@click="doPreviewImage(index, data.pics)"
/>
</template>
</view>
</view>
<!-- </uv-scroll-list>-->
</template>
<script setup>
import { toRefs } from 'vue';
import { useImage } from "@/hooks/useImage";
const props = defineProps(['data'])
const {data} = toRefs(props);
const {preview} = useImage()
function doPreviewImage(current, urls) {
preview({
current,
urls,
})
}
</script>
<style lang="scss">
.reply {
display: flex;
border: 1rpx solid #E6E6E6;
border-radius: 15rpx;
overflow: hidden;
width: 565rpx;
margin-top: 30rpx;
&-cont {
flex: 1;
padding: 20rpx;
.reply-user {
display: flex;
align-items: center;
&-pic {
margin-right: 10rpx;
}
&-name {
font-size: 24rpx;
color: #333333;
}
}
.reply-text {
font-size: 24rpx;
line-height: 32rpx;
color: #333333;
margin-top: 20rpx;
}
}
&-pic {
width: 187rpx;
}
}
.reply-pic {
.image {
width: 180rpx;
height: 180rpx;
}
}
</style>

View File

@ -16,11 +16,10 @@ import { ref } from 'vue'
const props = defineProps(['data'])
const goodsData = props.data
console.log("--> % goodsData:\n", goodsData)
</script>
<style lang="less">
<style lang="scss">
.goods {
width: 100%;
padding: 30rpx 0;

View File

@ -1,144 +1,251 @@
<template>
<uv-popup
ref="popupRef"
mode="bottom"
:style="{ height: '50%' }"
round="round"
<Popup
ref="popupRef"
:showCloseable="false"
@close="emit('close')"
>
<view
class="goodAttrSelect"
v-if="storeInfo"
class="goodAttrSelect"
>
<view class="goodAttrSelect-goods">
<goods
list
min
:storeName="storeInfo.storeName"
:price="storeInfo.price"
surplus="200"
:data="storeInfo"
/>
<view
class="goodAttrSelect-goods"
v-if="curAttr"
>
<uv-image
class="attr-image"
:src="curAttr.image || detailData.storeInfo.image"
width="150rpx"
height="150rpx"
></uv-image>
<view class="attr-info">
<view class="name">
{{ storeInfo.storeName }}
</view>
<view class="attr-info-bottom">
<view class="price">¥{{ curAttr.price }}</view>
<view class="stock">库存{{ curAttr.stock }}</view>
</view>
</view>
</view>
<div class="line"></div>
<view class="goodAttrSelect-attr row">
<view
class="goodAttrSelect-attr row"
v-if="curAttr"
>
<view class="goodAttrSelect-attr-title">
数量
</view>
<view class="goodAttrSelect-attr-content">
<uv-number-box
v-model="storeNum"
min="1"
/>
<!-- cart number -->
<view
class="cart-num flex flex-ai__center flex-jc__sb"
@click.stop=""
>
<view
class="button"
@click="handleCartNumberChange(curAttr,'minus')"
>
<uv-icon
name="minus"
color="#333"
size="12"
></uv-icon>
</view>
<view class="input">
<input
type="number"
inputmode="numeric"
v-model="storeNum"
@blur="(e)=>handleCartNumberInputChange(e,curAttr)"
>
</view>
<view
class="button"
@click="handleCartNumberChange(curAttr,'plus')"
>
<uv-icon
name="plus"
color="#333"
size="12"
></uv-icon>
</view>
</view>
<!-- <uv-number-box
v-model="storeNum"
min="1"
:max="curAttr.stock"
></uv-number-box>-->
</view>
</view>
<div class="line"></div>
<view
class="goodAttrSelect-attr"
v-for="(item, index) in productAttr"
:key="index"
class="goodAttrSelect-attr"
v-for="(item, index) in productAttr"
:key="index"
>
<view class="goodAttrSelect-attr-title">
{{ item.attrName }}
</view>
<view class="goodAttrSelect-attr-content">
<space>
<space wrap="warp">
<view
:class="{ attr: true, check: select[item.attrName] == attr.attr }"
v-for="(attr, attrIndex) in item.attrValue"
:key="attrIndex"
@tap="handleSelectAttr(item.attrName, attr)"
>{{ attr.attr }}</view>
:class="{ attr: true, check: selectedAttr[index] === attr }"
v-for="(attr, attrIndex) in item.attrValueArr"
:key="attrIndex"
@tap="handleSelectAttr(index, attr)"
>{{ attr }}
</view>
</space>
</view>
</view>
<view class="goodAttrSelect-action">
<uv-button
round
block
type="primary"
@tap="handleSubmit"
round
block
type="primary"
@tap="handleSubmit"
>
确定
</uv-button>
</view>
</view>
</uv-popup>
</Popup>
</template>
<script setup>
import { ref, watch } from 'vue';
import { getProductDetail, getProductAddCollect, getProductDelCollect } from '@/api/product'
import { ref, unref } from 'vue';
import { getProductDetail } from '@/api/product'
import { useInterface } from "@/hooks/useInterface";
import Popup from '@/components/Popup/index.vue';
const props = defineProps(["id"])
const emit = defineEmits(['onSelect', 'submit'])
const emit = defineEmits(['onSelect', 'submit', 'close'])
const popupRef = ref()
const select = ref({})
const selectedAttr = ref([])
const visible = ref(false)
const detailData = ref(null)
const storeInfo = ref(null)
const productAttr = ref(null)
const productValue = ref(null)
const storeNum = ref(1)
const curAttr = ref(null)
const defaultSelectAttrStr = ref(undefined)
const selectAttrPanel = ref(false)
const handleGetDetail = async (id) => {
const detail = await getProductDetail(id)
if (detail) {
detailData.value = detail
storeInfo.value = detail.storeInfo
productAttr.value = detail.productAttr
productValue.value = detail.productValue
let attr = {}
detail.productAttr.forEach(item => {
attr[item.attrName] = ''
})
select.value = attr
if (!defaultSelectAttrStr.value) {
// 设置默认选中
let attr = []
detail.productAttr.forEach((item, i) => {
attr[i] = item.attrValueArr[0]
})
selectedAttr.value = attr
let selectedAttrStr = selectedAttr.value.join(',')
curAttr.value = productValue.value[selectedAttrStr]
} else {
selectedAttr.value = unref(defaultSelectAttrStr).split(',')
curAttr.value = productValue.value[defaultSelectAttrStr.value]
}
}
}
const handleSelectAttr = (attr, value) => {
select.value[attr] = value.attr
const handleSelectAttr = (index, attr) => {
selectedAttr.value[index] = attr
let selectedAttrStr = selectedAttr.value.join(',')
curAttr.value = productValue.value[selectedAttrStr]
}
const handleSubmit = () => {
// let value = []
// productAttr.value.map(item => {
// value.push(selectedAttr.value[item.attrName] || '')
// })
let value = []
productAttr.value.map(item => {
value.push(select.value[item.attrName] || '')
})
if (value.includes('')) {
uni.showToast({
title: '请选择规格',
icon: 'none',
duration: 2000,
})
return
}
emit('select', {
store: productValue.value[value.toString()],
// if (value.includes('')) {
// uni.showToast({
// title: '请选择规格',
// icon: 'none',
// duration: 2000,
// })
// return
// }
let selectedAttrStr = selectedAttr.value.join(',')
emit('submit', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
emit('select', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
emit('onSelect', {
store: productValue.value[selectedAttrStr],
num: storeNum.value
})
}
const open = () => {
popupRef.value.open()
const {toast} = useInterface()
/**
* 用户手动输入改变数量
* @param e
* @param item
* @returns {*}
*/
function handleCartNumberInputChange(e, item) {
const value = Number(e.detail.value)
if (value <= 0) {
storeNum.value = 1
toast({title: '至少选一件哦~'})
}
if (value > item.stock) {
storeNum.value = item.stock
toast({title: '超出库存啦~'})
}
storeNum.value = value
}
/**
* 用户点击购物车+-改变数量
* @param item
* @param type
* @returns {*}
*/
function handleCartNumberChange(item, type = 'plus') {
if (type === 'plus') {
// +
if (storeNum.value + 1 > item.stock) return toast({title: '超出库存啦~'})
storeNum.value += 1
} else {
// -
if (storeNum.value - 1 <= 0) return toast({title: '至少选一件哦~'})
storeNum.value -= 1
}
}
const open = (cartNum = 1, selectAttrStr = undefined) => {
defaultSelectAttrStr.value = selectAttrStr
storeNum.value = cartNum
handleGetDetail(props.id)
popupRef.value.show()
}
const close = () => {
popupRef.value.close()
emit('close')
}
@ -148,12 +255,47 @@ defineExpose({
})
</script>
<style lang="less">
<style lang="scss">
.goodAttrSelect {
height: 100%;
&-goods {
padding: 20rpx 20rpx;
padding: 40rpx 30rpx;
display: flex;
.attr-image {
}
.attr-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 30rpx;
.name {
font-size: 28rpx;
line-height: 40rpx;
color: #333333;
}
&-bottom {
display: flex;
justify-content: space-between;
.price {
font-size: 30rpx;
line-height: 42rpx;
color: #333333;
}
.stock {
font-size: 24rpx;
line-height: 42rpx;
color: #666666;
}
}
}
}
&-action {
@ -178,7 +320,8 @@ defineExpose({
margin-bottom: 30rpx;
}
&-content {}
&-content {
}
}
}
@ -191,13 +334,49 @@ defineExpose({
height: 68rpx;
border: 1rpx solid #333333;
padding: 0 20rpx;
line-height: 68rpx;
font-size: 28rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: flex;
justify-content: center;
align-items: center;
&.check {
background: #333333;
color: #fff;
}
}
.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

@ -0,0 +1,96 @@
<template>
<Popup
ref="popupRef"
:showCloseable="false"
@close="emit('close')"
>
<view class="coupon-box">
<template v-if="!showEmpty">
<template v-for="item in couponList">
<CouponItem
:coupons="item"
:type="'get'"
/>
</template>
</template>
<Empty
:iconSrc="emptyIcon"
v-else
>
暂时没有可领取的优惠券~
</Empty>
</view>
</Popup>
</template>
<script setup>
import { ref, unref } from 'vue';
import { useInterface } from "@/hooks/useInterface";
import Empty from "@/components/Empty/index.vue"
import { getProductCoupon } from "@/api/coupon";
import CouponItem from "@/pages/discountCoupon/components/CouponItem.vue";
import Popup from '@/components/Popup/index.vue';
import emptyIcon from "@/static/icon/empty/优惠券.png";
const props = defineProps(["id"])
const emit = defineEmits(['submitCoupon', 'close'])
const couponList = ref([])
const popupRef = ref(false)
const currentCoupon = ref({})
const visible = ref(false)
const showEmpty = ref(false)
const selectCouponPanel = ref(false)
const handleGetDetail = async (id) => {
const list = await getProductCoupon(id)
if (list) {
couponList.value = list
}
}
const handleSubmit = () => {
emit('submitCoupon', {
couponId: currentCoupon.value.id
})
}
const {toast} = useInterface()
/**
* 用户手动输入改变数量
* @param e
* @param item
* @returns {*}
*/
const selectCurrentCoupon = (item) => {
currentCoupon.value = item
}
const open = () => {
handleGetDetail(props.id)
popupRef.value.show()
}
const close = () => {
popupRef.value.close()
emit('close')
}
defineExpose({
open,
close
})
</script>
<style lang="scss">
.coupon-box {
padding: 24rpx;
box-sizing: border-box;
width: 100%;
}
</style>

View File

@ -1,31 +1,31 @@
<template>
<view
:class="{ goods: true, 'goods-card': card, 'goods-list': list, 'goods-min': min, 'goods-fill': fill, 'goods-round': round }"
:class="{ goods: true, 'goods-card': card, 'goods-list': list, 'goods-min': min, 'goods-fill': fill, 'goods-round': round }"
@tap="toDetail"
>
<view
class="goods-header"
@tap="toDetail"
class="goods-header"
>
<view class="goods-thumb">
<image
:src="data.image"
class="goods-thumb-img"
style="object-fit: cover"
:src="data.image"
class="goods-thumb-img"
style="object-fit: cover"
/>
</view>
</view>
<view :class="['goods-content']">
<view
class="goods-storeName"
@tap="toDetail"
>{{ data.storeName }}</view>
class="goods-storeName"
>{{ data.storeName }}
</view>
<view
class="goods-list-model"
v-if="selectModel"
class="goods-list-model"
v-if="selectModel"
>
<div
class="goods-list-model-border"
@tap.stop="handleOpenSelect"
class="goods-list-model-border"
@tap.stop="handleOpenSelect"
>
<view class="goods-list-model-label">{{ data.attrInfo.sku }}</view>
<view class="goods-list-model-action icon">
@ -34,8 +34,8 @@
</div>
</view>
<view
class="goods-list-model-info"
v-if="model"
class="goods-list-model-info"
v-if="model"
>
<view class="goods-list-model-label">{{ data.attrInfo.sku }}</view>
@ -47,45 +47,54 @@
<view class="goods-info">
<view class="goods-info-left">
<view
class="goods-desc"
v-if="groupNum"
>{{ data.groupNum }}人团</view>
class="goods-desc"
v-if="groupNum"
>{{ data.groupNum }}人团
</view>
<view
class="goods-price-row"
v-if="primary"
class="goods-price-row"
v-if="primary"
>
<view class="goods-price goods-price-primary">
¥{{ data.price }}
¥{{price || data.price }}
</view>
<view
class="goods-price goods-price-original"
v-if="original"
class="goods-price goods-price-original"
v-if="original"
>
¥{{ data.original }}
</view>
</view>
<view
class="goods-price goods-price-default"
v-if="!primary"
>¥{{ data.price }}</view>
class="goods-price goods-price-default"
v-if="!primary"
>
<slot
name="price"
:row="data"
>
¥{{price || data.price }}
</slot>
</view>
</view>
<view class="goods-info-action">
<view class="goods-info-action-btn">
<slot name="action"></slot>
</view>
<view
class="goods-info-action-desc"
v-if="stock"
v-if="stock"
class="goods-info-action-desc"
>
仅剩{{ data.stock }}
<text v-if="data.stock">仅剩{{ data.stock }}</text>
<text v-else>库存不足</text>
</view>
<view
class="buy-num"
v-if="purchase"
class="buy-num"
v-if="purchase"
>
<view class="buy-num-info-desc">
{{ data.purchase }}
x{{ purchase }}
</view>
</view>
@ -95,21 +104,21 @@
<view class="buy-progress">
<view class="buy-progress-info">
<view
class="buy-progress-info-desc"
v-if="quantity"
class="buy-progress-info-desc"
v-if="quantity"
>
限量{{ data.quantity }}
</view>
<uv-line-progress
:percentage="50"
:showText="false"
:percentage="50"
:showText="false"
/>
</view>
<view class="buy-progress-action">
<uv-button
round
block
type="primary"
round
block
type="primary"
>
立即抢购
</uv-button>
@ -118,17 +127,18 @@
</view>
</view>
<good-attr-select
ref="selectAttrPanel"
:id="data.id"
@select="handleSelectAttr"
ref="selectAttrPanel"
:id="data.id"
@select="handleSelectAttr"
/>
</view>
</template>
<script setup>
import { navigateTo } from '@/utils/router';
import { ref } from 'vue'
const props = defineProps(['data', 'min', 'groupNum', 'original', 'stock', 'primary', 'card', 'list', 'buyProgress', 'quantity', 'selectModel', 'model', 'purchase', 'link', 'fill', 'round'])
import { useRouter } from "@/hooks/useRouter";
const props = defineProps(['data', 'min', 'groupNum', 'original', 'stock', 'primary', 'card', 'list', 'buyProgress', 'quantity', 'selectModel', 'model', 'purchase', 'link', 'fill', 'round','price'])
// 团购人数
const groupNum = ref(props.groupNum)
@ -150,23 +160,25 @@ const selectModel = ref(props.selectModel !== undefined)
const model = ref(props.model !== undefined)
// 购买数量
const purchase = ref(props.purchase)
// 购买价格
const price = ref(props.price)
// 原价
const original = ref(props.original)
// 小尺寸
const min = ref(props.min)
const selectAttrPanel = ref(null)
const link = ref(props.link !== undefined)
const fill = ref(props.fill !== undefined)
const round = ref(props.round !== undefined)
const {push} = useRouter()
const toDetail = () => {
if (!link.value) { return }
navigateTo({
url: '/pages/goodsDetail/goodsDetail',
query: {
id: props.data.id
}
})
if (!link.value) {
return
}
push({url: '/pages/goodsDetail/goodsDetail'}, {data: {id: props.data.id}})
}
const handleOpenSelect = () => {
@ -179,7 +191,7 @@ const handleSelectAttr = () => {
</script>
<style lang="less">
<style lang="scss">
.goods {
position: relative;
padding: 30rpx 0;
@ -188,7 +200,6 @@ const handleSelectAttr = () => {
&-card {
display: flex;
flex-direction: column;
.goods {
&-content {
padding: 0 20rpx;
@ -205,7 +216,8 @@ const handleSelectAttr = () => {
width: 100%;
&-img {
width: 100%;
width: 300rpx;
height: 300rpx;
display: block;
}
}
@ -213,43 +225,48 @@ const handleSelectAttr = () => {
}
}
&-header {}
&-header {
}
&-thumb {
background: #FAFAFA;
}
&-content {}
&-content {
}
&-storeName {
line-height: 40rpx;
font-size: 28rpx;
font-weight: 500;
color: #333333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
&-price {
&-row {
display: flex;
align-items: center;
.goods-price {}
.goods-price {
}
}
&-primary {
line-height: 42rpx;
font-size: 30rpx;
font-weight: 500;
color: #EE6D46;
}
&-default {
line-height: 40rpx;
font-size: 28rpx;
font-weight: 500;
color: #333333;
line-height: 42rpx;
font-size: 30rpx;
color: #EE6D46;
}
&-original {
@ -274,10 +291,12 @@ const handleSelectAttr = () => {
align-items: flex-end;
justify-content: space-between;
&-left {}
&-left {
}
&-action {
&-btn {}
&-btn {
}
&-desc {
line-height: 28rpx;
@ -290,21 +309,22 @@ const handleSelectAttr = () => {
}
&-image {
&-img {}
&-img {
}
}
&-list {
display: flex;
flex-direction: row;
padding: 14rpx;
padding: 40rpx 34rpx 30rpx;
box-sizing: border-box;
width: 100%;
.goods {
&-thumb {
margin-bottom: 0;
width: 220rpx;
height: 220rpx;
width: 200rpx;
height: 200rpx;
&-img {
width: 100%;
@ -314,17 +334,16 @@ const handleSelectAttr = () => {
}
&-content {
padding-right: 40rpx;
margin-left: 30rpx;
margin-left: 40rpx;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 0;
}
}
&-model {
display: flex;
@ -341,7 +360,8 @@ const handleSelectAttr = () => {
padding: 0 10rpx;
}
&-info {}
&-info {
}
&-label {
line-height: 38rpx;
@ -466,7 +486,6 @@ const handleSelectAttr = () => {
// }
// &-desc {
// color: #999999;
// font-size: 24rpx;
@ -556,7 +575,6 @@ const handleSelectAttr = () => {
}
.buy-num {
&-info-desc {
color: #999999;
font-size: 24rpx;

View File

@ -0,0 +1,169 @@
<!--
@name: 上哦
@author: kahu4
@date: 2023-11-02 18:30
@descriptionGoods
@update: 2023-11-02 18:30
-->
<script setup>
import { toRefs } from "vue";
import { GOODS_ITEM_TYPE } from "@/components/goodsComponents/utils/index.type";
import { useRouter } from "@/hooks/useRouter";
const props = defineProps({
goods: {
type: Object,
default: () => ({})
},
imgWidth: {
type: String,
default: '100%'
},
/** 图片比例 */
ratio: {
type: String,
default: () => "1/1"
},
infoPadding: {
type: String,
default: () => "0 0"
},
/** title是否换行 */
titleNowrap: {
type: Boolean,
default: () => true
},
/** title 文字大小 rpx */
titleSize: {
type: Number,
default: () => 28
},
type: {
type: String,
default: () => GOODS_ITEM_TYPE.NEW
},
row: {
type: Boolean,
default: false
},
})
const {
goods,
imgWidth,
ratio,
infoPadding,
titleNowrap,
titleSize,
row,
} = toRefs(props)
const {push} = useRouter()
function toDetail() {
push({url: '/pages/goodsDetail/goodsDetail'}, {data: {id: goods.value.id}})
}
</script>
<template>
<view
:class="{'goods':true,'row':row}"
@click="toDetail"
>
<!-- goods image -->
<view
class="goods-image"
:style="{
'width':imgWidth,
'aspect-ratio':ratio
}"
>
<image
:src="goods.image"
class="image"
mode="aspectFill"
/>
</view>
<!-- good info -->
<view
class="goods-info"
:style="{
'padding':infoPadding,
'width':row?`calc( 100% - ${imgWidth} )`:'100%',
'height':row?`${imgWidth}`:'auto'
}"
>
<view
:class="{
'title-row':true,
'nowrap': titleNowrap
}"
:style="{
'font-size':`${titleSize}rpx`
}"
>
{{ goods.storeName }}
</view>
<slot
name="options"
:goods="goods"
></slot>
</view>
</view>
</template>
<style
scoped
lang="scss"
>
.goods {
position: relative;
width: 100%;
&-image {
flex-shrink: 0;
.image {
width: 100%;
height: 100%;
}
}
&-info {
width: 100%;
box-sizing: border-box;
.title-row {
user-select: none;
width: 100%;
//padding: 14rpx 0;
font-size: 28rpx;
}
.nowrap {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
.goods-image {
}
.goods-info {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
}
}
</style>

View File

@ -0,0 +1,143 @@
<!--
@name: Options
@author: kahu4
@date: 2023-11-03 15:30
@descriptionOptions
@update: 2023-11-03 15:30
-->
<script setup>
import { toRefs } from "vue";
const props = defineProps({
row: {
type: Boolean,
default: false
},
showBtn: {
type: Boolean,
default: false
},
btnText: {
type: String,
default: '立即抢购'
},
oldPrice: {
type: Boolean,
default: false
},
tips: {
type: Boolean,
default: false
},
schedule: {
type: Boolean,
default: false
},
goods: {
type: Object,
default: () => ({})
}
})
const {row, showBtn, oldPrice, btnText} = toRefs(props)
</script>
<template>
<view
class="options flex flex-jc__sb flex-ai__center"
:class="{row,'flex-ai__center':row,'flex-ai__end':!row}"
>
<view class="price-box ">
<view class="price-row flex flex-jc__sb flex-ai__end">
<view class="price">
{{ goods.price }}
</view>
<view
class="old-price"
v-if="oldPrice"
>
{{ goods.otPrice }}
</view>
</view>
<view
class="tips-row"
v-if="tips"
>
限量100件
</view>
<view
class="process-row"
v-if="schedule"
>
<view class="schedule"></view>
</view>
</view>
<view
class="btn-box"
:class="{'no-btn':!showBtn}"
>
<slot
:goods="goods"
name="right-tip"
>
{{ showBtn ? btnText : `仅剩${ goods.stock }${ goods.unit || '' }` }}
</slot>
</view>
</view>
</template>
<style
scoped
lang="scss"
>
.options {
width: 100%;
.price-box {
.price-row {
font-size: 28rpx;
color: $primary-color;
.old-price {
color: $tips-color;
font-size: 20rpx;
text-decoration: line-through;
}
}
.tips-row {
color: $tips-color;
font-size: 24rpx;
margin: 6rpx 0;
}
.process-row {
width: 100%;
height: 15rpx;
background: #f5f5f5;
position: relative;
.schedule {
background: $primary-color;
width: 50%;
height: 100%;
}
}
}
.btn-box {
@include usePadding(15, 10);
flex-shrink: 0;
background: $primary-color;
color: $white-color;
font-size: 24rpx;
}
.no-btn {
padding: 0 0;
background: none;
color: $tips-color;
}
}
</style>

View File

@ -0,0 +1,15 @@
/**
* @name: index.type
* @author: kahu4
* @date: 2023-11-02 18:51
* @descriptionindex.type
* @update: 2023-11-02 18:51
* */
export const GOODS_ITEM_TYPE = {
NEW: 'NEW', // 新品首发
GROUP: 'GROUP', // 拼团
SECKILL: 'SECKILL', // 秒杀
BARGAIN: 'BARGAIN', // 砍价
SHOPPING_CART: 'CART', // 购物车数据
CONFIRM_ORDER: 'ORDER', // 确认订单数据
}

View File

@ -0,0 +1,101 @@
<!--
@name: HomeGoodItem
@author: kahu4
@date: 2023-10-27 14:47
@descriptionHomeGoodItem
@update: 2023-10-27 14:47
-->
<script setup>
import { useRouter } from "@/hooks/useRouter";
const {push} = useRouter()
const props = defineProps({
good: {
type: Object
},
background: {
type: String,
default: '#fff'
},
infoPadding: {
type: Number,
default: 0
}
})
const toDetail = () => {
push({url: '/pages/goodsDetail/goodsDetail'},{data:{id: props.good.id}})
}
</script>
<template>
<view
class="good-item"
:style="{background:props.background}"
@tap="toDetail"
>
<view class="image">
<image
:src="props.good.image"
mode="aspectFill"
/>
</view>
<view
class="info-box"
:style="{padding:`0 ${props.infoPadding}rpx`}"
>
<view class="title-row">
{{ props.good.storeName }}
</view>
<view class="price-row">
<slot
name="bottom"
:good="good"
></slot>
</view>
</view>
<view class="blank"></view>
</view>
</template>
<style
scoped
lang="scss"
>
.good-item {
position: relative;
width: 100%;
border-radius: 15rpx;
overflow: hidden;
.image {
width: 100%;
aspect-ratio: 1 / 1;
image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
}
.info-box {
width: 100%;
box-sizing: border-box;
.title-row {
margin: 14rpx 0;
max-width: 25vw;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
font-weight: 500;
font-size: 26rpx;
}
}
.blank {
width: 100%;
height: 22rpx;
}
}
</style>

View File

@ -7,10 +7,9 @@
<script setup>
import { ref, defineProps, reactive } from 'vue';
const props = defineProps(['size'])
console.log("gxs --> % props:\n", props)
</script>
<style lang="less">
<style lang="scss">
.layout {
min-height: 100%;
}

View File

@ -1,6 +1,6 @@
<template>
<view>
</view>
</template>
@ -9,12 +9,12 @@
name:"logo",
data() {
return {
};
}
}
</script>
<style lang="less">
<style lang="scss">
</style>
</style>

View File

@ -1,206 +1,119 @@
<template>
<view>
<view
:class="['order', className]"
v-if="data"
:class="['order', className]"
v-if="data"
>
<view
class="order-header"
@tap="toOrderInfo"
class="order-header"
@tap="toOrderInfo"
>
<view class="order-logo">
<!-- <image :src="data." alt=""> -->
<view class="color-y">Y</view>
<view>SHOP商城</view>
</view>
<view class="order-status status-1">
{{ data._status._msg }}
<view
class="order-status status-2"
v-if="data._status"
>
{{ data._status._title }}
</view>
</view>
<view
class="order-goods"
@tap="toOrderInfo"
class="order-goods"
@tap="toOrderInfo"
>
<goods
list
interval
desc="3"
showAction
model
purchase="x3"
:data="item.productInfo"
v-for="(item, index) in data.cartInfo"
/>
</view>
<view
class="order-info"
@tap="toOrderInfo"
>
<text>总价¥{{ payPrice }}</text>
<text>优惠¥{{ deductionPrice }}</text>
<text>运费-¥{{ freightPrice }}</text>
<text>总计¥{{ totalPrice }}</text>
</view>
<view
class="order-actions"
v-if="data._status._type == 0"
>
<!-- 未支付 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
v-for="(item, index) in data.cartInfo"
class="order-evaluate"
:class="{evaluateBtn: data._status._type == 3}"
>
<view
class="order-actions-delete"
@tap="handleCancel"
>
取消订单
</view>
<view
class="order-actions-primary"
@tap="handlePay"
>
立即付款
</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 1"
>
<!-- 待发货 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-delete"
@tap="toSelectRefundGood"
>
申请退款
</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 2"
>
<!-- 待收货 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-delete"
@tap="toOrderInfo"
>
查看物流
</view>
<view
class="order-actions-primary"
@tap="handleOrderTake"
>
确认收货
</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 3"
>
<!-- 待评价 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-delete"
@tap="handleDelete"
>
删除订单
</view>
<view
class="order-actions-primary"
@tap="toOrderInfo"
v-if="data._status._type == 3 && item.isReply === 0"
class="order-actions-primary order-evaluate-btn"
@tap.stop="toEvaluate(item)"
>
去评价
</view>
<goods
list
interval
desc="3"
showAction
model
:purchase="item.cartNum"
:data="item.productInfo"
>
<template #price>
{{ item.truePrice }}
</template>
</goods>
</view>
</view>
<view
class="order-info"
@tap="toOrderInfo"
>
<view class="text">总价¥{{ data.cost }}</view>
<view class="text">优惠¥{{ couponPrice }}</view>
<view class="text">运费¥{{ totalPostage }}</view>
<view class="text flex flex-ai__center">总计
<view class="total-price">¥{{ totalPrice }}</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 4"
>
<!-- 已完成 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-delete"
@tap="handleDelete"
>
删除订单
</view>
<view
class="order-actions-primary"
@tap="toOrderInfo"
>
查看订单
</view>
</view>
</view>
<view v-if="data._status">
<view
class="order-actions"
<view
class="order-actions"
v-if="data._status._type == 5"
>
<!-- 退款中 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-primary"
@tap="toOrderInfo"
>
查看订单
>
<!-- 未支付 -->
<view class="order-actions-left">
</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 6"
>
<!-- 已退款 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-primary"
@tap="toOrderInfo"
>
查看订单
</view>
</view>
</view>
<view
class="order-actions"
v-if="data._status._type == 7"
>
<!-- 退款 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-primary"
@tap="toOrderInfo"
>
查看订单
<view class="order-actions-btns">
<view
v-if="data._status._type == 0"
class="order-actions-default"
@tap="handleCancel"
>
取消订单
</view>
<view
v-if="data._status._type == 0"
class="order-actions-primary"
@tap="handlePay"
>
立即付款
</view>
<view
v-if="['1','2','3','4'].includes(data._status._type)"
class="order-actions-default"
@tap="toSelectRefundGood"
>
申请退款
</view>
<view
class="order-actions-default"
v-if="data._status._type == 2"
@tap="toOrderInfo"
>
查看物流
</view>
<view
v-if="data._status._type == 2"
class="order-actions-primary"
@tap="handleOrderTake"
>
确认收货
</view>
<view
v-if="['4'].includes(data._status._type)"
class="order-actions-default"
@tap="handleDelete"
>
删除订单
</view>
</view>
</view>
</view>
@ -211,24 +124,24 @@
<script setup>
import { ref } from 'vue';
import { orderCancel, orderInfo, orderDelete, orderTake } from '@/api/order'
import { orderCancel, orderDelete, orderTake } from '@/api/order'
import { navigateTo, back } from '@/utils/router'
import { useRouter } from "@/hooks/useRouter";
const emit = defineEmits(['refresh'])
const {push} = useRouter()
const emit = defineEmits(['refresh', 'pay'])
const props = defineProps(['class', 'data'])
const data = ref(props.data)
// 运费金额
const freightPrice = ref(props.data.freightPrice)
const totalPostage = ref(props.data.totalPostage)
// 实际支付金额
const payPrice = ref(props.data.payPrice)
// 优惠券金额
const couponPrice = ref(props.data.couponPrice)
// 抵扣金额
const deductionPrice = ref(props.data.deductionPrice)
// 订单总价
const totalPrice = ref(props.data.totalPrice)
@ -247,7 +160,6 @@ const handleCancel = async () => {
duration: 2000
});
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
@ -255,44 +167,40 @@ const handleCancel = async () => {
const toSelectRefundGood = () => {
navigateTo({
url: '/pages/selectRefundGood/selectRefundGood',
query: {
push({url: '/pages/selectRefundGood/selectRefundGood'}, {
data: {
orderId: data.value.orderId,
id: data.value.id,
status: data.value.status
}
})
}
const handlePay = () => {
navigateTo({
url: '/pages/selectPlay/selectPlay',
query: {
key: data.value.unique,
orderId: data.value.orderId,
}
})
emit('pay', data.value.orderId)
/* push({url: '/pages/selectPlay/selectPlay'}, {
data: {
key: data.value.unique,
orderId: data.value.orderId,
}
})*/
}
const toOrderInfo = () => {
navigateTo({
url: '/pages/orderInfo/orderInfo',
query: {
push({url: '/pages/orderInfo/orderInfo'}, {
data: {
key: data.value.unique,
orderId: data.value.orderId,
// id: data.value.id,
}
})
}
const toEvaluate = () => {
navigateTo({
url: '/pages/evaluate/evaluate',
query: {
unique: data.value.unique,
const toEvaluate = (item) => {
push({url: '/pages/evaluate/evaluate'}, {
data: {
unique: item.unique,
orderId: data.value.orderId,
}
})
@ -306,7 +214,7 @@ const handleDelete = async () => {
success: async (res) => {
if (res.confirm) {
await orderDelete({
id: data.value.orderId
uni: data.value.orderId
})
data.value = null
uni.showToast({
@ -314,29 +222,28 @@ const handleDelete = async () => {
duration: 2000
});
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
}
const handleOrderTake = async () => {
uni.showModal({
title: '提示',
content: '确认收货',
success: async (res) => {
console.log("gxs --> % success: % res:\n", res)
if (res.confirm) {
let option = {
uni: data.value.orderId,
}
const res = await orderTake(option)
await orderTake(option)
emit('refresh')
uni.showToast({
title: '已收货',
duration: 2000
});
} else if (res.cancel) {
// console.log('用户点击取消');
}
}
});
@ -346,5 +253,6 @@ const handleOrderTake = async () => {
</script>
<style lang="less">
<style lang="scss">
</style>

View File

@ -0,0 +1,200 @@
<template>
<view>
<view
:class="['order', className]"
v-if="data"
>
<view
class="order-header"
@tap="toOrderInfo"
>
<view class="order-logo">
<view class="color-y">Y</view>
<view>SHOP商城</view>
</view>
<view class="order-status status-2">
{{ refundOrderStatus[data.state] }}
</view>
</view>
<view
class="order-goods"
@tap="toOrderInfo"
>
<goods
list
interval
desc="3"
showAction
model
:purchase="item.cartNum"
:data="item.productInfo"
:price="item.truePrice"
v-for="(item, index) in data.cartInfo"
/>
</view>
<view
class="refund-order-info"
@tap="toOrderInfo"
>
<text
class="text"
:class="data.state === 3 && 'color-y'"
>{{ data.state === 3 ? '成功退款' : '待平台处理' }}
</text>
<text class="text">{{ data.state === 3 ? '成功' : '' }}退款¥{{data.refundAmount}}</text>
</view>
<view class="order-actions">
<!-- 已完成 -->
<view class="order-actions-left">
</view>
<view class="order-actions-btns">
<view
class="order-actions-default"
@tap="handleDelete"
v-if="[3,4].includes(data.state)"
>
删除记录
</view>
<view
class="order-actions-default"
@tap="handleRevoke"
v-if="[0,1].includes(data.state)"
>
撤销申请
</view>
<view
class="order-actions-default"
@tap="toRefund"
v-if="data.state === 5"
>
再次申请
</view>
<view
class="order-actions-default"
@tap="toAddLogistics"
v-if="data.state === 1"
>
填写物流
</view>
<view
class="order-actions-primary"
@tap="toOrderInfo"
>
查看详情
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { afterSalesOrderDelete, afterSalesOrderRevoke } from '@/api/order'
import { useRouter } from "@/hooks/useRouter";
import { refundOrderStatus } from '@/config'
const {push} = useRouter()
const emit = defineEmits(['refresh'])
const props = defineProps(['class', 'data'])
const data = ref(props.data)
// 运费金额
const freightPrice = ref(props.data.freightPrice)
// 实际支付金额
const payPrice = ref(props.data.payPrice)
// 优惠券金额
const couponPrice = ref(props.data.couponPrice)
// 订单总价
const totalPrice = ref(props.data.totalPrice)
// 删除记录
const handleDelete = async () => {
uni.showModal({
title: '提示',
content: '确认删除记录',
success: async (res) => {
if (res.confirm) {
await afterSalesOrderDelete({
id: data.value.id,
orderCode: data.value.orderCode
})
data.value = null
uni.showToast({
title: '已删除',
duration: 2000
});
} else if (res.cancel) {
}
}
});
}
// 撤销申请
const handleRevoke = async () => {
uni.showModal({
title: '提示',
content: '确认撤销申请吗',
success: async (res) => {
if (res.confirm) {
await afterSalesOrderRevoke({
id: data.value.id,
key: data.value.orderCode
})
data.value = null
uni.showToast({
title: '已撤销',
duration: 2000
});
} else if (res.cancel) {
}
}
});
}
// 查看详情
const toOrderInfo = () => {
push({url: '/pages/refundInfo/refundInfo'}, {
data: {
id: data.value.id,
}
})
}
// 填写物流
const toAddLogistics = () => {
push({url: '/pages/addLogistics/addLogistics'}, {
data: {
id: data.value.id,
orderCode: data.value.orderCode
}
})
}
// 再次申请
const toRefund = () => {
console.log(data.value)
push({url: '/pages/refund/refund'}, {
data: {
refundType: data.value.serviceType,
goods: data.value.cartInfo.map(v=>{
return v.productAttrUnique
}).toString(),
id: data.value.orderId
}
})
}
</script>
<style lang="scss">
</style>

View File

@ -1,72 +1,116 @@
<template>
<view :class="{
<view
:class="{
reply: true,
noPic: !data?.pics
}">
}"
>
<view class="reply-content">
<view class="reply-user">
<view class="reply-user-pic">
<image
class="img"
src="http://yshop.l1.ttut.cc/admin-api/infra/file/4/get/7599202df273d25f1ce3aeba21165a544849248fda23aad090098d7be0c063c9.jpeg"
/>
<!-- <image :src="data.avatar" /> -->
</view>
<view class="reply-user-name">
<view class="name">
<!-- {{ data.nickname }} -->
我的名字
<view class="reply-warp">
<view class="reply-user">
<view class="reply-user-pic">
<image
class="img"
:src="data.avatar"
@click="doPreviewImage(0,[data.avatar])"
/>
</view>
<view class="productScore">
<uv-rate
count="5"
:value="data.productScore"
readonly
size="12"
gutter="1"
active-color="#ee6d46"
inactive-color="#999999"
></uv-rate>
<view class="reply-user-name">
<view class="name">
{{ data.nickname }}
</view>
<view class="productScore">
<uv-rate
count="5"
:value="data.productScore"
readonly
size="26rpx"
gutter="1"
active-color="#ee6d46"
inactive-color="#999999"
></uv-rate>
</view>
</view>
</view>
<view class="reply-time">{{ data.createTime }}</view>
</view>
<view class="reply-text">
{{ data.comment }}
</view>
<view
class="reply-pic flex flex-ai__center"
v-if="data.pics && data.pics.length>0"
>
<template
v-for="(pic,index) in data.pics"
:key="index"
>
<image
class="image"
:src="pic"
@click="doPreviewImage(index, data.pics)"
/>
</template>
</view>
<view class="reply-sku">规格{{data.sku}}</view>
</view>
<view
class="reply-pic"
v-if="data?.pics"
>
<uv-album
:urls="data?.pics"
space="8rpx"
></uv-album>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { toRefs } from 'vue';
import { useImage } from "@/hooks/useImage";
const props = defineProps(['data'])
const data = ref(props.data)
const {data} = toRefs(props);
const {preview} = useImage()
function doPreviewImage(current, urls) {
preview({
current,
urls,
})
}
</script>
<style lang="less">
<style lang="scss">
.reply {
margin-top: 20rpx;
border-top: 1rpx solid #E6E6E6;
padding-bottom: 10rpx;
&-content {
padding: 32rpx 34rpx;
border-right: 0;
flex: 1;
font-size: 30rpx;
color: #333333;
.reply-warp {
display: flex;
justify-content: space-between;
align-items: center;
.reply-time {
font-size: 24rpx;
line-height: 1em;
color: #999999;
}
}
}
&.noPic {
.reply-content {}
.reply-content {
}
}
&-pic {
width: 180rpx;
height: 180rpx;
background: #FFFFFF;
border-radius: 15rpx;
//width: 180rpx;
//height: 180rpx;
//background: #FFFFFF;
//border-radius: 15rpx;
padding: 0 24rpx;
}
&-user {
@ -75,14 +119,12 @@ const data = ref(props.data)
margin-bottom: 16rpx;
&-pic {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
margin-right: 10rpx;
margin-right: 20rpx;
.img {
width: 60rpx;
height: 60rpx;
width: 70rpx;
height: 70rpx;
display: block;
border-radius: 50%;
@ -90,18 +132,33 @@ const data = ref(props.data)
}
&-name {
margin-left: 6rpx;
line-height: 33rpx;
font-size: 24rpx;
line-height: 40rpx;
font-size: 28rpx;
color: #333333;
}
}
&-content {
border-right: 0;
flex: 1;
font-size: 30rpx;
&-text {
margin-top: 30rpx;
font-size: 28rpx;
line-height: 40rpx;
color: #333333;
}
&-sku{
font-size: 24rpx;
line-height: 33rpx;
color: #999999;
margin-top: 20rpx;
}
}
.reply-pic {
gap: 20rpx;
margin-bottom: 20rpx;
.image {
width: 180rpx;
height: 180rpx;
}
}
</style>

View File

@ -1,220 +1,219 @@
<template>
<view
:class="classObject"
:style="[getStyle]"
>
<slot></slot>
</view>
</template>
<script setup>
import { ref, watch, onMounted } from "vue"
const props = defineProps([
'direction',
'align',
'wrap',
'justify',
'size',
'height',
'padding',
'flex',
'border'
])
console.log("--> % props:\n", props)
const classObject = ref({})
const handleClassObject = (props) => {
console.log("--> % handleClassObject % props:\n", props.value)
let className = 'yshop-space'
let direction = props.direction
let align = props.align
let wrap = props.wrap
let justify = props.justify
let flex = props.flex
let border = props.border
if (border) {
className += ' yshop-space-border'
}
if (direction) {
className += ` yshop-space-${direction}`
}
if (wrap) {
className += ` yshop-space-${wrap}`
}
if (align) {
className += ` yshop-space-align-${align}`
}
if (justify) {
className += ` yshop-space-justify-${justify}`
}
console.log("--> % handleClassObject % className:\n", className)
classObject.value = className
}
const getStyle = ({
size = 6,
wrap,
height,
padding,
flex
}) => {
let innerStyle = {}
// let size = size.value
// let wrap = wrap.value
// let height = height.value
// let padding = padding.value
if (height) {
innerStyle.height = `${height}rpx`
}
if (typeof size === 'number') {
innerStyle.gap = size + 'px'
}
if (wrap) {
innerStyle.flexWrap = 'wrap'
}
if (typeof size === 'string') {
switch (size) {
case 'small':
innerStyle.gap = '8rpx'
break
case 'middle':
innerStyle.gap = '16rpx'
break
case 'large':
innerStyle.gap = '24rpx'
break
}
}
if (typeof padding === 'string') {
innerStyle.padding = `${padding}rpx`
}
if (Object.prototype.toString.call(padding) === '[object Array]') {
if (typeof padding === 'object') {
if (padding.length == 1) {
innerStyle.padding = `${padding[0]}rpx`
}
if (padding.length == 2) {
innerStyle.padding = `${padding[0]}rpx ${padding[1]}rpx`
}
}
}
if (Object.prototype.toString.call(size) === '[object Array]') {
if (typeof size === 'object') {
if (size.length == 1) {
innerStyle.gap = `${size[0]}rpx`
}
if (size.length == 2) {
innerStyle.gap = `${size[0]}rpx ${size[1]}rpx`
}
}
}
if (flex) {
innerStyle.flex = flex
}
return innerStyle
}
onMounted(() => {
handleClassObject(props)
getStyle(props)
})
</script>
<style lang="less">
space {
width: 100%;
}
.yshop-space {
display: flex;
width: 100%;
flex: 1;
&-vertical {
flex-direction: column;
}
&-align-center {
align-items: center;
}
&-align-start {
align-items: flex-start;
}
&-align-end {
align-items: flex-end;
}
&-justify-center {
justify-content: center;
}
&-justify-between {
justify-content: space-between;
}
&-justify-around {
justify-content: space-around;
}
&-align-end {
align-items: flex-end;
}
&-align-baseline {
align-items: baseline;
}
&-item:empty {
display: none;
}
&-border {
border-bottom: 1rpx solid #f9f9f9;
}
}
// #ifdef APP-PLUS
.yshop-space {
&>view {
margin-right: 8rpx;
}
&>text {
margin-right: 8rpx;
}
&>image {
margin-right: 8rpx;
}
.u-tag-wrapper {
margin-right: 8rpx;
}
.u-tag {
margin-right: 8rpx;
}
}
// #endif
</style>
<template>
<view
:class="classObject"
:style="[getStyle]"
>
<slot></slot>
</view>
</template>
<script setup>
import { onMounted, ref } from "vue"
const props = defineProps([
'direction',
'align',
'wrap',
'justify',
'size',
'height',
'padding',
'flex',
'border'
])
const classObject = ref({})
const handleClassObject = (props) => {
let className = 'yshop-space'
let direction = props.direction
let align = props.align
let wrap = props.wrap
let justify = props.justify
let flex = props.flex
let border = props.border
if (border) {
className += ' yshop-space-border'
}
if (direction) {
className += ` yshop-space-${ direction }`
}
if (wrap) {
className += ` yshop-space-${ wrap }`
}
if (align) {
className += ` yshop-space-align-${ align }`
}
if (justify) {
className += ` yshop-space-justify-${ justify }`
}
classObject.value = className
}
const getStyle = ({
size = 6,
wrap,
height,
padding,
flex
}) => {
let innerStyle = {}
// let size = size.value
// let wrap = wrap.value
// let height = height.value
// let padding = padding.value
if (height) {
innerStyle.height = `${ height }rpx`
}
if (typeof size === 'number') {
innerStyle.gap = size + 'px'
}
if (wrap) {
innerStyle.flexWrap = 'wrap'
}
if (typeof size === 'string') {
switch (size) {
case 'small':
innerStyle.gap = '8rpx'
break
case 'middle':
innerStyle.gap = '16rpx'
break
case 'large':
innerStyle.gap = '24rpx'
break
}
}
if (typeof padding === 'string') {
innerStyle.padding = `${ padding }rpx`
}
if (Object.prototype.toString.call(padding) === '[object Array]') {
if (typeof padding === 'object') {
if (padding.length == 1) {
innerStyle.padding = `${ padding[0] }rpx`
}
if (padding.length == 2) {
innerStyle.padding = `${ padding[0] }rpx ${ padding[1] }rpx`
}
}
}
if (Object.prototype.toString.call(size) === '[object Array]') {
if (typeof size === 'object') {
if (size.length == 1) {
innerStyle.gap = `${ size[0] }rpx`
}
if (size.length == 2) {
innerStyle.gap = `${ size[0] }rpx ${ size[1] }rpx`
}
}
}
if (flex) {
innerStyle.flex = flex
}
return innerStyle
}
onMounted(() => {
handleClassObject(props)
getStyle(props)
})
</script>
<style lang="scss">
space {
width: 100%;
}
.yshop-space {
display: flex;
width: 100%;
flex: 1;
&-warp {
flex-wrap: wrap;
}
&-vertical {
flex-direction: column;
}
&-align-center {
align-items: center;
}
&-align-start {
align-items: flex-start;
}
&-align-end {
align-items: flex-end;
}
&-justify-center {
justify-content: center;
}
&-justify-between {
justify-content: space-between;
}
&-justify-around {
justify-content: space-around;
}
&-align-end {
align-items: flex-end;
}
&-align-baseline {
align-items: baseline;
}
&-item:empty {
display: none;
}
&-border {
border-bottom: 1rpx solid #f9f9f9;
}
}
// #ifdef APP-PLUS
.yshop-space {
& > view {
margin-right: 8rpx;
}
& > text {
margin-right: 8rpx;
}
& > image {
margin-right: 8rpx;
}
.u-tag-wrapper {
margin-right: 8rpx;
}
.u-tag {
margin-right: 8rpx;
}
}
// #endif
</style>

View File

@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keyup', listener)
})
},
render: () => {}
}
// #endif

View File

@ -0,0 +1,275 @@
<template>
<view class="uni-popup-dialog">
<view class="uni-dialog-title">
<text class="uni-dialog-title-text" :class="['uni-popup__'+dialogType]">{{titleText}}</text>
</view>
<view v-if="mode === 'base'" class="uni-dialog-content">
<slot>
<text class="uni-dialog-content-text">{{content}}</text>
</slot>
</view>
<view v-else class="uni-dialog-content">
<slot>
<input class="uni-dialog-input" v-model="val" :type="inputType" :placeholder="placeholderText" :focus="focus" >
</slot>
</view>
<view class="uni-dialog-button-group">
<view class="uni-dialog-button" @click="closeDialog">
<text class="uni-dialog-button-text">{{closeText}}</text>
</view>
<view class="uni-dialog-button uni-border-left" @click="onOk">
<text class="uni-dialog-button-text uni-button-color">{{okText}}</text>
</view>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
/**
* PopUp 弹出层-对话框样式
* @description 弹出层-对话框样式
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} value input 模式下的默认值
* @property {String} placeholder input 模式下输入提示
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} mode = [base|input] 模式、
* @value base 基础对话框
* @value input 可输入对话框
* @property {String} content 对话框内容
* @property {Boolean} beforeClose 是否拦截取消事件
* @event {Function} confirm 点击确认按钮触发
* @event {Function} close 点击取消按钮触发
*/
export default {
name: "uniPopupDialog",
mixins: [popup],
emits:['confirm','close'],
props: {
inputType:{
type: String,
default: 'text'
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: [String, Number],
default: ''
},
type: {
type: String,
default: 'error'
},
mode: {
type: String,
default: 'base'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
},
cancelText:{
type: String,
default: ''
},
confirmText:{
type: String,
default: ''
}
},
data() {
return {
dialogType: 'error',
focus: false,
val: ""
}
},
computed: {
okText() {
return this.confirmText || t("uni-popup.ok")
},
closeText() {
return this.cancelText || t("uni-popup.cancel")
},
placeholderText() {
return this.placeholder || t("uni-popup.placeholder")
},
titleText() {
return this.title || t("uni-popup.title")
}
},
watch: {
type(val) {
this.dialogType = val
},
mode(val) {
if (val === 'input') {
this.dialogType = 'info'
}
},
value(val) {
this.val = val
}
},
created() {
// 对话框遮罩不可点击
this.popup.disableMask()
// this.popup.closeMask()
if (this.mode === 'input') {
this.dialogType = 'info'
this.val = this.value
} else {
this.dialogType = this.type
}
},
mounted() {
this.focus = true
},
methods: {
/**
* 点击确认按钮
*/
onOk() {
if (this.mode === 'input'){
this.$emit('confirm', this.val)
}else{
this.$emit('confirm')
}
if(this.beforeClose) return
this.popup.close()
},
/**
* 点击取消按钮
*/
closeDialog() {
this.$emit('close')
if(this.beforeClose) return
this.popup.close()
},
close(){
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-dialog {
width: 300px;
border-radius: 11px;
background-color: #fff;
}
.uni-dialog-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 25px;
}
.uni-dialog-title-text {
font-size: 16px;
font-weight: 500;
}
.uni-dialog-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
align-items: center;
padding: 20px;
}
.uni-dialog-content-text {
font-size: 14px;
color: #6C6C6C;
}
.uni-dialog-button-group {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
border-top-color: #f5f5f5;
border-top-style: solid;
border-top-width: 1px;
}
.uni-dialog-button {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
justify-content: center;
align-items: center;
height: 45px;
}
.uni-border-left {
border-left-color: #f0f0f0;
border-left-style: solid;
border-left-width: 1px;
}
.uni-dialog-button-text {
font-size: 16px;
color: #333;
}
.uni-button-color {
color: #007aff;
}
.uni-dialog-input {
flex: 1;
font-size: 14px;
border: 1px #eee solid;
height: 40px;
padding: 0 10px;
border-radius: 5px;
color: #555;
}
.uni-popup__success {
color: #4cd964;
}
.uni-popup__warn {
color: #f0ad4e;
}
.uni-popup__error {
color: #dd524d;
}
.uni-popup__info {
color: #909399;
}
</style>

View File

@ -0,0 +1,143 @@
<template>
<view class="uni-popup-message">
<view class="uni-popup-message__box fixforpc-width" :class="'uni-popup__'+type">
<slot>
<text class="uni-popup-message-text" :class="'uni-popup__'+type+'-text'">{{message}}</text>
</slot>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
/**
* PopUp 弹出层-消息提示
* @description 弹出层-消息提示
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [success|warning|info|error] 主题样式
* @value success 成功
* @value warning 提示
* @value info 消息
* @value error 错误
* @property {String} message 消息提示文字
* @property {String} duration 显示时间,设置为 0 则不会自动关闭
*/
export default {
name: 'uniPopupMessage',
mixins:[popup],
props: {
/**
* 主题 success/warning/info/error 默认 success
*/
type: {
type: String,
default: 'success'
},
/**
* 消息文字
*/
message: {
type: String,
default: ''
},
/**
* 显示时间,设置为 0 则不会自动关闭
*/
duration: {
type: Number,
default: 3000
},
maskShow:{
type:Boolean,
default:false
}
},
data() {
return {}
},
created() {
this.popup.maskShow = this.maskShow
this.popup.messageChild = this
},
methods: {
timerClose(){
if(this.duration === 0) return
clearTimeout(this.timer)
this.timer = setTimeout(()=>{
this.popup.close()
},this.duration)
}
}
}
</script>
<style lang="scss" >
.uni-popup-message {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
}
.uni-popup-message__box {
background-color: #e1f3d8;
padding: 10px 15px;
border-color: #eee;
border-style: solid;
border-width: 1px;
flex: 1;
}
@media screen and (min-width: 500px) {
.fixforpc-width {
margin-top: 20px;
border-radius: 4px;
flex: none;
min-width: 380px;
/* #ifndef APP-NVUE */
max-width: 50%;
/* #endif */
/* #ifdef APP-NVUE */
max-width: 500px;
/* #endif */
}
}
.uni-popup-message-text {
font-size: 14px;
padding: 0;
}
.uni-popup__success {
background-color: #e1f3d8;
}
.uni-popup__success-text {
color: #67C23A;
}
.uni-popup__warn {
background-color: #faecd8;
}
.uni-popup__warn-text {
color: #E6A23C;
}
.uni-popup__error {
background-color: #fde2e2;
}
.uni-popup__error-text {
color: #F56C6C;
}
.uni-popup__info {
background-color: #F2F6FC;
}
.uni-popup__info-text {
color: #909399;
}
</style>

View File

@ -0,0 +1,187 @@
<template>
<view class="uni-popup-share">
<view class="uni-share-title"><text class="uni-share-title-text">{{shareTitleText}}</text></view>
<view class="uni-share-content">
<view class="uni-share-content-box">
<view class="uni-share-content-item" v-for="(item,index) in bottomData" :key="index" @click.stop="select(item,index)">
<image class="uni-share-image" :src="item.icon" mode="aspectFill"></image>
<text class="uni-share-text">{{item.text}}</text>
</view>
</view>
</view>
<view class="uni-share-button-box">
<button class="uni-share-button" @click="close">{{cancelText}}</button>
</view>
</view>
</template>
<script>
import popup from '../uni-popup/popup.js'
import {
initVueI18n
} from '@dcloudio/uni-i18n'
import messages from '../uni-popup/i18n/index.js'
const { t } = initVueI18n(messages)
export default {
name: 'UniPopupShare',
mixins:[popup],
emits:['select'],
props: {
title: {
type: String,
default: ''
},
beforeClose: {
type: Boolean,
default: false
}
},
data() {
return {
bottomData: [{
text: '微信',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/c2b17470-50be-11eb-b680-7980c8a877b8.png',
name: 'wx'
},
{
text: '支付宝',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/d684ae40-50be-11eb-8ff1-d5dcf8779628.png',
name: 'wx'
},
{
text: 'QQ',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/e7a79520-50be-11eb-b997-9918a5dda011.png',
name: 'qq'
},
{
text: '新浪',
icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/0dacdbe0-50bf-11eb-8ff1-d5dcf8779628.png',
name: 'sina'
},
// {
// text: '百度',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/1ec6e920-50bf-11eb-8a36-ebb87efcf8c0.png',
// name: 'copy'
// },
// {
// text: '其他',
// icon: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/2e0fdfe0-50bf-11eb-b997-9918a5dda011.png',
// name: 'more'
// }
]
}
},
created() {},
computed: {
cancelText() {
return t("uni-popup.cancel")
},
shareTitleText() {
return this.title || t("uni-popup.shareTitle")
}
},
methods: {
/**
* 选择内容
*/
select(item, index) {
this.$emit('select', {
item,
index
})
this.close()
},
/**
* 关闭窗口
*/
close() {
if(this.beforeClose) return
this.popup.close()
}
}
}
</script>
<style lang="scss" >
.uni-popup-share {
background-color: #fff;
border-top-left-radius: 11px;
border-top-right-radius: 11px;
}
.uni-share-title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
}
.uni-share-title-text {
font-size: 14px;
color: #666;
}
.uni-share-content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: center;
padding-top: 10px;
}
.uni-share-content-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
flex-wrap: wrap;
width: 360px;
}
.uni-share-content-item {
width: 90px;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: center;
padding: 10px 0;
align-items: center;
}
.uni-share-content-item:active {
background-color: #f5f5f5;
}
.uni-share-image {
width: 30px;
height: 30px;
}
.uni-share-text {
margin-top: 10px;
font-size: 14px;
color: #3B4144;
}
.uni-share-button-box {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
padding: 10px 15px;
}
.uni-share-button {
flex: 1;
border-radius: 50px;
color: #666;
font-size: 16px;
}
.uni-share-button::after {
border-radius: 50px;
}
</style>

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "cancel",
"uni-popup.ok": "ok",
"uni-popup.placeholder": "pleace enter",
"uni-popup.title": "Hint",
"uni-popup.shareTitle": "Share to"
}

View File

@ -0,0 +1,8 @@
import en from './en.json'
import zhHans from './zh-Hans.json'
import zhHant from './zh-Hant.json'
export default {
en,
'zh-Hans': zhHans,
'zh-Hant': zhHant
}

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "确定",
"uni-popup.placeholder": "请输入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@ -0,0 +1,7 @@
{
"uni-popup.cancel": "取消",
"uni-popup.ok": "確定",
"uni-popup.placeholder": "請輸入",
"uni-popup.title": "提示",
"uni-popup.shareTitle": "分享到"
}

View File

@ -0,0 +1,45 @@
// #ifdef H5
export default {
name: 'Keypress',
props: {
disable: {
type: Boolean,
default: false
}
},
mounted () {
const keyNames = {
esc: ['Esc', 'Escape'],
tab: 'Tab',
enter: 'Enter',
space: [' ', 'Spacebar'],
up: ['Up', 'ArrowUp'],
left: ['Left', 'ArrowLeft'],
right: ['Right', 'ArrowRight'],
down: ['Down', 'ArrowDown'],
delete: ['Backspace', 'Delete', 'Del']
}
const listener = ($event) => {
if (this.disable) {
return
}
const keyName = Object.keys(keyNames).find(key => {
const keyName = $event.key
const value = keyNames[key]
return value === keyName || (Array.isArray(value) && value.includes(keyName))
})
if (keyName) {
// 避免和其他按键事件冲突
setTimeout(() => {
this.$emit(keyName, {})
}, 0)
}
}
document.addEventListener('keyup', listener)
// this.$once('hook:beforeDestroy', () => {
// document.removeEventListener('keyup', listener)
// })
},
render: () => {}
}
// #endif

View File

@ -0,0 +1,26 @@
export default {
data() {
return {
}
},
created(){
this.popup = this.getParent()
},
methods:{
/**
* 获取父元素实例
*/
getParent(name = 'uniPopup') {
let parent = this.$parent;
let parentName = parent.$options.name;
while (parentName !== name) {
parent = parent.$parent;
if (!parent) return false
parentName = parent.$options.name;
}
return parent;
},
}
}

View File

@ -0,0 +1,482 @@
<template>
<view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']">
<view @touchstart="touchstart">
<UTransition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass"
:duration="duration" :show="showTrans" @click="onTap" />
<UTransition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration"
:show="showTrans" @click="onTap">
<view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear">
<slot />
</view>
</UTransition>
</view>
<!-- #ifdef H5 -->
<keypress v-if="maskShow" @esc="onTap" />
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
import UTransition from "../uni-transition/uni-transition.vue";
/**
* PopUp 弹出层
* @description 弹出层组件,为了解决遮罩弹层的问题
* @tutorial https://ext.dcloud.net.cn/plugin?id=329
* @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
* @value top 顶部弹出
* @value center 中间弹出
* @value bottom 底部弹出
* @value left 左侧弹出
* @value right 右侧弹出
* @value message 消息提示
* @value dialog 对话框
* @value share 底部分享示例
* @property {Boolean} animation = [true|false] 是否开启动画
* @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
* @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
* @property {String} backgroundColor 主窗口背景色
* @property {String} maskBackgroundColor 蒙版颜色
* @property {Boolean} safeArea 是否适配底部安全区
* @event {Function} change 打开关闭弹窗触发e={show: false}
* @event {Function} maskClick 点击遮罩触发
*/
export default {
name: 'uniPopup',
components: {
UTransition,
// #ifdef H5
keypress
// #endif
},
emits: ['change', 'maskClick'],
props: {
// 开启动画
animation: {
type: Boolean,
default: true
},
// 弹出层类型可选值top: 顶部弹出层bottom底部弹出层center全屏弹出层
// message: 消息提示 ; dialog : 对话框
type: {
type: String,
default: 'center'
},
// maskClick
isMaskClick: {
type: Boolean,
default: null
},
// TODO 2 个版本后废弃属性 ,使用 isMaskClick
maskClick: {
type: Boolean,
default: null
},
backgroundColor: {
type: String,
default: 'none'
},
safeArea: {
type: Boolean,
default: true
},
maskBackgroundColor: {
type: String,
default: 'rgba(0, 0, 0, 0.4)'
},
},
watch: {
/**
* 监听type类型
*/
type: {
handler: function(type) {
if (!this.config[type]) return
this[this.config[type]](true)
},
immediate: true
},
isDesktop: {
handler: function(newVal) {
if (!this.config[newVal]) return
this[this.config[this.type]](true)
},
immediate: true
},
/**
* 监听遮罩是否可点击
* @param {Object} val
*/
maskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
isMaskClick: {
handler: function(val) {
this.mkclick = val
},
immediate: true
},
// H5 下禁止底部滚动
showPopup(show) {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
// #endif
}
},
data() {
return {
duration: 300,
ani: [],
showPopup: false,
showTrans: false,
popupWidth: 0,
popupHeight: 0,
config: {
top: 'top',
bottom: 'bottom',
center: 'center',
left: 'left',
right: 'right',
message: 'top',
dialog: 'center',
share: 'bottom'
},
maskClass: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)'
},
transClass: {
position: 'fixed',
left: 0,
right: 0,
},
maskShow: true,
mkclick: true,
popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
}
},
computed: {
isDesktop() {
return this.popupWidth >= 500 && this.popupHeight >= 500
},
bg() {
if (this.backgroundColor === '' || this.backgroundColor === 'none') {
return 'transparent'
}
return this.backgroundColor
}
},
mounted() {
const fixSize = () => {
const {
windowWidth,
windowHeight,
windowTop,
safeArea,
screenHeight,
safeAreaInsets
} = uni.getSystemInfoSync()
this.popupWidth = windowWidth
this.popupHeight = windowHeight + (windowTop || 0)
// TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
if (safeArea && this.safeArea) {
// #ifdef MP-WEIXIN
this.safeAreaInsets = screenHeight - safeArea.bottom
// #endif
// #ifndef MP-WEIXIN
this.safeAreaInsets = safeAreaInsets.bottom
// #endif
} else {
this.safeAreaInsets = 0
}
}
fixSize()
// #ifdef H5
// window.addEventListener('resize', fixSize)
// this.$once('hook:beforeDestroy', () => {
// window.removeEventListener('resize', fixSize)
// })
// #endif
},
// #ifndef VUE3
// TODO vue2
destroyed() {
this.setH5Visible()
},
// #endif
// #ifdef VUE3
// TODO vue3
unmounted() {
this.setH5Visible()
},
// #endif
created() {
// this.mkclick = this.isMaskClick || this.maskClick
if (this.isMaskClick === null && this.maskClick === null) {
this.mkclick = true
} else {
this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
}
if (this.animation) {
this.duration = 300
} else {
this.duration = 0
}
// TODO 处理 message 组件生命周期异常的问题
this.messageChild = null
// TODO 解决头条冒泡的问题
this.clearPropagation = false
this.maskClass.backgroundColor = this.maskBackgroundColor
},
methods: {
setH5Visible() {
// #ifdef H5
// fix by mehaotian 处理 h5 滚动穿透的问题
document.getElementsByTagName('body')[0].style.overflow = 'visible'
// #endif
},
/**
* 公用方法,不显示遮罩层
*/
closeMask() {
this.maskShow = false
},
/**
* 公用方法,遮罩层禁止点击
*/
disableMask() {
this.mkclick = false
},
// TODO nvue 取消冒泡
clear(e) {
// #ifndef APP-NVUE
e.stopPropagation()
// #endif
this.clearPropagation = true
},
open(direction) {
// fix by mehaotian 处理快速打开关闭的情况
if (this.showPopup) {
return
}
let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
if (!(direction && innerType.indexOf(direction) !== -1)) {
direction = this.type
}
if (!this.config[direction]) {
console.error('缺少类型:', direction)
return
}
this[this.config[direction]]()
this.$emit('change', {
show: true,
type: direction
})
},
close(type) {
this.showTrans = false
this.$emit('change', {
show: false,
type: this.type
})
clearTimeout(this.timer)
// // 自定义关闭事件
// this.customOpen && this.customClose()
this.timer = setTimeout(() => {
this.showPopup = false
}, 300)
},
// TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
touchstart() {
this.clearPropagation = false
},
onTap() {
if (this.clearPropagation) {
// fix by mehaotian 兼容 nvue
this.clearPropagation = false
return
}
this.$emit('maskClick')
if (!this.mkclick) return
this.close()
},
/**
* 顶部弹出样式处理
*/
top(type) {
this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
this.ani = ['slide-top']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
backgroundColor: this.bg,
borderRadius:'15px 15px 0 0'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
this.$nextTick(() => {
if (this.messageChild && this.type === 'message') {
this.messageChild.timerClose()
}
})
},
/**
* 底部弹出样式处理
*/
bottom(type) {
this.popupstyle = 'bottom'
this.ani = ['slide-bottom']
this.transClass = {
position: 'fixed',
left: 0,
right: 0,
bottom: 0,
paddingBottom: 0 + 'px',
backgroundColor: this.bg,
borderRadius:'15px 15px 0 0'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
/**
* 中间弹出样式处理
*/
center(type) {
this.popupstyle = 'center'
this.ani = ['zoom-out', 'fade']
this.transClass = {
position: 'fixed',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column',
/* #endif */
bottom: 0,
left: 0,
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center',
borderRadius:'15px 15px 0 0'
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
left(type) {
this.popupstyle = 'left'
this.ani = ['slide-left']
this.transClass = {
position: 'fixed',
left: 0,
bottom: 0,
top: 0,
backgroundColor: this.bg,
borderRadius:'15px 15px 0 0',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
},
right(type) {
this.popupstyle = 'right'
this.ani = ['slide-right']
this.transClass = {
position: 'fixed',
bottom: 0,
right: 0,
top: 0,
backgroundColor: this.bg,
borderRadius:'15px 15px 0 0',
/* #ifndef APP-NVUE */
display: 'flex',
flexDirection: 'column'
/* #endif */
}
// TODO 兼容 type 属性 ,后续会废弃
if (type) return
this.showPopup = true
this.showTrans = true
}
}
}
</script>
<style lang="scss">
.uni-popup {
position: fixed;
/* #ifndef APP-NVUE */
z-index: 99;
/* #endif */
&.top,
&.left,
&.right {
/* #ifdef H5 */
top: var(--window-top);
/* #endif */
/* #ifndef H5 */
top: 0;
/* #endif */
}
.uni-popup__wrapper {
border-radius: 15px;
/* #ifndef APP-NVUE */
display: block;
/* #endif */
position: relative;
/* iphonex 等安全区设置,底部安全区适配 */
/* #ifndef APP-NVUE */
// padding-bottom: constant(safe-area-inset-bottom);
// padding-bottom: env(safe-area-inset-bottom);
/* #endif */
&.left,
&.right {
/* #ifdef H5 */
padding-top: var(--window-top);
/* #endif */
/* #ifndef H5 */
padding-top: 0;
/* #endif */
flex: 1;
}
}
}
.fixforpc-z-index {
/* #ifndef APP-NVUE */
z-index: 999;
/* #endif */
}
.fixforpc-top {
top: 0;
}
</style>

View File

@ -0,0 +1,131 @@
// const defaultOption = {
// duration: 300,
// timingFunction: 'linear',
// delay: 0,
// transformOrigin: '50% 50% 0'
// }
// #ifdef APP-NVUE
const nvueAnimation = uni.requireNativePlugin('animation')
// #endif
class MPAnimation {
constructor(options, _this) {
this.options = options
// 在iOS10+QQ小程序平台下传给原生的对象一定是个普通对象而不是Proxy对象否则会报parameter should be Object instead of ProxyObject的错误
this.animation = uni.createAnimation({
...options
})
this.currentStepAnimates = {}
this.next = 0
this.$ = _this
}
_nvuePushAnimates(type, args) {
let aniObj = this.currentStepAnimates[this.next]
let styles = {}
if (!aniObj) {
styles = {
styles: {},
config: {}
}
} else {
styles = aniObj
}
if (animateTypes1.includes(type)) {
if (!styles.styles.transform) {
styles.styles.transform = ''
}
let unit = ''
if(type === 'rotate'){
unit = 'deg'
}
styles.styles.transform += `${type}(${args+unit}) `
} else {
styles.styles[type] = `${args}`
}
this.currentStepAnimates[this.next] = styles
}
_animateRun(styles = {}, config = {}) {
let ref = this.$.$refs['ani'].ref
if (!ref) return
return new Promise((resolve, reject) => {
nvueAnimation.transition(ref, {
styles,
...config
}, res => {
resolve()
})
})
}
_nvueNextAnimate(animates, step = 0, fn) {
let obj = animates[step]
if (obj) {
let {
styles,
config
} = obj
this._animateRun(styles, config).then(() => {
step += 1
this._nvueNextAnimate(animates, step, fn)
})
} else {
this.currentStepAnimates = {}
typeof fn === 'function' && fn()
this.isEnd = true
}
}
step(config = {}) {
// #ifndef APP-NVUE
this.animation.step(config)
// #endif
// #ifdef APP-NVUE
this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config)
this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin
this.next++
// #endif
return this
}
run(fn) {
// #ifndef APP-NVUE
this.$.animationData = this.animation.export()
this.$.timer = setTimeout(() => {
typeof fn === 'function' && fn()
}, this.$.durationTime)
// #endif
// #ifdef APP-NVUE
this.isEnd = false
let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref
if(!ref) return
this._nvueNextAnimate(this.currentStepAnimates, 0, fn)
this.next = 0
// #endif
}
}
const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d',
'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY',
'translateZ'
]
const animateTypes2 = ['opacity', 'backgroundColor']
const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom']
animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => {
MPAnimation.prototype[type] = function(...args) {
// #ifndef APP-NVUE
this.animation[type](...args)
// #endif
// #ifdef APP-NVUE
this._nvuePushAnimates(type, args)
// #endif
return this
}
})
export function createAnimation(option, _this) {
if(!_this) return
clearTimeout(_this.timer)
return new MPAnimation(option, _this)
}

View File

@ -0,0 +1,286 @@
<template>
<!-- #ifndef APP-NVUE -->
<view v-show="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
<!-- #ifdef APP-NVUE -->
<view v-if="isShow" ref="ani" :animation="animationData" :class="customClass" :style="transformStyles" @click="onClick"><slot></slot></view>
<!-- #endif -->
</template>
<script>
import { createAnimation } from './createAnimation'
/**
* Transition 过渡动画
* @description 简单过渡动画组件
* @tutorial https://ext.dcloud.net.cn/plugin?id=985
* @property {Boolean} show = [false|true] 控制组件显示或隐藏
* @property {Array|String} modeClass = [fade|slide-top|slide-right|slide-bottom|slide-left|zoom-in|zoom-out] 过渡动画类型
* @value fade 渐隐渐出过渡
* @value slide-top 由上至下过渡
* @value slide-right 由右至左过渡
* @value slide-bottom 由下至上过渡
* @value slide-left 由左至右过渡
* @value zoom-in 由小到大过渡
* @value zoom-out 由大到小过渡
* @property {Number} duration 过渡动画持续时间
* @property {Object} styles 组件样式,同 css 样式,注意带’-‘连接符的属性需要使用小驼峰写法如:`backgroundColor:red`
*/
export default {
name: 'uniTransition',
emits:['click','change'],
props: {
show: {
type: Boolean,
default: false
},
modeClass: {
type: [Array, String],
default() {
return 'fade'
}
},
duration: {
type: Number,
default: 300
},
styles: {
type: Object,
default() {
return {}
}
},
customClass:{
type: String,
default: ''
},
onceRender:{
type:Boolean,
default:false
},
},
data() {
return {
isShow: false,
transform: '',
opacity: 1,
animationData: {},
durationTime: 300,
config: {}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.open()
} else {
// 避免上来就执行 close,导致动画错乱
if (this.isShow) {
this.close()
}
}
},
immediate: true
}
},
computed: {
// 生成样式数据
stylesObject() {
let styles = {
...this.styles,
'transition-duration': this.duration / 1000 + 's'
}
let transform = ''
for (let i in styles) {
let line = this.toLine(i)
transform += line + ':' + styles[i] + ';'
}
return transform
},
// 初始化动画条件
transformStyles() {
return 'transform:' + this.transform + ';' + 'opacity:' + this.opacity + ';' + this.stylesObject
}
},
created() {
// 动画默认配置
this.config = {
duration: this.duration,
timingFunction: 'ease',
transformOrigin: '50% 50%',
delay: 0
}
this.durationTime = this.duration
},
methods: {
/**
* ref 触发 初始化动画
*/
init(obj = {}) {
if (obj.duration) {
this.durationTime = obj.duration
}
this.animation = createAnimation(Object.assign(this.config, obj),this)
},
/**
* 点击组件触发回调
*/
onClick() {
this.$emit('click', {
detail: this.isShow
})
},
/**
* ref 触发 动画分组
* @param {Object} obj
*/
step(obj, config = {}) {
if (!this.animation) return
for (let i in obj) {
try {
if(typeof obj[i] === 'object'){
this.animation[i](...obj[i])
}else{
this.animation[i](obj[i])
}
} catch (e) {
console.error(`方法 ${i} 不存在`)
}
}
this.animation.step(config)
return this
},
/**
* ref 触发 执行动画
*/
run(fn) {
if (!this.animation) return
this.animation.run(fn)
},
// 开始过度动画
open() {
clearTimeout(this.timer)
this.transform = ''
this.isShow = true
let { opacity, transform } = this.styleInit(false)
if (typeof opacity !== 'undefined') {
this.opacity = opacity
}
this.transform = transform
// 确保动态样式已经生效后,执行动画,如果不加 nextTick ,会导致 wx 动画执行异常
this.$nextTick(() => {
// TODO 定时器保证动画完全执行,目前有些问题,后面会取消定时器
this.timer = setTimeout(() => {
this.animation = createAnimation(this.config, this)
this.tranfromInit(false).step()
this.animation.run()
this.$emit('change', {
detail: this.isShow
})
}, 20)
})
},
// 关闭过度动画
close(type) {
if (!this.animation) return
this.tranfromInit(true)
.step()
.run(() => {
this.isShow = false
this.animationData = null
this.animation = null
let { opacity, transform } = this.styleInit(false)
this.opacity = opacity || 1
this.transform = transform
this.$emit('change', {
detail: this.isShow
})
})
},
// 处理动画开始前的默认样式
styleInit(type) {
let styles = {
transform: ''
}
let buildStyle = (type, mode) => {
if (mode === 'fade') {
styles.opacity = this.animationType(type)[mode]
} else {
styles.transform += this.animationType(type)[mode] + ' '
}
}
if (typeof this.modeClass === 'string') {
buildStyle(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildStyle(type, mode)
})
}
return styles
},
// 处理内置组合动画
tranfromInit(type) {
let buildTranfrom = (type, mode) => {
let aniNum = null
if (mode === 'fade') {
aniNum = type ? 0 : 1
} else {
aniNum = type ? '-100%' : '0'
if (mode === 'zoom-in') {
aniNum = type ? 0.8 : 1
}
if (mode === 'zoom-out') {
aniNum = type ? 1.2 : 1
}
if (mode === 'slide-right') {
aniNum = type ? '100%' : '0'
}
if (mode === 'slide-bottom') {
aniNum = type ? '100%' : '0'
}
}
this.animation[this.animationMode()[mode]](aniNum)
}
if (typeof this.modeClass === 'string') {
buildTranfrom(type, this.modeClass)
} else {
this.modeClass.forEach(mode => {
buildTranfrom(type, mode)
})
}
return this.animation
},
animationType(type) {
return {
fade: type ? 1 : 0,
'slide-top': `translateY(${type ? '0' : '-100%'})`,
'slide-right': `translateX(${type ? '0' : '100%'})`,
'slide-bottom': `translateY(${type ? '0' : '100%'})`,
'slide-left': `translateX(${type ? '0' : '-100%'})`,
'zoom-in': `scaleX(${type ? 1 : 0.8}) scaleY(${type ? 1 : 0.8})`,
'zoom-out': `scaleX(${type ? 1 : 1.2}) scaleY(${type ? 1 : 1.2})`
}
},
// 内置动画类型与实际动画对应字典
animationMode() {
return {
fade: 'opacity',
'slide-top': 'translateY',
'slide-right': 'translateX',
'slide-bottom': 'translateY',
'slide-left': 'translateX',
'zoom-in': 'scale',
'zoom-out': 'scale'
}
},
// 驼峰转中横线
toLine(name) {
return name.replace(/([A-Z])/g, '-$1').toLowerCase()
}
}
}
</script>
<style></style>

View File

@ -14,7 +14,6 @@ import { VUE_APP_UPLOAD_URL } from '@/config';
import { ref } from 'vue';
const props = defineProps(['modelValue'])
console.log("--> % modelValue:\n", props.modelValue)
const emit = defineEmits(['update:modelValue'])
const list = ref(props.modelValue)
@ -38,7 +37,6 @@ const afterRead = async (event) => {
})
for (let i = 0; i < lists.length; i++) {
const result = await uploadFilePromise(lists[i].url)
console.log("gxs --> % afterRead % result:\n", result)
let item = list.value[fileListLen]
list.value.splice(fileListLen, 1, Object.assign(item, {
status: 'success',
@ -60,9 +58,8 @@ const uploadFilePromise = (url) => {
user: 'test'
},
success: (res) => {
console.log("gxs --> % returnnewPromise % res:\n", res)
setTimeout(() => {
resolve(res.data.data)
resolve(JSON.parse(res.data).data)
}, 10)
}
});
@ -71,7 +68,7 @@ const uploadFilePromise = (url) => {
</script>
<style lang="less">
<style lang="scss">
.activity {
&-header {

View File

@ -31,6 +31,14 @@ const buttonText = computed(() => {
});
const startCountdown = () => {
if(!props.mobile){
uni.showToast({
title: '用户名不能为空',
icon: 'none',
duration: 2000,
})
return false
}
if (!countingDown.value) {
countingDown.value = true;
handleSendSmsCode()
@ -42,7 +50,6 @@ const handleSendSmsCode = () => {
uni.showLoading({
title: '发送验证码中'
});
console.log("gxs --> % handleSendSmsCode % props.mobile:\n", props.mobile)
sendSmsCode({
"mobile": props.mobile,
"scene": props.scene
@ -55,7 +62,6 @@ const handleSendSmsCode = () => {
});
timer.value = setInterval(() => {
countdownSeconds.value--;
console.log("gxs --> % timer % countdownSeconds.value:\n", countdownSeconds.value)
if (countdownSeconds.value <= 0) {
clearInterval(timer.value);
countdownSeconds.value = 60; // 倒计时结束后重置为初始值
@ -64,7 +70,6 @@ const handleSendSmsCode = () => {
}, 1000);
}).catch(error => {
countingDown.value = false;
})
}
@ -73,7 +78,7 @@ onUnmounted(() => {
})
</script>
<style lang="less">
<style lang="scss">
.goods {
position: relative;
padding: 30rpx 0;