项目初始化
This commit is contained in:
105
sheep/components/s-activity-pop/s-activity-pop.vue
Normal file
105
sheep/components/s-activity-pop/s-activity-pop.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<!-- 商品信息:满减送等营销活动的弹窗 -->
|
||||
<template>
|
||||
<su-popup :show="show" type="bottom" round="20" @close="emits('close')" showClose>
|
||||
<view class="model-box">
|
||||
<view class="title ss-m-t-16 ss-m-l-20 ss-flex">营销活动</view>
|
||||
<scroll-view
|
||||
class="model-content ss-m-t-50"
|
||||
scroll-y
|
||||
:scroll-with-animation="false"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<view v-for="item in state.activityInfo" :key="item.id">
|
||||
<view class="ss-flex ss-col-top ss-m-b-40" @tap="onGoodsList(item)">
|
||||
<view class="model-content-tag ss-flex ss-row-center">满减</view>
|
||||
<view class="ss-m-l-20 model-content-title ss-flex-1">
|
||||
<view class="ss-m-b-24" v-for="rule in state.activityMap[item.id]?.rules" :key="rule">
|
||||
{{ formatRewardActivityRule(state.activityMap[item.id], rule) }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="cicon-forward" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import RewardActivityApi from '@/sheep/api/promotion/rewardActivity';
|
||||
import { formatRewardActivityRule } from '@/sheep/hooks/useGoods';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['close']);
|
||||
const state = reactive({
|
||||
activityInfo: computed(() => props.modelValue),
|
||||
activityMap: {}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
// 展示的情况下,加载每个活动的详细信息
|
||||
if (props.show) {
|
||||
state.activityInfo?.forEach(activity => {
|
||||
RewardActivityApi.getRewardActivity(activity.id).then(res => {
|
||||
if (res.code !== 0) {
|
||||
return;
|
||||
}
|
||||
state.activityMap[activity.id] = res.data;
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function onGoodsList(e) {
|
||||
sheep.$router.go('/pages/activity/index', {
|
||||
activityId: e.id,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.model-box {
|
||||
height: 60vh;
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
height: 80rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
.model-content {
|
||||
padding: 0 20rpx;
|
||||
box-sizing: border-box;
|
||||
.model-content-tag {
|
||||
background: rgba(#ff6911, 0.1);
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ff6911;
|
||||
line-height: 42rpx;
|
||||
width: 68rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
.model-content-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
.cicon-forward {
|
||||
font-size: 28rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
110
sheep/components/s-address-item/s-address-item.vue
Normal file
110
sheep/components/s-address-item/s-address-item.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<!-- 地址卡片 -->
|
||||
<template>
|
||||
<view
|
||||
class="address-item ss-flex ss-row-between ss-col-center"
|
||||
:class="[{ 'border-bottom': props.hasBorderBottom }]"
|
||||
>
|
||||
<view class="item-left" v-if="!isEmpty(props.item)">
|
||||
<view class="area-text ss-flex ss-col-center">
|
||||
<uni-tag
|
||||
class="ss-m-r-10"
|
||||
size="small"
|
||||
custom-style="background-color: var(--ui-BG-Main); border-color: var(--ui-BG-Main); color: #fff;"
|
||||
v-if="props.item.defaultStatus"
|
||||
text="默认"
|
||||
/>
|
||||
{{ props.item.areaName }}
|
||||
</view>
|
||||
<view class="address-text">
|
||||
{{ props.item.detailAddress }}
|
||||
</view>
|
||||
<view class="person-text"> {{ props.item.name }} {{ props.item.mobile }} </view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="address-text ss-m-b-10">请选择收货地址</view>
|
||||
</view>
|
||||
<slot>
|
||||
<button class="ss-reset-button edit-btn" @tap.stop="onEdit">
|
||||
<view class="edit-icon ss-flex ss-row-center ss-col-center">
|
||||
<image :src="sheep.$url.static('/static/img/shop/user/address/edit.png')" />
|
||||
</view>
|
||||
</button>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 基础组件 - 地址卡片
|
||||
*
|
||||
* @param {String} icon = _icon-edit - icon
|
||||
*
|
||||
* @event {Function()} click - 点击
|
||||
* @event {Function()} actionClick - 点击工具栏
|
||||
*
|
||||
* @slot - 默认插槽
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
hasBorderBottom: {
|
||||
type: Boolean,
|
||||
defult: true,
|
||||
},
|
||||
});
|
||||
|
||||
const onEdit = () => {
|
||||
sheep.$router.go('/pages/user/address/edit', {
|
||||
id: props.item.id,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.address-item {
|
||||
padding: 24rpx 30rpx;
|
||||
|
||||
.item-left {
|
||||
width: 600rpx;
|
||||
}
|
||||
|
||||
.area-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
color: $dark-9;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
line-height: 48rpx;
|
||||
}
|
||||
|
||||
.person-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
color: $dark-9;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
background: $gray-f;
|
||||
border-radius: 50%;
|
||||
|
||||
.edit-icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
}
|
||||
}
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
107
sheep/components/s-auth-modal/components/account-login.vue
Normal file
107
sheep/components/s-auth-modal/components/account-login.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<!-- 账号密码登录 accountLogin -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60 ss-flex-col">
|
||||
<view class="ss-flex ss-m-b-20">
|
||||
<view class="head-title-active head-title-line" @tap="showAuthModal('smsLogin')">
|
||||
短信登录
|
||||
</view>
|
||||
<view class="head-title ss-m-r-40 head-title-animation">账号登录</view>
|
||||
</view>
|
||||
<view class="head-subtitle">如果未设置过密码,请点击忘记密码</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="accountLoginRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<uni-forms-item name="mobile" label="账号">
|
||||
<uni-easyinput placeholder="请输入账号" v-model="state.model.mobile" :inputBorder="false">
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button forgot-btn" @tap="showAuthModal('resetPassword')">
|
||||
忘记密码
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="password" label="密码">
|
||||
<uni-easyinput
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
v-model="state.model.password"
|
||||
:inputBorder="false"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button login-btn-start" @tap="accountLoginSubmit">登录</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { mobile, password } from '@/sheep/validate/form';
|
||||
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
|
||||
import AuthUtil from '@/sheep/api/member/auth';
|
||||
|
||||
const accountLoginRef = ref(null);
|
||||
|
||||
const emits = defineEmits(['onConfirm']);
|
||||
|
||||
const props = defineProps({
|
||||
agreeStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
model: {
|
||||
mobile: '', // 账号
|
||||
password: '', // 密码
|
||||
},
|
||||
rules: {
|
||||
mobile,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
// 账号登录
|
||||
async function accountLoginSubmit() {
|
||||
// 表单验证
|
||||
const validate = await unref(accountLoginRef)
|
||||
.validate()
|
||||
.catch((error) => {
|
||||
console.log('error: ', error);
|
||||
});
|
||||
if (!validate) return;
|
||||
|
||||
// 同意协议
|
||||
if (!props.agreeStatus) {
|
||||
emits('onConfirm', true)
|
||||
sheep.$helper.toast('请勾选同意');
|
||||
return;
|
||||
}
|
||||
|
||||
// 提交数据
|
||||
const { code, data } = await AuthUtil.login(state.model);
|
||||
if (code === 0) {
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
</style>
|
127
sheep/components/s-auth-modal/components/change-mobile.vue
Normal file
127
sheep/components/s-auth-modal/components/change-mobile.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<!-- 绑定/更换手机号 changeMobile -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60">
|
||||
<view class="head-title ss-m-b-20">
|
||||
{{ userInfo.mobile ? '更换手机号' : '绑定手机号' }}
|
||||
</view>
|
||||
<view class="head-subtitle">为了您的账号安全,请使用本人手机号码</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="changeMobileRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<uni-forms-item name="mobile" label="手机号">
|
||||
<uni-easyinput
|
||||
placeholder="请输入手机号"
|
||||
v-model="state.model.mobile"
|
||||
:inputBorder="false"
|
||||
type="number"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button
|
||||
class="ss-reset-button code-btn-start"
|
||||
:disabled="state.isMobileEnd"
|
||||
:class="{ 'code-btn-end': state.isMobileEnd }"
|
||||
@tap="getSmsCode('changeMobile', state.model.mobile)"
|
||||
>
|
||||
{{ getSmsTimer('changeMobile') }}
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="code" label="验证码">
|
||||
<uni-easyinput
|
||||
placeholder="请输入验证码"
|
||||
v-model="state.model.code"
|
||||
:inputBorder="false"
|
||||
type="number"
|
||||
maxlength="4"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button login-btn-start" @tap="changeMobileSubmit">
|
||||
确认
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
|
||||
<!-- 微信独有:读取手机号 -->
|
||||
<button
|
||||
v-if="'WechatMiniProgram' === sheep.$platform.name"
|
||||
class="ss-reset-button type-btn"
|
||||
open-type="getPhoneNumber"
|
||||
@getphonenumber="getPhoneNumber"
|
||||
>
|
||||
使用微信手机号
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, reactive, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { code, mobile } from '@/sheep/validate/form';
|
||||
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const changeMobileRef = ref(null);
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
isMobileEnd: false, // 手机号输入完毕
|
||||
model: {
|
||||
mobile: '', // 手机号
|
||||
code: '', // 验证码
|
||||
},
|
||||
rules: {
|
||||
code,
|
||||
mobile,
|
||||
},
|
||||
});
|
||||
|
||||
// 绑定手机号
|
||||
async function changeMobileSubmit() {
|
||||
const validate = await unref(changeMobileRef)
|
||||
.validate()
|
||||
.catch((error) => {
|
||||
console.log('error: ', error);
|
||||
});
|
||||
if (!validate) {
|
||||
return;
|
||||
}
|
||||
// 提交更新请求
|
||||
const { code } = await UserApi.updateUserMobile(state.model);
|
||||
if (code !== 0) {
|
||||
return;
|
||||
}
|
||||
sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
}
|
||||
|
||||
// 使用微信手机号
|
||||
async function getPhoneNumber(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
return;
|
||||
}
|
||||
const result = await sheep.$platform.useProvider().bindUserPhoneNumber(e.detail);
|
||||
if (result) {
|
||||
sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
</style>
|
106
sheep/components/s-auth-modal/components/change-password.vue
Normal file
106
sheep/components/s-auth-modal/components/change-password.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<!-- 修改密码(登录时) -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60">
|
||||
<view class="head-title ss-m-b-20">修改密码</view>
|
||||
<view class="head-subtitle">如密码丢失或未设置,请点击忘记密码重新设置</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="changePasswordRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<uni-forms-item name="code" label="验证码">
|
||||
<uni-easyinput
|
||||
placeholder="请输入验证码"
|
||||
v-model="state.model.code"
|
||||
type="number"
|
||||
maxlength="4"
|
||||
:inputBorder="false"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button
|
||||
class="ss-reset-button code-btn code-btn-start"
|
||||
:disabled="state.isMobileEnd"
|
||||
:class="{ 'code-btn-end': state.isMobileEnd }"
|
||||
@tap="getSmsCode('changePassword')"
|
||||
>
|
||||
{{ getSmsTimer('resetPassword') }}
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="reNewPassword" label="密码">
|
||||
<uni-easyinput
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
v-model="state.model.password"
|
||||
:inputBorder="false"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button login-btn-start" @tap="changePasswordSubmit">
|
||||
确认
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
|
||||
<button class="ss-reset-button type-btn" @tap="closeAuthModal">
|
||||
取消修改
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, unref } from 'vue';
|
||||
import { code, password } from '@/sheep/validate/form';
|
||||
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const changePasswordRef = ref(null);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
model: {
|
||||
mobile: '', // 手机号
|
||||
code: '', // 验证码
|
||||
password: '', // 密码
|
||||
},
|
||||
rules: {
|
||||
code,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
// 更改密码
|
||||
async function changePasswordSubmit() {
|
||||
// 参数校验
|
||||
const validate = await unref(changePasswordRef)
|
||||
.validate()
|
||||
.catch((error) => {
|
||||
console.log('error: ', error);
|
||||
});
|
||||
if (!validate) {
|
||||
return;
|
||||
}
|
||||
// 发起请求
|
||||
const { code } = await UserApi.updateUserPassword(state.model);
|
||||
if (code !== 0) {
|
||||
return;
|
||||
}
|
||||
// 成功后,只需要关闭弹窗
|
||||
closeAuthModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
</style>
|
170
sheep/components/s-auth-modal/components/chat-authorization.vue
Normal file
170
sheep/components/s-auth-modal/components/chat-authorization.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<!-- 微信授权信息 mpAuthorization -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60 ss-flex-col">
|
||||
<view class="ss-flex ss-m-b-20">
|
||||
<view class="head-title ss-m-r-40 head-title-animation">完善资料</view>
|
||||
</view>
|
||||
<view class="head-subtitle">完善您的交友名片、个人相册</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="accountLoginRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<!-- 获取头像昵称:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
|
||||
<uni-forms-item name="qrcode" label="微信名片">
|
||||
<button
|
||||
class="ss-reset-button avatar-btn"
|
||||
@click="onChooseAvatar"
|
||||
>
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="sheep.$url.cdn(state.model.qrcode)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="cicon-forward" />
|
||||
</button>
|
||||
</uni-forms-item>
|
||||
<form-qrcode v-model="state.model.qrcodeShow"></form-qrcode>
|
||||
<form-image :number="5" v-model="state.imgList"></form-image>
|
||||
<view class="foot-box">
|
||||
<button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认保存 </button>
|
||||
</view>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { closeAuthModal } from '@/sheep/hooks/useModal';
|
||||
import formQrcode from '@/sheep/components/s-auth-modal/components/formQrcode.vue';
|
||||
import formImage from '@/sheep/components/s-auth-modal/components/formImage.vue';
|
||||
import FileApi from '@/sheep/api/infra/file';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const props = defineProps({
|
||||
agreeStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
|
||||
const accountLoginRef = ref(null);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
model: {
|
||||
nickname: userInfo.value.nickname,
|
||||
avatar: userInfo.value.avatar,
|
||||
sex: userInfo.value.sex,
|
||||
age: userInfo.value.age,
|
||||
qrcode: userInfo.value.qrcode,
|
||||
photo: userInfo.value.photo,
|
||||
qrcodeShow: userInfo.value.qrcodeShow,
|
||||
},
|
||||
imgList: userInfo.value.photo ? userInfo.value.photo.split(',') : [],
|
||||
rules: {},
|
||||
disabledStyle: {
|
||||
color: '#999',
|
||||
disableColor: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
function onChooseAvatar() {
|
||||
uni.chooseImage({
|
||||
count: 1, //默认9
|
||||
sourceType: ['album', 'camera'],
|
||||
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
for (let i = 0; i < res.tempFilePaths.length; i++) {
|
||||
uni.getImageInfo({
|
||||
src: res.tempFilePaths[i],
|
||||
success: (image) => {
|
||||
uploadAvatar(image.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 选择头像(来自文件系统)
|
||||
async function uploadAvatar(tempUrl) {
|
||||
if (!tempUrl) {
|
||||
return;
|
||||
}
|
||||
let { data } = await FileApi.uploadFile(tempUrl);
|
||||
state.model.qrcode = data;
|
||||
}
|
||||
|
||||
// 确认授权
|
||||
async function onConfirm() {
|
||||
const { model, imgList } = state;
|
||||
const { qrcode } = model;
|
||||
if (!qrcode) {
|
||||
sheep.$helper.toast('请上传微信名片');
|
||||
return;
|
||||
}
|
||||
if (imgList.length < 1) {
|
||||
sheep.$helper.toast('请上传相册');
|
||||
return;
|
||||
}
|
||||
|
||||
// 发起更新
|
||||
const { code } = await UserApi.updateUser({
|
||||
photo: imgList.join(','),
|
||||
qrcode: state.model.qrcode,
|
||||
qrcodeShow: state.model.qrcodeShow,
|
||||
});
|
||||
// 更新成功
|
||||
if (code === 0) {
|
||||
sheep.$helper.toast('保存成功');
|
||||
await sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
|
||||
.foot-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.authorization-btn {
|
||||
width: 686rpx;
|
||||
height: 80rpx;
|
||||
background-color: var(--ui-BG-Main);
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
}
|
||||
.cicon-forward {
|
||||
font-size: 30rpx;
|
||||
color: #595959;
|
||||
}
|
||||
.avatar-btn {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
|
||||
}
|
||||
</style>
|
73
sheep/components/s-auth-modal/components/formImage.vue
Normal file
73
sheep/components/s-auth-modal/components/formImage.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<view>
|
||||
<view class="form-item">
|
||||
<view class="label">上传相册</view>
|
||||
<view>{{imgList.length}}/{{number}}</view>
|
||||
</view>
|
||||
<view class="upload-box">
|
||||
<shmily-drag-image :number="number" :cols="5" v-model="imgList"></shmily-drag-image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
number: {
|
||||
type: Number,
|
||||
default: 6
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imgList: [],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.imgList = this.modelValue;
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
watch: {
|
||||
imgList: {
|
||||
handler: function(newVal, oldVal) {
|
||||
this.$emit('update:modelValue', newVal);
|
||||
}
|
||||
},
|
||||
modelValue: {
|
||||
handler: function(newVal, oldVal) {
|
||||
this.imgList = newVal;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
min-width: 200rpx;
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
padding: 15px 0;
|
||||
}
|
||||
</style>
|
88
sheep/components/s-auth-modal/components/formQrcode.vue
Normal file
88
sheep/components/s-auth-modal/components/formQrcode.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<view class="form-item">
|
||||
<view class="label">交换名片</view>
|
||||
<view class="radio-box">
|
||||
<view @click="change(item)" class="text" :class="item.value == valueDom ? 'active' : ''" v-for="(item,index) in list">{{item.name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
modelValue: '',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
name: '打开',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: '关闭',
|
||||
value: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
computed: {
|
||||
valueDom() {
|
||||
return this.modelValue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
change(e) {
|
||||
this.$emit('update:modelValue', e.value);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 2px;
|
||||
padding-bottom: 22px;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
width: 100rpx;
|
||||
height: 50rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #ececec;
|
||||
color: #949494;
|
||||
border-radius: 10rpx;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #fff;
|
||||
background-color: #949494;
|
||||
}
|
||||
}
|
||||
</style>
|
88
sheep/components/s-auth-modal/components/formSex.vue
Normal file
88
sheep/components/s-auth-modal/components/formSex.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<view class="form-item">
|
||||
<view class="label">性别</view>
|
||||
<view class="radio-box">
|
||||
<view @click="change(item)" class="text" :class="item.value == valueDom ? 'active' : ''" v-for="(item,index) in list">{{item.name}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
|
||||
},
|
||||
props: {
|
||||
modelValue: '',
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
list: [
|
||||
{
|
||||
name: '男',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
name: '女',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
computed: {
|
||||
valueDom() {
|
||||
return this.modelValue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
change(e) {
|
||||
this.$emit('update:modelValue', e.value);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item {
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 17px;
|
||||
padding-bottom: 22px;
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
min-width: 200rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.text {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #ececec;
|
||||
color: #949494;
|
||||
border-radius: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: #fff;
|
||||
background-color: #949494;
|
||||
}
|
||||
}
|
||||
</style>
|
194
sheep/components/s-auth-modal/components/h5-authorization.vue
Normal file
194
sheep/components/s-auth-modal/components/h5-authorization.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<!-- 微信授权信息 mpAuthorization -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60 ss-flex-col">
|
||||
<view class="ss-flex ss-m-b-20">
|
||||
<view class="head-title ss-m-r-40 head-title-animation">授权信息</view>
|
||||
</view>
|
||||
<view class="head-subtitle">完善您的头像、昵称、性别、年龄</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="accountLoginRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<!-- 获取头像昵称:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
|
||||
<uni-forms-item name="avatar" label="头像">
|
||||
<button
|
||||
class="ss-reset-button avatar-btn"
|
||||
@click="onChooseAvatar"
|
||||
>
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="sheep.$url.cdn(state.model.avatar)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text class="cicon-forward" />
|
||||
</button>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="nickname" label="昵称">
|
||||
<uni-easyinput
|
||||
type="text"
|
||||
placeholder="请输入昵称"
|
||||
v-model="state.model.nickname"
|
||||
:inputBorder="false"
|
||||
/>
|
||||
</uni-forms-item>
|
||||
<form-sex v-model="state.model.sex"></form-sex>
|
||||
<uni-forms-item name="age" label="年龄">
|
||||
<uni-easyinput
|
||||
type="number"
|
||||
placeholder="请输入年龄"
|
||||
v-model="state.model.age"
|
||||
:inputBorder="false"
|
||||
/>
|
||||
</uni-forms-item>
|
||||
<view class="foot-box">
|
||||
<button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认授权 </button>
|
||||
</view>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import $store from '@/sheep/store';
|
||||
import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
|
||||
import formSex from '@/sheep/components/s-auth-modal/components/formSex.vue';
|
||||
import FileApi from '@/sheep/api/infra/file';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const props = defineProps({
|
||||
agreeStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
|
||||
const isPass = computed(() => sheep.$store('user').tradeConfig.weixinEnabled);
|
||||
|
||||
const friendEnabled = computed(() => sheep.$store('user').tradeConfig.friendEnabled);
|
||||
|
||||
const accountLoginRef = ref(null);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
model: {
|
||||
nickname: userInfo.value.nickname,
|
||||
avatar: userInfo.value.avatar,
|
||||
sex: userInfo.value.sex,
|
||||
age: userInfo.value.age,
|
||||
},
|
||||
rules: {},
|
||||
disabledStyle: {
|
||||
color: '#999',
|
||||
disableColor: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
function onChooseAvatar() {
|
||||
uni.chooseImage({
|
||||
count: 1, //默认9
|
||||
sourceType: ['album', 'camera'],
|
||||
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
||||
success: (res) => {
|
||||
for (let i = 0; i < res.tempFilePaths.length; i++) {
|
||||
uni.getImageInfo({
|
||||
src: res.tempFilePaths[i],
|
||||
success: (image) => {
|
||||
uploadAvatar(image.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 选择头像(来自文件系统)
|
||||
async function uploadAvatar(tempUrl) {
|
||||
if (!tempUrl) {
|
||||
return;
|
||||
}
|
||||
let { data } = await FileApi.uploadFile(tempUrl);
|
||||
state.model.avatar = data;
|
||||
}
|
||||
|
||||
// 确认授权
|
||||
async function onConfirm() {
|
||||
const { model } = state;
|
||||
const { nickname, avatar, sex, age } = model;
|
||||
if (!avatar) {
|
||||
sheep.$helper.toast('请选择头像');
|
||||
return;
|
||||
}
|
||||
if (!nickname) {
|
||||
sheep.$helper.toast('请输入昵称');
|
||||
return;
|
||||
}
|
||||
if (!sex) {
|
||||
sheep.$helper.toast('请选择性别');
|
||||
return;
|
||||
}
|
||||
if (!age || age < 18 || age > 100) {
|
||||
sheep.$helper.toast('请输入正确的年龄');
|
||||
return;
|
||||
}
|
||||
if (age < 18) {
|
||||
sheep.$helper.toast('未成年禁止注册');
|
||||
return;
|
||||
}
|
||||
// 发起更新
|
||||
const { code } = await UserApi.updateUser({
|
||||
avatar: state.model.avatar,
|
||||
nickname: state.model.nickname,
|
||||
sex: state.model.sex,
|
||||
age: state.model.age,
|
||||
});
|
||||
// 更新成功
|
||||
if (code === 0) {
|
||||
sheep.$helper.toast('授权成功');
|
||||
await sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
|
||||
.foot-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.authorization-btn {
|
||||
width: 686rpx;
|
||||
height: 80rpx;
|
||||
background-color: var(--ui-BG-Main);
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
.cicon-forward {
|
||||
font-size: 30rpx;
|
||||
color: #595959;
|
||||
}
|
||||
.avatar-btn {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
203
sheep/components/s-auth-modal/components/mp-authorization.vue
Normal file
203
sheep/components/s-auth-modal/components/mp-authorization.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<!-- 微信授权信息 mpAuthorization -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60 ss-flex-col">
|
||||
<view class="ss-flex ss-m-b-20">
|
||||
<view class="head-title ss-m-r-40 head-title-animation">授权信息</view>
|
||||
</view>
|
||||
<view class="head-subtitle">完善您的头像、昵称、手机号</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="accountLoginRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<!-- 获取头像昵称:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
|
||||
<uni-forms-item name="avatar" label="头像">
|
||||
<button
|
||||
class="ss-reset-button avatar-btn"
|
||||
open-type="chooseAvatar"
|
||||
@chooseavatar="onChooseAvatar"
|
||||
>
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="sheep.$url.cdn(state.model.avatar)"
|
||||
mode="aspectFill"
|
||||
@tap="sheep.$router.go('/pages/user/info')"
|
||||
/>
|
||||
<text class="cicon-forward" />
|
||||
</button>
|
||||
</uni-forms-item>
|
||||
<uni-forms-item name="nickname" label="昵称">
|
||||
<uni-easyinput
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
v-model="state.model.nickname"
|
||||
:inputBorder="false"
|
||||
/>
|
||||
</uni-forms-item>
|
||||
<form-sex v-model="state.model.sex"></form-sex>
|
||||
<uni-forms-item name="age" label="年龄">
|
||||
<uni-easyinput
|
||||
type="number"
|
||||
placeholder="请输入年龄"
|
||||
v-model="state.model.age"
|
||||
:inputBorder="false"
|
||||
/>
|
||||
</uni-forms-item>
|
||||
<view class="foot-box">
|
||||
<button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认授权 </button>
|
||||
</view>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
|
||||
import { WxaSubscribeTemplate } from '@/sheep/util/const';
|
||||
import formSex from '@/sheep/components/s-auth-modal/components/formSex.vue';
|
||||
import FileApi from '@/sheep/api/infra/file';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const props = defineProps({
|
||||
agreeStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
|
||||
const isPass = computed(() => sheep.$store('user').tradeConfig.weixinEnabled);
|
||||
|
||||
const friendEnabled = computed(() => sheep.$store('user').tradeConfig.friendEnabled);
|
||||
|
||||
const accountLoginRef = ref(null);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
model: {
|
||||
nickname: userInfo.value.nickname,
|
||||
avatar: userInfo.value.avatar,
|
||||
sex: userInfo.value.sex,
|
||||
age: userInfo.value.age,
|
||||
},
|
||||
rules: {},
|
||||
disabledStyle: {
|
||||
color: '#999',
|
||||
disableColor: '#fff',
|
||||
},
|
||||
});
|
||||
|
||||
// 选择头像(来自微信)
|
||||
function onChooseAvatar(e) {
|
||||
const tempUrl = e.detail.avatarUrl || '';
|
||||
uploadAvatar(tempUrl);
|
||||
}
|
||||
|
||||
// 选择头像(来自文件系统)
|
||||
async function uploadAvatar(tempUrl) {
|
||||
if (!tempUrl) {
|
||||
return;
|
||||
}
|
||||
let { data } = await FileApi.uploadFile(tempUrl);
|
||||
state.model.avatar = data;
|
||||
}
|
||||
|
||||
// #ifdef MP
|
||||
function subscribeMessage() {
|
||||
const event = [WxaSubscribeTemplate.UNREAD_MESSAGE];
|
||||
sheep.$platform.useProvider('wechat').subscribeMessage(event, () => {
|
||||
// 订阅后记录一下订阅状态
|
||||
uni.removeStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE);
|
||||
uni.setStorageSync(WxaSubscribeTemplate.UNREAD_MESSAGE, '已订阅');
|
||||
});
|
||||
}
|
||||
|
||||
async function autoSubscribeMessage() {
|
||||
// 2. 订阅消息
|
||||
subscribeMessage();
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 确认授权
|
||||
async function onConfirm() {
|
||||
// #ifdef MP
|
||||
autoSubscribeMessage();
|
||||
// #endif
|
||||
|
||||
const { model } = state;
|
||||
const { nickname, avatar, sex, age } = model;
|
||||
if (!avatar) {
|
||||
sheep.$helper.toast('请选择头像');
|
||||
return;
|
||||
}
|
||||
if (!nickname) {
|
||||
sheep.$helper.toast('请输入昵称');
|
||||
return;
|
||||
}
|
||||
if (!sex) {
|
||||
sheep.$helper.toast('请选择性别');
|
||||
return;
|
||||
}
|
||||
if (!age || age < 18 || age > 100) {
|
||||
sheep.$helper.toast('请输入正确的年龄');
|
||||
return;
|
||||
}
|
||||
if (age < 18) {
|
||||
sheep.$helper.toast('未成年禁止注册');
|
||||
return;
|
||||
}
|
||||
// 发起更新
|
||||
const { code } = await UserApi.updateUser({
|
||||
avatar: state.model.avatar,
|
||||
nickname: state.model.nickname,
|
||||
sex: state.model.sex,
|
||||
age: state.model.age,
|
||||
});
|
||||
// 更新成功
|
||||
if (code === 0) {
|
||||
sheep.$helper.toast('授权成功');
|
||||
await sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
|
||||
.foot-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.authorization-btn {
|
||||
width: 686rpx;
|
||||
height: 80rpx;
|
||||
background-color: var(--ui-BG-Main);
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
}
|
||||
.cicon-forward {
|
||||
font-size: 30rpx;
|
||||
color: #595959;
|
||||
}
|
||||
.avatar-btn {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
119
sheep/components/s-auth-modal/components/reset-password.vue
Normal file
119
sheep/components/s-auth-modal/components/reset-password.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- 重置密码(未登录时) -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60">
|
||||
<view class="head-title ss-m-b-20">重置密码</view>
|
||||
<view class="head-subtitle">为了您的账号安全,设置密码前请先进行安全验证</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="resetPasswordRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<uni-forms-item name="mobile" label="手机号">
|
||||
<uni-easyinput
|
||||
placeholder="请输入手机号"
|
||||
v-model="state.model.mobile"
|
||||
type="number"
|
||||
:inputBorder="false"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button
|
||||
class="ss-reset-button code-btn code-btn-start"
|
||||
:disabled="state.isMobileEnd"
|
||||
:class="{ 'code-btn-end': state.isMobileEnd }"
|
||||
@tap="getSmsCode('resetPassword', state.model.mobile)"
|
||||
>
|
||||
{{ getSmsTimer('resetPassword') }}
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="code" label="验证码">
|
||||
<uni-easyinput
|
||||
placeholder="请输入验证码"
|
||||
v-model="state.model.code"
|
||||
type="number"
|
||||
maxlength="4"
|
||||
:inputBorder="false"
|
||||
/>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="password" label="密码">
|
||||
<uni-easyinput
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
v-model="state.model.password"
|
||||
:inputBorder="false"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button login-btn-start" @tap="resetPasswordSubmit">
|
||||
确认
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
|
||||
<button v-if="!isLogin" class="ss-reset-button type-btn" @tap="showAuthModal('accountLogin')">
|
||||
返回登录
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, reactive, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { code, mobile, password } from '@/sheep/validate/form';
|
||||
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
|
||||
import UserApi from '@/sheep/api/member/user';
|
||||
|
||||
const resetPasswordRef = ref(null);
|
||||
const isLogin = computed(() => sheep.$store('user').isLogin);
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
isMobileEnd: false, // 手机号输入完毕
|
||||
model: {
|
||||
mobile: '', // 手机号
|
||||
code: '', // 验证码
|
||||
password: '', // 密码
|
||||
},
|
||||
rules: {
|
||||
code,
|
||||
mobile,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
// 重置密码
|
||||
const resetPasswordSubmit = async () => {
|
||||
// 参数校验
|
||||
const validate = await unref(resetPasswordRef)
|
||||
.validate()
|
||||
.catch((error) => {
|
||||
console.log('error: ', error);
|
||||
});
|
||||
if (!validate) {
|
||||
return;
|
||||
}
|
||||
// 发起请求
|
||||
const { code } = await UserApi.resetUserPassword(state.model);
|
||||
if (code !== 0) {
|
||||
return;
|
||||
}
|
||||
// 成功后,用户重新登录
|
||||
showAuthModal('accountLogin')
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
</style>
|
119
sheep/components/s-auth-modal/components/sms-login.vue
Normal file
119
sheep/components/s-auth-modal/components/sms-login.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- 短信登录 - smsLogin -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 标题栏 -->
|
||||
<view class="head-box ss-m-b-60">
|
||||
<view class="ss-flex ss-m-b-20">
|
||||
<view class="head-title head-title-line head-title-animation">短信登录</view>
|
||||
<view class="head-title-active ss-m-r-40" @tap="showAuthModal('accountLogin')">
|
||||
账号登录
|
||||
</view>
|
||||
</view>
|
||||
<view class="head-subtitle">未注册的手机号,验证后自动注册账号</view>
|
||||
</view>
|
||||
|
||||
<!-- 表单项 -->
|
||||
<uni-forms
|
||||
ref="smsLoginRef"
|
||||
v-model="state.model"
|
||||
:rules="state.rules"
|
||||
validateTrigger="bind"
|
||||
labelWidth="140"
|
||||
labelAlign="center"
|
||||
>
|
||||
<uni-forms-item name="mobile" label="手机号">
|
||||
<uni-easyinput
|
||||
placeholder="请输入手机号"
|
||||
v-model="state.model.mobile"
|
||||
:inputBorder="false"
|
||||
type="number"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button
|
||||
class="ss-reset-button code-btn code-btn-start"
|
||||
:disabled="state.isMobileEnd"
|
||||
:class="{ 'code-btn-end': state.isMobileEnd }"
|
||||
@tap="getSmsCode('smsLogin', state.model.mobile)"
|
||||
>
|
||||
{{ getSmsTimer('smsLogin') }}
|
||||
</button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
|
||||
<uni-forms-item name="code" label="验证码">
|
||||
<uni-easyinput
|
||||
placeholder="请输入验证码"
|
||||
v-model="state.model.code"
|
||||
:inputBorder="false"
|
||||
type="number"
|
||||
maxlength="4"
|
||||
>
|
||||
<template v-slot:right>
|
||||
<button class="ss-reset-button login-btn-start" @tap="smsLoginSubmit"> 登录 </button>
|
||||
</template>
|
||||
</uni-easyinput>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { code, mobile } from '@/sheep/validate/form';
|
||||
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
|
||||
import AuthUtil from '@/sheep/api/member/auth';
|
||||
|
||||
const smsLoginRef = ref(null);
|
||||
|
||||
const emits = defineEmits(['onConfirm']);
|
||||
|
||||
const props = defineProps({
|
||||
agreeStatus: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
isMobileEnd: false, // 手机号输入完毕
|
||||
codeText: '获取验证码',
|
||||
model: {
|
||||
mobile: '', // 手机号
|
||||
code: '', // 验证码
|
||||
},
|
||||
rules: {
|
||||
code,
|
||||
mobile,
|
||||
},
|
||||
});
|
||||
|
||||
// 短信登录
|
||||
async function smsLoginSubmit() {
|
||||
// 参数校验
|
||||
const validate = await unref(smsLoginRef)
|
||||
.validate()
|
||||
.catch((error) => {
|
||||
console.log('error: ', error);
|
||||
});
|
||||
if (!validate) {
|
||||
return;
|
||||
}
|
||||
if (!props.agreeStatus) {
|
||||
emits('onConfirm', true)
|
||||
sheep.$helper.toast('请勾选同意');
|
||||
return;
|
||||
}
|
||||
// 提交数据
|
||||
const { code } = await AuthUtil.smsLogin(state.model);
|
||||
if (code === 0) {
|
||||
closeAuthModal();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../index.scss';
|
||||
</style>
|
151
sheep/components/s-auth-modal/index.scss
Normal file
151
sheep/components/s-auth-modal/index.scss
Normal file
@@ -0,0 +1,151 @@
|
||||
@keyframes title-animation {
|
||||
0% {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
100% {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
padding: 50rpx 34rpx;
|
||||
min-height: 500rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
}
|
||||
|
||||
.head-box {
|
||||
.head-title {
|
||||
min-width: 160rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
.head-title-active {
|
||||
width: 160rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #999;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
.head-title-animation {
|
||||
animation-name: title-animation;
|
||||
animation-duration: 0.1s;
|
||||
animation-timing-function: ease-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
.head-title-line {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
width: 1rpx;
|
||||
height: 34rpx;
|
||||
background-color: #e4e7ed;
|
||||
position: absolute;
|
||||
left: -30rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
.head-subtitle {
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
color: #afb6c0;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
// .code-btn[disabled] {
|
||||
// background-color: #fff;
|
||||
// }
|
||||
.code-btn-start {
|
||||
width: 160rpx;
|
||||
height: 56rpx;
|
||||
line-height: normal;
|
||||
border: 2rpx solid var(--ui-BG-Main);
|
||||
border-radius: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
color: var(--ui-BG-Main);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.forgot-btn {
|
||||
width: 160rpx;
|
||||
line-height: 56rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.login-btn-start {
|
||||
width: 158rpx;
|
||||
height: 56rpx;
|
||||
line-height: normal;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
border-radius: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
padding: 20rpx;
|
||||
margin: 40rpx auto;
|
||||
width: 200rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.auto-login-box {
|
||||
width: 100%;
|
||||
.auto-login-btn {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
.auto-login-img {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-box {
|
||||
margin: 80rpx auto 0;
|
||||
.protocol-check {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
.agreement-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #999999;
|
||||
.tcp-text {
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
.editPwd-btn-box {
|
||||
.save-btn {
|
||||
width: 690rpx;
|
||||
line-height: 70rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
border-radius: 35rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
.forgot-btn {
|
||||
width: 690rpx;
|
||||
line-height: 70rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
256
sheep/components/s-auth-modal/s-auth-modal.vue
Normal file
256
sheep/components/s-auth-modal/s-auth-modal.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<!-- 规格弹窗 -->
|
||||
<su-popup :show="authType !== ''" round="10" :showClose="true" @close="closeAuthModal">
|
||||
<view class="login-wrap">
|
||||
<!-- 1. 账号密码登录 accountLogin -->
|
||||
<account-login
|
||||
v-if="authType === 'accountLogin'"
|
||||
:agreeStatus="state.protocol"
|
||||
@onConfirm="onConfirm"
|
||||
/>
|
||||
|
||||
<block v-if="['H5', 'App'].includes(sheep.$platform.name)">
|
||||
<!-- 2. 短信登录 smsLogin -->
|
||||
<sms-login v-if="authType === 'smsLogin'" :agreeStatus="state.protocol" @onConfirm="onConfirm" />
|
||||
</block>
|
||||
|
||||
<!-- 3. 忘记密码 resetPassword-->
|
||||
<reset-password v-if="authType === 'resetPassword'" />
|
||||
|
||||
<!-- 4. 绑定手机号 changeMobile -->
|
||||
<change-mobile v-if="authType === 'changeMobile'" />
|
||||
|
||||
<!-- 5. 修改密码 changePassword-->
|
||||
<changePassword v-if="authType === 'changePassword'" />
|
||||
|
||||
<!-- 6. 微信小程序授权 -->
|
||||
<mp-authorization v-if="authType === 'mpAuthorization'" />
|
||||
|
||||
<!-- h5授权 -->
|
||||
<h5-authorization v-if="authType === 'h5Authorization'" />
|
||||
|
||||
<!-- 完善资料 -->
|
||||
<chat-authorization v-if="authType === 'chatAuthorization'" />
|
||||
|
||||
<!-- 7. 第三方登录 -->
|
||||
<view
|
||||
v-if="['accountLogin', 'smsLogin'].includes(authType)"
|
||||
class="auto-login-box ss-flex ss-flex-col ss-row-center ss-col-center"
|
||||
>
|
||||
<!-- 7.1 微信小程序的快捷登录 -->
|
||||
<!-- <view v-if="sheep.$platform.name === 'WechatMiniProgram'" class="ss-flex register-box">
|
||||
<view class="register-title">还没有账号?</view>
|
||||
<button class="ss-reset-button login-btn" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
|
||||
快捷登录
|
||||
</button>
|
||||
<view class="circle" />
|
||||
</view> -->
|
||||
|
||||
<!-- 7.2 微信的公众号、App、小程序的登录,基于 openid + code -->
|
||||
<button
|
||||
v-if="
|
||||
['WechatOfficialAccount', 'WechatMiniProgram', 'App'].includes(sheep.$platform.name) &&
|
||||
sheep.$platform.isWechatInstalled
|
||||
"
|
||||
@tap="thirdLogin('wechat')"
|
||||
class="ss-reset-button auto-login-btn"
|
||||
>
|
||||
<image
|
||||
class="auto-login-img"
|
||||
src="https://rbtnet.oss-cn-hangzhou.aliyuncs.com/1a2e97b866fd3de0b3c5c225a9cd3c705a645b0f76ad2ad8b0e25c8e6e20ab38.png"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- 7.3 iOS 登录 TODO 芋艿:等后面搞 App 再弄 -->
|
||||
<button
|
||||
v-if="sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
|
||||
@tap="thirdLogin('apple')"
|
||||
class="ss-reset-button auto-login-btn"
|
||||
>
|
||||
<image
|
||||
class="auto-login-img"
|
||||
:src="sheep.$url.static('/static/img/shop/platform/apple.png')"
|
||||
/>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 用户协议的勾选 -->
|
||||
<view
|
||||
v-if="['accountLogin', 'smsLogin'].includes(authType)"
|
||||
class="agreement-box ss-flex ss-row-center"
|
||||
:class="{ shake: currentProtocol }"
|
||||
>
|
||||
<label class="radio ss-flex ss-col-center" @tap="onChange">
|
||||
<radio
|
||||
:checked="state.protocol"
|
||||
color="var(--ui-BG-Main)"
|
||||
style="transform: scale(0.8)"
|
||||
@tap.stop="onChange"
|
||||
/>
|
||||
<view class="agreement-text ss-flex ss-col-center ss-m-l-8">
|
||||
我已阅读并遵守
|
||||
<view class="tcp-text" @tap.stop="onProtocol('用户协议')">
|
||||
《用户协议》
|
||||
</view>
|
||||
<view class="agreement-text">与</view>
|
||||
<view class="tcp-text" @tap.stop="onProtocol('隐私协议')">
|
||||
《隐私协议》
|
||||
</view>
|
||||
</view>
|
||||
</label>
|
||||
</view>
|
||||
<view class="safe-box"/>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import accountLogin from './components/account-login.vue';
|
||||
import smsLogin from './components/sms-login.vue';
|
||||
import resetPassword from './components/reset-password.vue';
|
||||
import changeMobile from './components/change-mobile.vue';
|
||||
import changePassword from './components/change-password.vue';
|
||||
import mpAuthorization from './components/mp-authorization.vue';
|
||||
import h5Authorization from './components/h5-authorization.vue';
|
||||
import chatAuthorization from './components/chat-authorization.vue';
|
||||
import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
|
||||
|
||||
const appInfo = computed(() => sheep.$store('app').info);
|
||||
|
||||
const modalStore = sheep.$store('modal');
|
||||
// 授权弹窗类型
|
||||
const authType = computed(() => modalStore.auth);
|
||||
|
||||
const state = reactive({
|
||||
protocol: false,
|
||||
});
|
||||
|
||||
const currentProtocol = ref(false);
|
||||
|
||||
// 勾选协议
|
||||
function onChange() {
|
||||
state.protocol = !state.protocol;
|
||||
}
|
||||
|
||||
// 查看协议
|
||||
function onProtocol(title) {
|
||||
closeAuthModal();
|
||||
sheep.$router.go('/pages/public/richtext', {
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
// 点击登录 / 注册事件
|
||||
function onConfirm(e) {
|
||||
currentProtocol.value = e;
|
||||
setTimeout(() => {
|
||||
currentProtocol.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 第三方授权登陆(微信小程序、Apple)
|
||||
const thirdLogin = async (provider) => {
|
||||
if (!state.protocol) {
|
||||
currentProtocol.value = true;
|
||||
setTimeout(() => {
|
||||
currentProtocol.value = false;
|
||||
}, 1000);
|
||||
sheep.$helper.toast('请勾选同意');
|
||||
return;
|
||||
}
|
||||
const loginRes = await sheep.$platform.useProvider(provider).login();
|
||||
if (loginRes) {
|
||||
const userInfo = await sheep.$store('user').getInfo();
|
||||
closeAuthModal();
|
||||
// 如果用户已经有头像和昵称,不要每次登录都要重新上传头像。
|
||||
if(userInfo.visible) return;
|
||||
// 触发小程序授权信息弹框
|
||||
// #ifdef MP-WEIXIN
|
||||
showAuthModal('mpAuthorization');
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
showAuthModal('h5Authorization');
|
||||
// #endif
|
||||
}
|
||||
};
|
||||
|
||||
// 微信小程序的“手机号快速验证”:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
|
||||
const getPhoneNumber = async (e) => {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
sheep.$helper.toast('快捷登录失败');
|
||||
return;
|
||||
}
|
||||
let result = await sheep.$platform.useProvider().mobileLogin(e.detail);
|
||||
if (result) {
|
||||
closeAuthModal();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './index.scss';
|
||||
|
||||
.shake {
|
||||
animation: shake 0.05s linear 4 alternate;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
from {
|
||||
transform: translateX(-10rpx);
|
||||
}
|
||||
to {
|
||||
transform: translateX(10rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.register-box {
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
.register-btn {
|
||||
color: #999999;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.register-title {
|
||||
color: #999999;
|
||||
font-size: 30rpx;
|
||||
font-weight: 400;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
.or-title {
|
||||
margin: 0 16rpx;
|
||||
color: #999999;
|
||||
font-size: 30rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
.login-btn {
|
||||
color: var(--ui-BG-Main);
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
.circle {
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
top: 18rpx;
|
||||
width: 8rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 8rpx;
|
||||
background: var(--ui-BG-Main);
|
||||
}
|
||||
}
|
||||
.safe-box {
|
||||
height: calc(constant(safe-area-inset-bottom) / 5 * 3);
|
||||
height: calc(env(safe-area-inset-bottom) / 5 * 3);
|
||||
}
|
||||
|
||||
.tcp-text {
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
|
||||
.agreement-text {
|
||||
color: $dark-9;
|
||||
}
|
||||
</style>
|
81
sheep/components/s-block-item/s-block-item.vue
Normal file
81
sheep/components/s-block-item/s-block-item.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 基础组件:搜索框 -->
|
||||
<s-search-block v-if="type === 'SearchBar'" :data="data" :styles="styles" :navbar="false" />
|
||||
<!-- 基础组件:公告栏 -->
|
||||
<s-notice-block v-if="type === 'NoticeBar'" :data="data" />
|
||||
<!-- 基础组件:菜单导航 -->
|
||||
<s-menu-button v-if="type === 'MenuSwiper'" :data="data" :styles="styles" />
|
||||
<!-- 基础组件:列表导航 -->
|
||||
<s-menu-list v-if="type === 'MenuList'" :data="data" />
|
||||
<!-- 基础组件:宫格导航 -->
|
||||
<s-menu-grid v-if="type === 'MenuGrid'" :data="data" :styles="styles" />
|
||||
<!-- 基础组件:弹窗广告 -->
|
||||
<s-popup-image v-if="type === 'Popover'" :data="data" />
|
||||
<!-- 基础组件:悬浮按钮 -->
|
||||
<s-float-menu v-if="type === 'FloatingActionButton'" :data="data" />
|
||||
|
||||
<!-- 图文组件:图片展示 -->
|
||||
<s-image-block v-if="type === 'ImageBar'" :data="data" :styles="styles" />
|
||||
<!-- 图文组件:图片轮播 -->
|
||||
<s-image-banner v-if="type === 'Carousel'" :data="data" :styles="styles" />
|
||||
<!-- 基础组件:标题栏 -->
|
||||
<s-title-block v-if="type === 'TitleBar'" :data="data" :styles="styles" />
|
||||
<!-- 图文组件:广告魔方 -->
|
||||
<s-image-cube v-if="type === 'MagicCube'" :data="data" :styles="styles" />
|
||||
<!-- 图文组件:视频播放 -->
|
||||
<s-video-block v-if="type === 'VideoPlayer'" :data="data" :styles="styles" />
|
||||
<!-- 基础组件:分割线 -->
|
||||
<s-line-block v-if="type === 'Divider'" :data="data" />
|
||||
<!-- 图文组件:热区 -->
|
||||
<s-hotzone-block v-if="type === 'HotZone'" :data="data" :styles="styles" />
|
||||
|
||||
<!-- 商品组件:商品卡片 -->
|
||||
<s-goods-card v-if="type === 'ProductCard'" :data="data" :styles="styles" />
|
||||
<!-- 商品组件:商品栏 -->
|
||||
<s-goods-shelves v-if="type === 'ProductList'" :data="data" :styles="styles" />
|
||||
|
||||
<!-- 营销组件:拼团 -->
|
||||
<s-groupon-block v-if="type === 'PromotionCombination'" :data="data" :styles="styles" />
|
||||
<!-- 营销组件:秒杀 -->
|
||||
<s-seckill-block v-if="type === 'PromotionSeckill'" :data="data" :styles="styles" />
|
||||
<!-- 营销组件:小程序直播(暂时没有这个功能) -->
|
||||
<s-live-block v-if="type === 'MpLive'" :data="data" :styles="styles" />
|
||||
<!-- 营销组件:优惠券 -->
|
||||
<s-coupon-block v-if="type === 'CouponCard'" :data="data" :styles="styles" />
|
||||
<!-- 营销组件:文章 -->
|
||||
<s-richtext-block v-if="type === 'PromotionArticle'" :data="data" :styles="styles" />
|
||||
|
||||
<!-- 用户组件:用户卡片 -->
|
||||
<s-user-card v-if="type === 'UserCard'" :data="data" :styles="styles" />
|
||||
<!-- 用户组件:用户订单 -->
|
||||
<s-order-card v-if="type === 'UserOrder'" :data="data" :styles="styles" />
|
||||
<!-- 用户组件:用户资产 -->
|
||||
<s-wallet-card v-if="type === 'UserWallet'" :data="data" :styles="styles" />
|
||||
<!-- 用户组件:用户卡券 -->
|
||||
<s-coupon-card v-if="type === 'UserCoupon'" :data="data" :styles="styles" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 组件集
|
||||
*/
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
function onSearch() {}
|
||||
</script>
|
||||
|
||||
<style></style>
|
54
sheep/components/s-block/s-block.vue
Normal file
54
sheep/components/s-block/s-block.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<!-- 装修组件容器 -->
|
||||
<template>
|
||||
<view :style="[elStyles, elBackground]"><slot /></view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 容器组件 - 装修组件的样式容器
|
||||
*/
|
||||
import { computed, provide, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
const props = defineProps({
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
// 组件样式
|
||||
|
||||
const elBackground = computed(() => {
|
||||
if (props.styles) {
|
||||
if (props.styles.bgType === 'color')
|
||||
return { background: props.styles.bgColor };
|
||||
if (props.styles.bgType === 'img')
|
||||
return {
|
||||
background: `url(${sheep.$url.cdn(
|
||||
props.styles.bgImage,
|
||||
)}) no-repeat top center / 100% auto`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const elStyles = computed(() => {
|
||||
if (props.styles) {
|
||||
return {
|
||||
marginTop: `${props.styles.marginTop || 0}px`,
|
||||
marginBottom: `${props.styles.marginBottom || 0}px`,
|
||||
marginLeft: `${props.styles.marginLeft || 0}px`,
|
||||
marginRight: `${props.styles.marginRight || 0}px`,
|
||||
paddingTop: `${props.styles.paddingTop || 0}px`,
|
||||
paddingRight: `${props.styles.paddingRight || 0}px`,
|
||||
paddingBottom: `${props.styles.paddingBottom || 0}px`,
|
||||
paddingLeft: `${props.styles.paddingLeft || 0}px`,
|
||||
borderTopLeftRadius: `${props.styles.borderTopLeftRadius || 0}px`,
|
||||
borderTopRightRadius: `${props.styles.borderTopRightRadius || 0}px`,
|
||||
borderBottomRightRadius: `${props.styles.borderBottomRightRadius || 0}px`,
|
||||
borderBottomLeftRadius: `${props.styles.borderBottomLeftRadius || 0}px`,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
173
sheep/components/s-count-down/s-count-down.vue
Normal file
173
sheep/components/s-count-down/s-count-down.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<view class="time" :style="justifyLeft">
|
||||
<text class="" v-if="tipText">{{ tipText }}</text>
|
||||
<text class="styleAll p6" v-if="isDay === true"
|
||||
:style="{background:bgColor.bgColor,color:bgColor.Color}">{{ day }}{{bgColor.isDay?'天':''}}</text>
|
||||
<text class="timeTxt" v-if="dayText"
|
||||
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ dayText }}</text>
|
||||
<text class="styleAll" :class='isCol?"timeCol":""'
|
||||
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ hour }}</text>
|
||||
<text class="timeTxt" v-if="hourText" :class='isCol?"whit":""'
|
||||
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ hourText }}</text>
|
||||
<text class="styleAll" :class='isCol?"timeCol":""'
|
||||
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ minute }}</text>
|
||||
<text class="timeTxt" v-if="minuteText" :class='isCol?"whit":""'
|
||||
:style="{width:bgColor.timeTxtwidth,color:bgColor.bgColor}">{{ minuteText }}</text>
|
||||
<text class="styleAll" :class='isCol?"timeCol":""'
|
||||
:style="{background:bgColor.bgColor,color:bgColor.Color,width:bgColor.width}">{{ second }}</text>
|
||||
<text class="timeTxt" v-if="secondText">{{ secondText }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "countDown",
|
||||
props: {
|
||||
justifyLeft: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
//距离开始提示文字
|
||||
tipText: {
|
||||
type: String,
|
||||
default: "倒计时"
|
||||
},
|
||||
dayText: {
|
||||
type: String,
|
||||
default: "天"
|
||||
},
|
||||
hourText: {
|
||||
type: String,
|
||||
default: "时"
|
||||
},
|
||||
minuteText: {
|
||||
type: String,
|
||||
default: "分"
|
||||
},
|
||||
secondText: {
|
||||
type: String,
|
||||
default: "秒"
|
||||
},
|
||||
datatime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
isDay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isCol: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
bgColor: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
day: "00",
|
||||
hour: "00",
|
||||
minute: "00",
|
||||
second: "00"
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.show_time();
|
||||
},
|
||||
mounted: function() {},
|
||||
methods: {
|
||||
show_time: function() {
|
||||
let that = this;
|
||||
|
||||
function runTime() {
|
||||
//时间函数
|
||||
let intDiff = that.datatime - Date.parse(new Date()) / 1000; //获取数据中的时间戳的时间差;
|
||||
let day = 0,
|
||||
hour = 0,
|
||||
minute = 0,
|
||||
second = 0;
|
||||
if (intDiff > 0) {
|
||||
//转换时间
|
||||
if (that.isDay === true) {
|
||||
day = Math.floor(intDiff / (60 * 60 * 24));
|
||||
} else {
|
||||
day = 0;
|
||||
}
|
||||
hour = Math.floor(intDiff / (60 * 60)) - day * 24;
|
||||
minute = Math.floor(intDiff / 60) - day * 24 * 60 - hour * 60;
|
||||
second =
|
||||
Math.floor(intDiff) -
|
||||
day * 24 * 60 * 60 -
|
||||
hour * 60 * 60 -
|
||||
minute * 60;
|
||||
if (hour <= 9) hour = "0" + hour;
|
||||
if (minute <= 9) minute = "0" + minute;
|
||||
if (second <= 9) second = "0" + second;
|
||||
that.day = day;
|
||||
that.hour = hour;
|
||||
that.minute = minute;
|
||||
that.second = second;
|
||||
} else {
|
||||
that.day = "00";
|
||||
that.hour = "00";
|
||||
that.minute = "00";
|
||||
that.second = "00";
|
||||
}
|
||||
}
|
||||
runTime();
|
||||
setInterval(runTime, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.p6 {
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.styleAll {
|
||||
/* color: #fff; */
|
||||
font-size: 24rpx;
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
border-radius: 6rpx;
|
||||
text-align: center;
|
||||
/* padding: 0 6rpx; */
|
||||
}
|
||||
|
||||
.timeTxt {
|
||||
text-align: center;
|
||||
/* width: 16rpx; */
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.whit {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #fc4141;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
|
||||
.timeCol {
|
||||
/* width: 40rpx;
|
||||
height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
text-align:center;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
font-size: 24rpx; */
|
||||
color: #E93323;
|
||||
}
|
||||
</style>
|
176
sheep/components/s-coupon-block/s-coupon-block.vue
Normal file
176
sheep/components/s-coupon-block/s-coupon-block.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<!-- 装修营销组件:优惠券 -->
|
||||
<template>
|
||||
<scroll-view class="scroll-box" scroll-x scroll-anchoring :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<view class="coupon-box ss-flex" :style="couponList.length === 2 ? couponBoxStyleTwo : couponBoxStyleNormal">
|
||||
<view class="coupon-item" :style="[couponBg, { marginLeft: `${data.space}px` }]"
|
||||
v-for="(item, index) in couponList" :key="index">
|
||||
<su-coupon :size="SIZE_LIST[columns - 1]" :textColor="data.textColor" background="" :couponId="item.id"
|
||||
:title="item.name" :type="formatCouponDiscountType(item)" :value="formatCouponDiscountValue(item)"
|
||||
:sellBy="formatValidityType(item)">
|
||||
<template v-slot:btn>
|
||||
<!-- 两列时,领取按钮坚排 -->
|
||||
<button v-if="columns === 2" @click.stop="onGetCoupon(item.id)"
|
||||
class="ss-reset-button card-btn vertical" :style="[btnStyles]">
|
||||
<view class="btn-text">立即领取</view>
|
||||
</button>
|
||||
<button v-else class="ss-reset-button card-btn" :style="[btnStyles]"
|
||||
@click.stop="onGetCoupon(item.id)">
|
||||
立即领取
|
||||
</button>
|
||||
</template>
|
||||
</su-coupon>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import CouponApi from '@/sheep/api/promotion/coupon';
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
computed
|
||||
} from 'vue';
|
||||
import {
|
||||
CouponTemplateValidityTypeEnum,
|
||||
PromotionDiscountTypeEnum
|
||||
} from "@/sheep/util/const";
|
||||
import {
|
||||
floatToFixed2,
|
||||
formatDate
|
||||
} from "@/sheep/util";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
const {
|
||||
columns,
|
||||
button
|
||||
} = props.data;
|
||||
const SIZE_LIST = ['lg', 'md', 'xs']
|
||||
const couponBg = {
|
||||
background: `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`,
|
||||
};
|
||||
const btnStyles = {
|
||||
background: button.bgColor,
|
||||
color: button.color,
|
||||
};
|
||||
// 两列优惠券时的排版方式
|
||||
const couponBoxStyleNormal = {
|
||||
'display': 'flex',
|
||||
'justify-content': 'space-between'
|
||||
};
|
||||
// 非两列优惠券时的排版方式
|
||||
const couponBoxStyleTwo = {
|
||||
'display': 'flex',
|
||||
'justify-content': 'space-around'
|
||||
};
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const {
|
||||
bgType,
|
||||
bgImg,
|
||||
bgColor
|
||||
} = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img' ? `url(${bgImg}) no-repeat top center / 100% 100%` : bgColor
|
||||
};
|
||||
});
|
||||
// 格式化【折扣类型】
|
||||
const formatCouponDiscountType = (coupon) => {
|
||||
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
|
||||
return 'reduce'
|
||||
}
|
||||
if (coupon.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
|
||||
return 'percent'
|
||||
}
|
||||
return `未知【${coupon.discountType}】`
|
||||
}
|
||||
|
||||
// 格式化【折扣】
|
||||
const formatCouponDiscountValue = (coupon) => {
|
||||
if (coupon.discountType === PromotionDiscountTypeEnum.PRICE.type) {
|
||||
return floatToFixed2(coupon.discountPrice)
|
||||
}
|
||||
if (coupon.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
|
||||
return coupon.discountPercent
|
||||
}
|
||||
return `未知【${coupon.discountType}】`
|
||||
}
|
||||
|
||||
// 格式化【有效期限】
|
||||
const formatValidityType = (row) => {
|
||||
if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) {
|
||||
return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`
|
||||
}
|
||||
if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) {
|
||||
return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`
|
||||
}
|
||||
return '未知【' + row.validityType + '】'
|
||||
}
|
||||
|
||||
const couponList = ref([]);
|
||||
// 立即领取优惠券
|
||||
async function onGetCoupon(id) {
|
||||
const {
|
||||
error,
|
||||
msg
|
||||
} = await CouponApi.takeCoupon(id);
|
||||
if (error === 0) {
|
||||
uni.showToast({
|
||||
title: msg,
|
||||
icon: 'none',
|
||||
});
|
||||
return
|
||||
}
|
||||
await getCouponTemplateList()
|
||||
}
|
||||
const getCouponTemplateList = async () => {
|
||||
const {
|
||||
data
|
||||
} = await CouponApi.getCouponTemplateListByIds(props.data.couponIds.join(','));
|
||||
couponList.value = data;
|
||||
}
|
||||
onMounted(() => {
|
||||
getCouponTemplateList()
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-btn {
|
||||
width: 140rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 50rpx;
|
||||
|
||||
&.vertical {
|
||||
width: 50rpx;
|
||||
height: 140rpx;
|
||||
margin: auto 20rpx auto 0;
|
||||
|
||||
.btn-text {
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
writing-mode: vertical-lr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coupon-item {
|
||||
&:nth-of-type(1) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
102
sheep/components/s-coupon-card/s-coupon-card.vue
Normal file
102
sheep/components/s-coupon-card/s-coupon-card.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- 装修用户组件:用户卡券 -->
|
||||
<template>
|
||||
<view class="ss-coupon-menu-wrap ss-flex ss-col-center" :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<view class="menu-item ss-flex-col ss-row-center ss-col-center" v-for="item in props.list" :key="item.title"
|
||||
@tap="sheep.$router.go(item.path, { type: item.type })"
|
||||
:class="item.type === 'all' ? 'menu-wallet' : 'ss-flex-1'">
|
||||
<image class="item-icon" :src="sheep.$url.static(item.icon)" mode="aspectFit"></image>
|
||||
<view class="menu-title ss-m-t-28">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 优惠券菜单
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default () {
|
||||
return [{
|
||||
title: '已领取',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/nouse_coupon.png',
|
||||
path: '/pages/coupon/list',
|
||||
type: 'geted',
|
||||
},
|
||||
{
|
||||
title: '已使用',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/useend_coupon.png',
|
||||
path: '/pages/coupon/list',
|
||||
type: 'used',
|
||||
},
|
||||
{
|
||||
title: '已失效',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/out_coupon.png',
|
||||
path: '/pages/coupon/list',
|
||||
type: 'expired',
|
||||
},
|
||||
{
|
||||
title: '领券中心',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/all_coupon.png',
|
||||
path: '/pages/coupon/list',
|
||||
type: 'all',
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const { bgType, bgImg, bgColor } = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img'
|
||||
? `url(${bgImg}) no-repeat top center / 100% 100%`
|
||||
: bgColor
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-coupon-menu-wrap {
|
||||
.menu-item {
|
||||
height: 160rpx;
|
||||
|
||||
.menu-title {
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wallet {
|
||||
width: 144rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
109
sheep/components/s-coupon-get/s-coupon-get.vue
Normal file
109
sheep/components/s-coupon-get/s-coupon-get.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<!-- 商品详情 - 优惠劵领取 -->
|
||||
<template>
|
||||
<su-popup
|
||||
:show="show"
|
||||
type="bottom"
|
||||
round="20"
|
||||
@close="emits('close')"
|
||||
showClose
|
||||
backgroundColor="#f2f2f2"
|
||||
>
|
||||
<view class="model-box">
|
||||
<view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
|
||||
<scroll-view
|
||||
class="model-content"
|
||||
scroll-y
|
||||
:scroll-with-animation="false"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<view class="subtitle ss-m-l-20">可使用优惠券</view>
|
||||
<view v-for="item in state.couponInfo" :key="item.id">
|
||||
<s-coupon-list :data="item">
|
||||
<template #default>
|
||||
<button
|
||||
class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
|
||||
:class="!item.canTake ? 'boder-btn' : ''"
|
||||
@click.stop="getBuy(item.id)"
|
||||
:disabled="!item.canTake"
|
||||
>
|
||||
{{ item.canTake ? '立即领取' : '已领取' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-coupon-list>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['get', 'close']);
|
||||
|
||||
const state = reactive({
|
||||
couponInfo: computed(() => props.modelValue)
|
||||
});
|
||||
|
||||
// 领取优惠劵
|
||||
const getBuy = (id) => {
|
||||
emits('get', id);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.model-box {
|
||||
height: 60vh;
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
height: 80rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
.model-content {
|
||||
height: 54vh;
|
||||
}
|
||||
.modal-footer {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.confirm-btn {
|
||||
width: 710rpx;
|
||||
margin-left: 20rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
// 优惠券按钮
|
||||
.card-btn {
|
||||
// width: 144rpx;
|
||||
padding: 0 16rpx;
|
||||
height: 50rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
.boder-btn {
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main-opacity-4), var(--ui-BG-Main-light));
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
205
sheep/components/s-coupon-list/s-coupon-list.vue
Normal file
205
sheep/components/s-coupon-list/s-coupon-list.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<view class="ss-m-20" :style="{ opacity: disabled ? '0.5' : '1' }">
|
||||
<view class="content">
|
||||
<view
|
||||
class="tag ss-flex ss-row-center"
|
||||
:class="isDisable ? 'disabled-bg-color' : 'info-bg-color'"
|
||||
>
|
||||
{{ data.discountType === 1 ? '满减券' : '折扣券' }}
|
||||
</view>
|
||||
<view class="title ss-m-x-30 ss-p-t-18">
|
||||
<view class="ss-flex ss-row-between">
|
||||
<view
|
||||
class="value-text ss-flex-1 ss-m-r-10"
|
||||
:class="isDisable ? 'disabled-color' : 'info-color'"
|
||||
>
|
||||
{{ data.name }}
|
||||
</view>
|
||||
<view>
|
||||
<view
|
||||
class="ss-flex ss-col-bottom"
|
||||
:class="isDisable ? 'disabled-color' : 'price-text'"
|
||||
>
|
||||
<view class="value-reduce ss-m-b-10" v-if="data.discountType === 1">¥</view>
|
||||
<view class="value-price">
|
||||
{{
|
||||
data.discountType === 1
|
||||
? fen2yuan(data.discountPrice)
|
||||
: data.discountPercent / 10.0
|
||||
}}
|
||||
</view>
|
||||
<view class="value-discount ss-m-b-10 ss-m-l-4" v-if="data.discountType === 2"
|
||||
>折</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ss-flex ss-row-between ss-m-t-16">
|
||||
<view
|
||||
class="sellby-text"
|
||||
:class=" isDisable ? 'disabled-color' : 'subtitle-color'"
|
||||
v-if="data.validityType === 2"
|
||||
>
|
||||
有效期:领取后 {{ data.fixedEndTerm }} 天内可用
|
||||
</view>
|
||||
<view
|
||||
class="sellby-text"
|
||||
:class=" isDisable ? 'disabled-color' : 'subtitle-color'"
|
||||
v-else
|
||||
>
|
||||
有效期: {{ sheep.$helper.timeFormat(data.validStartTime, 'yyyy-mm-dd') }} 至
|
||||
{{ sheep.$helper.timeFormat(data.validEndTime, 'yyyy-mm-dd') }}
|
||||
</view>
|
||||
<view
|
||||
class="value-enough"
|
||||
:class="isDisable ? 'disabled-color' : 'subtitle-color'"
|
||||
>
|
||||
满 {{ fen2yuan(data.usePrice) }} 可用
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- TODO 芋艿:可优化,增加优惠劵的描述 -->
|
||||
<view class="desc ss-flex ss-row-between">
|
||||
<view>
|
||||
<view class="desc-title">{{ data.description }}</view>
|
||||
<view>
|
||||
<slot name="reason" />
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<slot />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import { fen2yuan } from '../../hooks/useGoods';
|
||||
import sheep from '../../index';
|
||||
|
||||
const state = reactive({});
|
||||
|
||||
const isDisable = computed(() => {
|
||||
if (props.type === 'coupon') {
|
||||
return false;
|
||||
}
|
||||
return props.data.status !== 1;
|
||||
});
|
||||
|
||||
// 接受参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'coupon', // coupon 优惠劵模版;user 用户优惠劵
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.info-bg-color {
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
}
|
||||
|
||||
.disabled-bg-color {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.info-color {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle-color {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.disabled-color {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
-webkit-mask: radial-gradient(circle at 12rpx 100%, #0000 12rpx, red 0) -12rpx;
|
||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.04);
|
||||
|
||||
.tag {
|
||||
width: 100rpx;
|
||||
|
||||
color: #fff;
|
||||
height: 40rpx;
|
||||
font-size: 24rpx;
|
||||
border-radius: 20rpx 0 20rpx 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-bottom: 22rpx;
|
||||
border-bottom: 2rpx dashed #d3d3d3;
|
||||
|
||||
.value-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sellby-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.value-price {
|
||||
font-size: 64rpx;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
|
||||
.value-reduce {
|
||||
line-height: normal;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.value-discount {
|
||||
line-height: normal;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.value-enough {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
-webkit-mask: radial-gradient(circle at 12rpx 0%, #0000 12rpx, red 0) -12rpx;
|
||||
box-shadow: rgba(#000, 0.1);
|
||||
box-sizing: border-box;
|
||||
padding: 24rpx 30rpx;
|
||||
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.04);
|
||||
border-radius: 0 0 20rpx 20rpx;
|
||||
|
||||
.desc-title {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #ff0000;
|
||||
}
|
||||
</style>
|
140
sheep/components/s-coupon-select/s-coupon-select.vue
Normal file
140
sheep/components/s-coupon-select/s-coupon-select.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<!-- 订单确认的优惠劵选择弹窗 -->
|
||||
<template>
|
||||
<su-popup :show="show" type="bottom" round="20" @close="emits('close')" showClose backgroundColor="#f2f2f2">
|
||||
<view class="model-box">
|
||||
<view class="title ss-m-t-16 ss-m-l-20 ss-flex">优惠券</view>
|
||||
<scroll-view class="model-content" scroll-y :scroll-with-animation="false" :enable-back-to-top="true">
|
||||
<!--可使用的优惠券区域-->
|
||||
<!-- <view class="subtitle ss-m-l-20">可使用优惠券</view> -->
|
||||
<view v-for="(item, index) in state.couponInfo" :key="index">
|
||||
<s-coupon-list :data="item" type="user" :disabled="false">
|
||||
<template v-slot:reason>
|
||||
<view class="ss-flex ss-m-t-24">
|
||||
<view class="reason-title">可用原因:</view>
|
||||
<view class="reason-desc">{{ item.description || '已达到使用门槛' }}</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<label class="ss-flex ss-col-center" @tap="radioChange(item.id)">
|
||||
<radio
|
||||
color="var(--ui-BG-Main)"
|
||||
style="transform: scale(0.8)"
|
||||
:checked="state.couponId === item.id"
|
||||
@tap.stop="radioChange(item.id)"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
</s-coupon-list>
|
||||
</view>
|
||||
<!--不可使用的优惠券区域-->
|
||||
<!-- <view class="subtitle ss-m-t-40 ss-m-l-20">不可使用优惠券</view>
|
||||
<view v-for="item in state.couponInfo.filter(coupon => !coupon.match)" :key="item.id">
|
||||
<s-coupon-list :data="item" type="user" :disabled="true">
|
||||
<template v-slot:reason>
|
||||
<view class="ss-flex ss-m-t-24">
|
||||
<view class="reason-title"> 不可用原因:</view>
|
||||
<view class="reason-desc">{{ item.description || '未达到使用门槛' }}</view>
|
||||
</view>
|
||||
</template>
|
||||
</s-coupon-list>
|
||||
</view> -->
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer ss-flex">
|
||||
<button class="confirm-btn ss-reset-button" @tap="onConfirm">确认</button>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { // 优惠劵列表
|
||||
type: Object,
|
||||
default() {
|
||||
},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['confirm', 'close']);
|
||||
|
||||
const state = reactive({
|
||||
couponInfo: computed(() => props.modelValue), // 优惠劵列表
|
||||
couponId: undefined, // 选中的优惠劵编号
|
||||
});
|
||||
|
||||
// 选中优惠劵
|
||||
function radioChange(couponId) {
|
||||
if (state.couponId === couponId) {
|
||||
state.couponId = undefined;
|
||||
} else {
|
||||
state.couponId = couponId;
|
||||
}
|
||||
}
|
||||
|
||||
// 确认优惠劵
|
||||
const onConfirm = () => {
|
||||
emits('confirm', state.couponId);
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep() {
|
||||
.uni-checkbox-input {
|
||||
background-color: var(--ui-BG-Main);
|
||||
}
|
||||
}
|
||||
|
||||
.model-box {
|
||||
height: 60vh;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
height: 80rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.model-content {
|
||||
height: 54vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.confirm-btn {
|
||||
width: 710rpx;
|
||||
margin-left: 20rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reason-title {
|
||||
font-weight: 600;
|
||||
font-size: 20rpx;
|
||||
line-height: 26rpx;
|
||||
color: #ff0003;
|
||||
}
|
||||
|
||||
.reason-desc {
|
||||
font-weight: 600;
|
||||
font-size: 20rpx;
|
||||
line-height: 26rpx;
|
||||
color: #434343;
|
||||
}
|
||||
</style>
|
66
sheep/components/s-custom-navbar/components/navbar-item.vue
Normal file
66
sheep/components/s-custom-navbar/components/navbar-item.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- 顶部导航栏 - 单元格 -->
|
||||
<template>
|
||||
<view class="ss-flex ss-col-center">
|
||||
<!-- 类型一: 文字 -->
|
||||
<view
|
||||
v-if="data.type === 'text'"
|
||||
class="nav-title inline"
|
||||
:style="[{ color: data.textColor, width: width }]"
|
||||
>
|
||||
{{ data.text }}
|
||||
</view>
|
||||
<!-- 类型二: 图片 -->
|
||||
<view
|
||||
v-if="data.type === 'image'"
|
||||
:style="[{ width: width }]"
|
||||
class="menu-icon-wrap ss-flex ss-row-center ss-col-center"
|
||||
@tap="sheep.$router.go(data.url)"
|
||||
>
|
||||
<image class="nav-image" :src="sheep.$url.cdn(data.imgUrl)" mode="aspectFit"></image>
|
||||
</view>
|
||||
<!-- 类型三: 搜索框 -->
|
||||
<view class="ss-flex-1" v-if="data.type === 'search'" :style="[{ width: width }]">
|
||||
<s-search-block
|
||||
:placeholder="data.placeholder || '搜索关键字'"
|
||||
:radius="data.borderRadius"
|
||||
elBackground="#fff"
|
||||
:height="height"
|
||||
:width="width"
|
||||
@click="sheep.$router.go('/pages/index/search')"
|
||||
></s-search-block>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '1px',
|
||||
},
|
||||
});
|
||||
|
||||
const height = computed(() => sheep.$platform.capsule.height);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-icon-wrap {
|
||||
.nav-image {
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
314
sheep/components/s-custom-navbar/components/navbar.vue
Normal file
314
sheep/components/s-custom-navbar/components/navbar.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<su-fixed
|
||||
:noFixed="props.noFixed"
|
||||
:alway="props.alway"
|
||||
:bgStyles="props.bgStyles"
|
||||
:val="0"
|
||||
:index="props.zIndex"
|
||||
noNav
|
||||
:bg="props.bg"
|
||||
:ui="props.ui"
|
||||
:opacity="props.opacity"
|
||||
:placeholder="props.placeholder"
|
||||
:sticky="props.sticky"
|
||||
>
|
||||
<su-status-bar />
|
||||
<!--
|
||||
:class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
|
||||
-->
|
||||
<view class="ui-navbar-box">
|
||||
<view
|
||||
class="ui-bar"
|
||||
:class="
|
||||
props.status == '' ? `text-a` : props.status == 'light' ? 'text-white' : 'text-black'
|
||||
"
|
||||
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
|
||||
>
|
||||
<slot name="item"></slot>
|
||||
<view class="right">
|
||||
<!-- #ifdef MP -->
|
||||
<view :style="[state.capsuleStyle]"></view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-fixed>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 标题栏 - 基础组件navbar
|
||||
*
|
||||
* @param {Number} zIndex = 100 - 层级
|
||||
* @param {Boolean} back = true - 是否返回上一页
|
||||
* @param {String} backtext = '' - 返回文本
|
||||
* @param {String} bg = 'bg-white' - 公共Class
|
||||
* @param {String} status = '' - 状态栏颜色
|
||||
* @param {Boolean} alway = true - 是否常驻
|
||||
* @param {Boolean} opacity = false - 是否开启透明渐变
|
||||
* @param {Boolean} opacityBg = false - 开启滑动渐变后,返回按钮是否添加背景
|
||||
* @param {Boolean} noFixed = false - 是否浮动
|
||||
* @param {String} ui = '' - 公共Class
|
||||
* @param {Boolean} capsule = false - 是否开启胶囊返回
|
||||
* @param {Boolean} stopBack = false - 是否禁用返回
|
||||
* @param {Boolean} placeholder = true - 是否开启占位
|
||||
* @param {Object} bgStyles = {} - 背景样式
|
||||
*
|
||||
*/
|
||||
|
||||
import { computed, reactive, onBeforeMount } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 本地数据
|
||||
const state = reactive({
|
||||
statusCur: '',
|
||||
capsuleStyle: {},
|
||||
capsuleBack: {},
|
||||
});
|
||||
|
||||
const sys_statusBar = sheep.$platform.device.statusBarHeight;
|
||||
const sys_navBar = sheep.$platform.navbar;
|
||||
|
||||
const props = defineProps({
|
||||
sticky: Boolean,
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 100,
|
||||
},
|
||||
back: {
|
||||
//是否返回上一页
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
backtext: {
|
||||
//返回文本
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bg: {
|
||||
type: String,
|
||||
default: 'bg-white',
|
||||
},
|
||||
status: {
|
||||
//状态栏颜色 可以选择light dark/其他字符串视为黑色
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 常驻
|
||||
alway: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
opacity: {
|
||||
//是否开启滑动渐变
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
opacityBg: {
|
||||
//开启滑动渐变后 返回按钮是否添加背景
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
noFixed: {
|
||||
//是否浮动
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
capsule: {
|
||||
//是否开启胶囊返回
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
stopBack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: [Boolean],
|
||||
default: true,
|
||||
},
|
||||
bgStyles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['navback']);
|
||||
|
||||
onBeforeMount(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
// 返回
|
||||
const onNavback = () => {
|
||||
sheep.$router.back();
|
||||
};
|
||||
|
||||
// 初始化
|
||||
const init = () => {
|
||||
state.capsuleStyle = {
|
||||
width: sheep.$platform.capsule.width + 'px',
|
||||
height: sheep.$platform.capsule.height + 'px',
|
||||
margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
|
||||
};
|
||||
|
||||
state.capsuleBack = state.capsuleStyle;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ui-navbar-box {
|
||||
background-color: transparent;
|
||||
width: 100%;
|
||||
|
||||
.ui-bar {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.left {
|
||||
@include flex-bar;
|
||||
|
||||
.back {
|
||||
@include flex-bar;
|
||||
|
||||
.back-icon {
|
||||
@include flex-center;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
margin: 0 10rpx;
|
||||
font-size: 46rpx !important;
|
||||
|
||||
&.opacityIcon {
|
||||
position: relative;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(127, 127, 127, 0.5);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 200%;
|
||||
width: 200%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.1;
|
||||
border: 1px solid currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* #ifdef MP-ALIPAY */
|
||||
._icon-back {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.capsule {
|
||||
@include flex-bar;
|
||||
border-radius: 100px;
|
||||
position: relative;
|
||||
|
||||
&.dark {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 60%;
|
||||
width: 1px;
|
||||
left: 50%;
|
||||
top: 20%;
|
||||
background-color: currentColor;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 200%;
|
||||
width: 200%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-radius: inherit;
|
||||
transform: scale(0.5);
|
||||
transform-origin: 0 0;
|
||||
opacity: 0.1;
|
||||
border: 1px solid currentColor;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.capsule-back,
|
||||
.capsule-home {
|
||||
@include flex-center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.isFristPage {
|
||||
.capsule-back,
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
@include flex-bar;
|
||||
|
||||
.right-content {
|
||||
@include flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.center {
|
||||
@include flex-center;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
|
||||
.image {
|
||||
display: block;
|
||||
height: 36px;
|
||||
max-width: calc(100vw - 200px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ui-bar-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
207
sheep/components/s-custom-navbar/s-custom-navbar.vue
Normal file
207
sheep/components/s-custom-navbar/s-custom-navbar.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<!-- 顶部导航栏 -->
|
||||
<template>
|
||||
<navbar
|
||||
:alway="isAlways"
|
||||
:back="false"
|
||||
bg=""
|
||||
:placeholder="isPlaceholder"
|
||||
:bgStyles="bgStyles"
|
||||
:opacity="isOpacity"
|
||||
:sticky="sticky"
|
||||
>
|
||||
<template #item>
|
||||
<view class="nav-box">
|
||||
<view class="nav-icon" v-if="showLeftButton">
|
||||
<view class="icon-box ss-flex" :class="{ 'inner-icon-box': data.styleType === 'inner' }">
|
||||
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
|
||||
<text class="sicon-back" v-if="hasHistory" />
|
||||
<text class="sicon-home" v-else />
|
||||
</view>
|
||||
<view class="line"></view>
|
||||
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
|
||||
<text class="sicon-more" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="nav-item"
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
:style="[parseImgStyle(item)]"
|
||||
:class="[{ 'ss-flex ss-col-center ss-row-center': item.type !== 'search' }]"
|
||||
>
|
||||
<navbar-item :data="item" :width="parseImgStyle(item).width" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</navbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 自定义标题栏
|
||||
*
|
||||
*
|
||||
* @property {Number | String} alwaysShow = [0,1] - 是否常驻
|
||||
* @property {Number | String} styleType = [inner] - 是否沉浸式
|
||||
* @property {String | Number} type - 标题背景模式
|
||||
* @property {String} color - 页面背景色
|
||||
* @property {String} src - 页面背景图片
|
||||
*/
|
||||
import { computed, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import Navbar from './components/navbar.vue';
|
||||
import NavbarItem from './components/navbar-item.vue';
|
||||
import { showMenuTools } from '@/sheep/hooks/useModal';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
showLeftButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const hasHistory = sheep.$router.hasHistory();
|
||||
const sticky = computed(() => {
|
||||
if (props.data.styleType === 'inner') {
|
||||
if (props.data.alwaysShow) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (props.data.styleType === 'normal') {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
const navList = computed(() => {
|
||||
// #ifdef MP
|
||||
return props.data.mpCells || [];
|
||||
// #endif
|
||||
return props.data.otherCells || [];
|
||||
});
|
||||
// 页面宽度
|
||||
const windowWidth = sheep.$platform.device.windowWidth;
|
||||
// 单元格宽度
|
||||
const cell = computed(() => {
|
||||
if (unref(navList).length) {
|
||||
// 默认宽度为8个格子,微信公众号右上角有胶囊按钮所以是6个格子
|
||||
let cell = (windowWidth - 90) / 8;
|
||||
// #ifdef MP
|
||||
cell = (windowWidth - 80 - unref(sheep.$platform.capsule).width) / 6;
|
||||
// #endif
|
||||
return cell;
|
||||
}
|
||||
});
|
||||
// 解析位置
|
||||
const parseImgStyle = (item) => {
|
||||
let obj = {
|
||||
width: item.width * cell.value + (item.width - 1) * 10 + 'px',
|
||||
left: item.left * cell.value + (item.left + 1) * 10 + 'px',
|
||||
'border-radius': item.borderRadius + 'px',
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
const isAlways = computed(() =>
|
||||
props.data.styleType === 'inner' ? Boolean(props.data.alwaysShow) : true,
|
||||
);
|
||||
const isOpacity = computed(() =>
|
||||
props.data.styleType === 'normal'
|
||||
? false
|
||||
: props.showLeftButton
|
||||
? false
|
||||
: props.data.styleType === 'inner',
|
||||
);
|
||||
const isPlaceholder = computed(() => props.data.styleType === 'normal');
|
||||
const bgStyles = computed(() => {
|
||||
return {
|
||||
background:
|
||||
props.data.bgType === 'img' && props.data.bgImg
|
||||
? `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`
|
||||
: props.data.bgColor,
|
||||
};
|
||||
});
|
||||
|
||||
// 左侧按钮:返回上一页或首页
|
||||
function onClickLeft() {
|
||||
if (hasHistory) {
|
||||
sheep.$router.back();
|
||||
} else {
|
||||
sheep.$router.go('/pages/tabbar/index');
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧按钮:打开快捷菜单
|
||||
function onClickRight() {
|
||||
showMenuTools();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-box {
|
||||
width: 750rpx;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.nav-item {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 20rpx;
|
||||
|
||||
.inner-icon-box {
|
||||
border: 1px solid rgba(#fff, 0.4);
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
|
||||
border-radius: 30rpx;
|
||||
width: 134rpx;
|
||||
height: 56rpx;
|
||||
margin-left: 8rpx;
|
||||
|
||||
.line {
|
||||
width: 2rpx;
|
||||
height: 24rpx;
|
||||
background: #e5e5e7;
|
||||
}
|
||||
|
||||
.sicon-back {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.sicon-home {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.sicon-more {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
width: 67rpx;
|
||||
height: 56rpx;
|
||||
|
||||
&-left:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 30rpx 0px 0px 30rpx;
|
||||
}
|
||||
|
||||
&-right:hover {
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
border-radius: 0px 30rpx 30rpx 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
114
sheep/components/s-discount-list/s-discount-list.vue
Normal file
114
sheep/components/s-discount-list/s-discount-list.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<su-popup
|
||||
:show="show"
|
||||
type="bottom"
|
||||
round="20"
|
||||
@close="emits('close')"
|
||||
showClose
|
||||
backgroundColor="#f2f2f2"
|
||||
>
|
||||
<view class="model-box">
|
||||
<view class="title ss-m-t-38 ss-m-l-20 ss-m-b-40">活动优惠</view>
|
||||
<scroll-view
|
||||
class="model-content ss-m-l-20"
|
||||
scroll-y
|
||||
:scroll-with-animation="false"
|
||||
:enable-back-to-top="true"
|
||||
>
|
||||
<view v-for="(item, index) in state.orderInfo.promo_infos" :key="index">
|
||||
<view class="ss-flex ss-m-b-40 subtitle">
|
||||
<view>共{{ item.goods_ids.length }}件,</view>
|
||||
<view v-if="item.activity_type === 'full_discount'">
|
||||
满{{ item.discount_rule.full }}打{{ item.discount_rule.discount }}折,已减
|
||||
</view>
|
||||
<view v-if="item.activity_type === 'full_gift'">满赠</view>
|
||||
<view v-if="item.activity_type === 'full_reduce'">
|
||||
满{{ item.discount_rule.full }}减{{ item.discount_rule.discount }},已减
|
||||
</view>
|
||||
<view class="price-text">¥{{ item.promo_discount_money || '0.00' }}</view>
|
||||
</view>
|
||||
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
|
||||
<view class="ss-flex">
|
||||
<view v-for="i in item.goods_ids" :key="i">
|
||||
<image class="content-img" :src="sheep.$url.cdn(getGoodsImg(i))" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer ss-flex">
|
||||
<button class="confirm-btn ss-reset-button" @tap="emits('close')">确认</button>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
const props = defineProps({
|
||||
promoInfo: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
goodsList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['close']);
|
||||
const state = reactive({
|
||||
orderInfo: computed(() => props.modelValue),
|
||||
});
|
||||
const getGoodsImg = (e) => {
|
||||
let goodsImg = '';
|
||||
state.orderInfo.goods_list.forEach((i) => {
|
||||
if (e == i.goods_id) {
|
||||
goodsImg = i.goods.image;
|
||||
}
|
||||
});
|
||||
return goodsImg;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.model-box {
|
||||
height: 60vh;
|
||||
}
|
||||
.model-content {
|
||||
height: 54vh;
|
||||
}
|
||||
.modal-footer {
|
||||
width: 100%;
|
||||
height: 120rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.confirm-btn {
|
||||
width: 710rpx;
|
||||
margin-left: 20rpx;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
border-radius: 40rpx;
|
||||
color: #fff;
|
||||
}
|
||||
.content-img {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
margin-right: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
.price-text {
|
||||
color: #ff3000;
|
||||
}
|
||||
</style>
|
93
sheep/components/s-empty/s-empty.vue
Normal file
93
sheep/components/s-empty/s-empty.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<view
|
||||
class="ss-flex-col ss-col-center ss-row-center empty-box"
|
||||
:style="[{ paddingTop: paddingTop + 'rpx' }]"
|
||||
>
|
||||
<view class=""><img class="empty-icon" :src="icon" mode="widthFix"></img></view>
|
||||
<view class="empty-text ss-m-t-28 ss-m-b-40">
|
||||
<text v-if="text !== ''">{{ text }}</text>
|
||||
</view>
|
||||
<button class="ss-reset-button empty-btn" v-if="showAction" @tap="clickAction">
|
||||
{{ actionText }}
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
/**
|
||||
* 容器组件 - 装修组件的样式容器
|
||||
*/
|
||||
|
||||
const props = defineProps({
|
||||
// 图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 描述
|
||||
text: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 是否显示button
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// button 文字
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 链接
|
||||
actionUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
// 间距
|
||||
paddingTop: {
|
||||
type: String,
|
||||
default: '260',
|
||||
},
|
||||
//主题色
|
||||
buttonColor: {
|
||||
type: String,
|
||||
default: 'var(--ui-BG-Main)',
|
||||
},
|
||||
});
|
||||
|
||||
const emits = defineEmits(['clickAction']);
|
||||
|
||||
function clickAction() {
|
||||
if (props.actionUrl !== '') {
|
||||
sheep.$router.go(props.actionUrl);
|
||||
}
|
||||
emits('clickAction');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.empty-box {
|
||||
width: 100%;
|
||||
}
|
||||
.empty-icon {
|
||||
width: 240rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
width: 320rpx;
|
||||
height: 70rpx;
|
||||
border: 2rpx solid v-bind('buttonColor');
|
||||
border-radius: 35rpx;
|
||||
font-weight: 500;
|
||||
color: v-bind('buttonColor');
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
88
sheep/components/s-float-menu/s-float-menu.vue
Normal file
88
sheep/components/s-float-menu/s-float-menu.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<!-- 装修基础组件:悬浮按钮 -->
|
||||
<template>
|
||||
<!-- 模态背景:展开时显示,点击后折叠 -->
|
||||
<view class="modal-bg" v-if="fabRef?.isShow" @click="handleCollapseFab"></view>
|
||||
<!-- 悬浮按钮 -->
|
||||
<uni-fab
|
||||
ref="fabRef"
|
||||
horizontal="right"
|
||||
vertical="bottom"
|
||||
:direction="state.direction"
|
||||
:pattern="state.pattern"
|
||||
:content="state.content"
|
||||
@trigger="handleOpenLink"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
/**
|
||||
* 悬浮按钮
|
||||
*/
|
||||
|
||||
import sheep from '@/sheep';
|
||||
import { reactive, ref, unref } from 'vue';
|
||||
import { onBackPress } from '@dcloudio/uni-app';
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
}
|
||||
})
|
||||
|
||||
// 悬浮按钮配置: https://uniapp.dcloud.net.cn/component/uniui/uni-fab.html#fab-props
|
||||
const state = reactive({
|
||||
// 可选样式配置项
|
||||
pattern: [],
|
||||
// 展开菜单内容配置项
|
||||
content: [],
|
||||
// 展开菜单显示方式:horizontal-水平显示,vertical-垂直显示
|
||||
direction: '',
|
||||
});
|
||||
|
||||
// 悬浮按钮引用
|
||||
const fabRef = ref(null);
|
||||
// 按钮方向
|
||||
state.direction = props.data.direction;
|
||||
props.data?.list.forEach((item) => {
|
||||
// 按钮文字
|
||||
const text = props.data?.showText ? item.text : ''
|
||||
// 生成内容配置项
|
||||
state.content.push({ iconPath: sheep.$url.cdn(item.imgUrl), url: item.url, text });
|
||||
// 生成样式配置项
|
||||
state.pattern.push({ color: item.textColor });
|
||||
});
|
||||
|
||||
// 处理链接跳转
|
||||
function handleOpenLink(e) {
|
||||
sheep.$router.go(e.item.url);
|
||||
}
|
||||
|
||||
// 折叠
|
||||
function handleCollapseFab() {
|
||||
if (unref(fabRef)?.isShow) {
|
||||
unref(fabRef)?.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 按返回值后,折叠悬浮按钮
|
||||
onBackPress(() => {
|
||||
if (unref(fabRef)?.isShow) {
|
||||
unref(fabRef)?.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/* 模态背景 */
|
||||
.modal-bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 11;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(#000000, 0.4);
|
||||
}
|
||||
</style>
|
286
sheep/components/s-goods-card/s-goods-card.vue
Normal file
286
sheep/components/s-goods-card/s-goods-card.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<!-- 装修商品组件:商品卡片 -->
|
||||
<template>
|
||||
<!-- 商品卡片 -->
|
||||
<view>
|
||||
<!-- 布局1. 单列大图(上图,下内容)-->
|
||||
<view v-if="layoutType === LayoutTypeEnum.ONE_COL_BIG_IMG && state.goodsList.length" class="goods-sl-box">
|
||||
<view
|
||||
class="goods-box"
|
||||
v-for="item in state.goodsList"
|
||||
:key="item.id"
|
||||
:style="[{ marginBottom: data.space * 2 + 'rpx' }]"
|
||||
>
|
||||
<s-goods-column
|
||||
class=""
|
||||
size="sl"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
>
|
||||
<!-- 购买按钮 -->
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy.type === 'text' ? btnBuy.text : '' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 布局2. 双列(每一列:上图,下内容)-->
|
||||
<view
|
||||
v-if="layoutType === LayoutTypeEnum.TWO_COL && state.goodsList.length"
|
||||
class="goods-md-wrap ss-flex ss-flex-wrap ss-col-top"
|
||||
>
|
||||
<view class="goods-list-box">
|
||||
<view
|
||||
class="left-list"
|
||||
:style="[{ paddingRight: data.space + 'rpx', marginBottom: data.space + 'px' }]"
|
||||
v-for="item in state.leftGoodsList"
|
||||
:key="item.id"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-md-box"
|
||||
size="md"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
:titleWidth="330 - marginLeft - marginRight"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
@getHeight="calculateGoodsColumn($event, 'left')"
|
||||
>
|
||||
<!-- 购买按钮 -->
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy.type === 'text' ? btnBuy.text : '' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
<view class="goods-list-box">
|
||||
<view
|
||||
class="right-list"
|
||||
:style="[{ paddingLeft: data.space + 'rpx', marginBottom: data.space + 'px' }]"
|
||||
v-for="item in state.rightGoodsList"
|
||||
:key="item.id"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-md-box"
|
||||
size="md"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
:titleWidth="330 - marginLeft - marginRight"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
@getHeight="calculateGoodsColumn($event, 'right')"
|
||||
>
|
||||
<!-- 购买按钮 -->
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy.type === 'text' ? btnBuy.text : '' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 布局3. 单列小图(左图,右内容) -->
|
||||
<view v-if="layoutType === LayoutTypeEnum.ONE_COL_SMALL_IMG && state.goodsList.length" class="goods-lg-box">
|
||||
<view
|
||||
class="goods-box"
|
||||
:style="[{ marginBottom: data.space + 'px' }]"
|
||||
v-for="item in state.goodsList"
|
||||
:key="item.id"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="lg"
|
||||
:goodsFields="data.fields"
|
||||
:data="item"
|
||||
:tagStyle="data.badge"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@tap="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
>
|
||||
<!-- 购买按钮 -->
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy.type === 'text' ? btnBuy.text : '' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 商品卡片
|
||||
*/
|
||||
import { computed, reactive, onMounted } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import SpuApi from '@/sheep/api/product/spu';
|
||||
|
||||
// 布局类型
|
||||
const LayoutTypeEnum = {
|
||||
// 单列大图
|
||||
ONE_COL_BIG_IMG: 'oneColBigImg',
|
||||
// 双列
|
||||
TWO_COL: 'twoCol',
|
||||
// 单列小图
|
||||
ONE_COL_SMALL_IMG: 'oneColSmallImg',
|
||||
}
|
||||
|
||||
const state = reactive({
|
||||
goodsList: [],
|
||||
leftGoodsList: [],
|
||||
rightGoodsList: [],
|
||||
});
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
const { layoutType, btnBuy, spuIds } = props.data ?? {};
|
||||
const { marginLeft, marginRight } = props.styles ?? {};
|
||||
|
||||
// 购买按钮样式
|
||||
const buyStyle = computed(() => {
|
||||
if (btnBuy.type === 'text') {
|
||||
// 文字按钮:线性渐变背景颜色
|
||||
return {
|
||||
background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
|
||||
};
|
||||
}
|
||||
if (btnBuy.type === 'img') {
|
||||
// 图片按钮
|
||||
return {
|
||||
width: '54rpx',
|
||||
height: '54rpx',
|
||||
background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
//region 商品瀑布流布局
|
||||
// 下一个要处理的商品索引
|
||||
let count = 0;
|
||||
// 左列的高度
|
||||
let leftHeight = 0;
|
||||
// 右列的高度
|
||||
let rightHeight = 0;
|
||||
|
||||
/**
|
||||
* 计算商品在左列还是右列
|
||||
* @param height 商品的高度
|
||||
* @param where 添加到哪一列
|
||||
*/
|
||||
function calculateGoodsColumn(height = 0, where = 'left') {
|
||||
// 处理完
|
||||
if (!state.goodsList[count]) return;
|
||||
// 增加列的高度
|
||||
if (where === 'left') leftHeight += height;
|
||||
if (where === 'right') rightHeight += height;
|
||||
// 添加到矮的一列
|
||||
if (leftHeight <= rightHeight) {
|
||||
state.leftGoodsList.push(state.goodsList[count]);
|
||||
} else {
|
||||
state.rightGoodsList.push(state.goodsList[count]);
|
||||
}
|
||||
// 计数
|
||||
count++;
|
||||
}
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* 根据商品编号列表,获取商品列表
|
||||
* @param ids 商品编号列表
|
||||
* @return {Promise<undefined>} 商品列表
|
||||
*/
|
||||
async function getGoodsListByIds(ids) {
|
||||
const { data } = await SpuApi.getSpuListByIds(ids);
|
||||
return data;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 加载商品列表
|
||||
state.goodsList = await getGoodsListByIds(spuIds.join(','));
|
||||
// 只有双列布局时需要
|
||||
if (layoutType === LayoutTypeEnum.TWO_COL){
|
||||
// 分列
|
||||
calculateGoodsColumn();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-md-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.goods-list-box {
|
||||
width: 50%;
|
||||
box-sizing: border-box;
|
||||
.left-list {
|
||||
&:nth-last-child(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
.right-list {
|
||||
&:nth-last-child(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.goods-box {
|
||||
&:nth-last-of-type(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-md-box,
|
||||
.goods-sl-box,
|
||||
.goods-lg-box {
|
||||
position: relative;
|
||||
|
||||
.cart-btn {
|
||||
position: absolute;
|
||||
bottom: 18rpx;
|
||||
right: 20rpx;
|
||||
z-index: 11;
|
||||
height: 50rpx;
|
||||
line-height: 50rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
751
sheep/components/s-goods-column/s-goods-column.vue
Normal file
751
sheep/components/s-goods-column/s-goods-column.vue
Normal file
@@ -0,0 +1,751 @@
|
||||
<!-- 页面 -->
|
||||
<template>
|
||||
<view class="ss-goods-wrap">
|
||||
<!-- xs卡片:横向紧凑型,一行放两个,图片左内容右边 -->
|
||||
<view
|
||||
v-if="size === 'xs'"
|
||||
class="xs-goods-card ss-flex ss-col-stretch"
|
||||
:style="[elStyles]"
|
||||
@tap="onClick"
|
||||
>
|
||||
<view v-if="tagStyle.show" class="tag-icon-box">
|
||||
<image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
|
||||
</view>
|
||||
<image
|
||||
class="xs-img-box"
|
||||
:src="sheep.$url.cdn(data.image || data.picUrl)"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show || goodsFields.price?.show"
|
||||
class="xs-goods-content ss-flex-col ss-row-around"
|
||||
>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show"
|
||||
class="xs-goods-title ss-line-1"
|
||||
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
|
||||
>
|
||||
{{ data.title || data.name }}
|
||||
</view>
|
||||
<view
|
||||
v-if="goodsFields.price?.show"
|
||||
class="xs-goods-price font-OPPOSANS"
|
||||
:style="[{ color: goodsFields.price.color }]"
|
||||
>
|
||||
<text class="price-unit ss-font-24">{{ priceUnit }}</text>
|
||||
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- sm卡片:竖向紧凑,一行放三个,图上内容下 -->
|
||||
<view v-if="size === 'sm'" class="sm-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
|
||||
<view v-if="tagStyle.show" class="tag-icon-box">
|
||||
<image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
|
||||
</view>
|
||||
<image
|
||||
class="sm-img-box"
|
||||
:src="sheep.$url.cdn(data.image || data.picUrl)"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show || goodsFields.price?.show"
|
||||
class="sm-goods-content"
|
||||
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
|
||||
>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show"
|
||||
class="sm-goods-title ss-line-1 ss-m-b-16"
|
||||
>
|
||||
{{ data.title || data.name }}
|
||||
</view>
|
||||
<view
|
||||
v-if="goodsFields.price?.show"
|
||||
class="sm-goods-price font-OPPOSANS"
|
||||
:style="[{ color: goodsFields.price.color }]"
|
||||
>
|
||||
<text class="price-unit ss-font-24">{{ priceUnit }}</text>
|
||||
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- md卡片:竖向,一行放两个,图上内容下 -->
|
||||
<view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
|
||||
<view v-if="tagStyle.show" class="tag-icon-box">
|
||||
<image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
|
||||
</view>
|
||||
<image
|
||||
class="md-img-box"
|
||||
:src="sheep.$url.cdn(data.image || data.picUrl)"
|
||||
mode="widthFix"
|
||||
></image>
|
||||
<view
|
||||
class="md-goods-content ss-flex-col ss-row-around ss-p-b-20 ss-p-t-20 ss-p-x-16"
|
||||
:id="elId"
|
||||
>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show"
|
||||
class="md-goods-title ss-line-1"
|
||||
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
|
||||
>
|
||||
{{ data.title || data.name }}
|
||||
</view>
|
||||
<view
|
||||
v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
|
||||
class="md-goods-subtitle ss-m-t-16 ss-line-1"
|
||||
:style="[{ color: subTitleColor, background: subTitleBackground }]"
|
||||
>
|
||||
{{ data.subtitle || data.introduction }}
|
||||
</view>
|
||||
<slot name="activity">
|
||||
<view v-if="data.promos?.length" class="tag-box ss-flex-wrap ss-flex ss-col-center">
|
||||
<view
|
||||
class="activity-tag ss-m-r-10 ss-m-t-16"
|
||||
v-for="item in data.promos"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="ss-flex ss-col-bottom">
|
||||
<view
|
||||
v-if="goodsFields.price?.show"
|
||||
class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10"
|
||||
:style="[{ color: goodsFields.price.color }]"
|
||||
>
|
||||
<text class="price-unit ss-font-24">{{ priceUnit }}</text>
|
||||
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="
|
||||
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
|
||||
(data.original_price > 0 || data.marketPrice > 0)
|
||||
"
|
||||
class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
|
||||
:style="[{ color: originPriceColor }]"
|
||||
>
|
||||
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
|
||||
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="ss-m-t-16 ss-flex ss-col-center ss-flex-wrap">
|
||||
<view class="sales-text">{{ salesAndStock }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<slot name="cart">
|
||||
<view class="cart-box ss-flex ss-col-center ss-row-center">
|
||||
<image class="cart-icon" src="/static/img/shop/tabbar/category2.png" mode="" />
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- lg卡片:横向型,一行放一个,图片左内容右边 -->
|
||||
<view
|
||||
v-if="size === 'lg'"
|
||||
class="lg-goods-card ss-flex ss-col-stretch"
|
||||
:style="[elStyles]"
|
||||
@tap="onClick"
|
||||
>
|
||||
<view v-if="tagStyle.show" class="tag-icon-box">
|
||||
<image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
|
||||
</view>
|
||||
<view v-if="seckillTag" class="seckill-tag ss-flex ss-row-center"> 秒杀 </view>
|
||||
<view v-if="grouponTag" class="groupon-tag ss-flex ss-row-center">
|
||||
<view class="tag-icon">拼团</view>
|
||||
</view>
|
||||
<image
|
||||
class="lg-img-box"
|
||||
:src="sheep.$url.cdn(data.image || data.picUrl)"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="lg-goods-content ss-flex-1 ss-flex-col ss-row-between ss-p-b-10 ss-p-t-20">
|
||||
<view>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show"
|
||||
class="lg-goods-title ss-line-2"
|
||||
:style="[{ color: titleColor }]"
|
||||
>
|
||||
{{ data.title || data.name }}
|
||||
</view>
|
||||
<view
|
||||
v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
|
||||
class="lg-goods-subtitle ss-m-t-10 ss-line-1"
|
||||
:style="[{ color: subTitleColor, background: subTitleBackground }]"
|
||||
>
|
||||
{{ data.subtitle || data.introduction }}
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<slot name="activity">
|
||||
<view v-if="data.promos?.length" class="tag-box ss-flex ss-col-center">
|
||||
<view class="activity-tag ss-m-r-10" v-for="item in data.promos" :key="item.id">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<view class="ss-flex ss-col-bottom ss-m-t-10">
|
||||
<view
|
||||
v-if="goodsFields.price?.show"
|
||||
class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
|
||||
:style="[{ color: goodsFields.price.color }]"
|
||||
>
|
||||
<text class="ss-font-24">{{ priceUnit }}</text>
|
||||
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="
|
||||
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
|
||||
(data.original_price > 0 || data.marketPrice > 0)
|
||||
"
|
||||
class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
|
||||
:style="[{ color: originPriceColor }]"
|
||||
>
|
||||
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
|
||||
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ss-m-t-8 ss-flex ss-col-center ss-flex-wrap">
|
||||
<view class="sales-text">{{ salesAndStock }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<slot name="cart">
|
||||
<view class="buy-box ss-flex ss-col-center ss-row-center" v-if="buttonShow"> 去购买 </view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- sl卡片:竖向型,一行放一个,图片上内容下边 -->
|
||||
<view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
|
||||
<view v-if="tagStyle.show" class="tag-icon-box">
|
||||
<image class="tag-icon" :src="sheep.$url.cdn(tagStyle.src || tagStyle.imgUrl)"></image>
|
||||
</view>
|
||||
|
||||
<image
|
||||
class="sl-img-box"
|
||||
:src="sheep.$url.cdn(data.image || data.picUrl)"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
|
||||
<view class="sl-goods-content">
|
||||
<view>
|
||||
<view
|
||||
v-if="goodsFields.title?.show || goodsFields.name?.show"
|
||||
class="sl-goods-title ss-line-1"
|
||||
:style="[{ color: titleColor }]"
|
||||
>
|
||||
{{ data.title || data.name }}
|
||||
</view>
|
||||
<view
|
||||
v-if="goodsFields.subtitle?.show || goodsFields.introduction?.show"
|
||||
class="sl-goods-subtitle ss-m-t-16"
|
||||
:style="[{ color: subTitleColor, background: subTitleBackground }]"
|
||||
>
|
||||
{{ data.subtitle || data.introduction }}
|
||||
</view>
|
||||
</view>
|
||||
<view>
|
||||
<slot name="activity">
|
||||
<view v-if="data.promos?.length" class="tag-box ss-flex ss-col-center ss-flex-wrap">
|
||||
<view
|
||||
class="activity-tag ss-m-r-10 ss-m-t-16"
|
||||
v-for="item in data.promos"
|
||||
:key="item.id"
|
||||
>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
<view v-if="goodsFields.price?.show" class="ss-flex ss-col-bottom font-OPPOSANS">
|
||||
<view class="sl-goods-price ss-m-r-12" :style="[{ color: goodsFields.price.color }]">
|
||||
<text class="price-unit ss-font-24">{{ priceUnit }}</text>
|
||||
{{ isArray(data.price) ? fen2yuan(data.price[0]) : fen2yuan(data.price) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="
|
||||
(goodsFields.original_price?.show || goodsFields.marketPrice?.show) &&
|
||||
(data.original_price > 0 || data.marketPrice > 0)
|
||||
"
|
||||
class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
|
||||
:style="[{ color: originPriceColor }]"
|
||||
>
|
||||
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
|
||||
<view class="ss-m-l-8">{{ fen2yuan(data.marketPrice) }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ss-m-t-16 ss-flex ss-flex-wrap">
|
||||
<view class="sales-text">{{ salesAndStock }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<slot name="cart"
|
||||
><view class="buy-box ss-flex ss-col-center ss-row-center">去购买</view></slot
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 商品卡片
|
||||
*
|
||||
* @property {Array} size = [xs | sm | md | lg | sl ] - 列表数据
|
||||
* @property {String} tag - md及以上才有
|
||||
* @property {String} img - 图片
|
||||
* @property {String} background - 背景色
|
||||
* @property {String} topRadius - 上圆角
|
||||
* @property {String} bottomRadius - 下圆角
|
||||
* @property {String} title - 标题
|
||||
* @property {String} titleColor - 标题颜色
|
||||
* @property {Number} titleWidth = 0 - 标题宽度,默认0,单位rpx
|
||||
* @property {String} subTitle - 副标题
|
||||
* @property {String} subTitleColor - 副标题颜色
|
||||
* @property {String} subTitleBackground - 副标题背景
|
||||
* @property {String | Number} price - 价格
|
||||
* @property {String} priceColor - 价格颜色
|
||||
* @property {String | Number} originPrice - 原价/划线价
|
||||
* @property {String} originPriceColor - 原价颜色
|
||||
* @property {String | Number} sales - 销售数量
|
||||
* @property {String} salesColor - 销售数量颜色
|
||||
*
|
||||
* @slots activity - 活动插槽
|
||||
* @slots cart - 购物车插槽,默认包含文字,背景色,文字颜色 || 图片 || 行为
|
||||
*
|
||||
* @event {Function()} click - 点击卡片
|
||||
*
|
||||
*/
|
||||
import { computed, reactive, getCurrentInstance, onMounted, nextTick } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { fen2yuan, formatSales } from '@/sheep/hooks/useGoods';
|
||||
import { formatStock } from '@/sheep/hooks/useGoods';
|
||||
import goodsCollectVue from '@/pages/user/goods-collect.vue';
|
||||
import { isArray } from 'lodash-es';
|
||||
|
||||
// 数据
|
||||
const state = reactive({});
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
goodsFields: {
|
||||
type: [Array, Object],
|
||||
default() {
|
||||
return {
|
||||
// 商品价格
|
||||
price: { show: true },
|
||||
// 库存
|
||||
stock: { show: true },
|
||||
// 商品名称
|
||||
name: { show: true },
|
||||
// 商品介绍
|
||||
introduction: { show: true },
|
||||
// 市场价
|
||||
marketPrice: { show: true },
|
||||
// 销量
|
||||
salesCount: { show: true },
|
||||
};
|
||||
},
|
||||
},
|
||||
tagStyle: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sl',
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
topRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
bottomRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
titleWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#333',
|
||||
},
|
||||
priceColor: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
originPriceColor: {
|
||||
type: String,
|
||||
default: '#C4C4C4',
|
||||
},
|
||||
priceUnit: {
|
||||
type: String,
|
||||
default: '¥',
|
||||
},
|
||||
subTitleColor: {
|
||||
type: String,
|
||||
default: '#999999',
|
||||
},
|
||||
subTitleBackground: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
buttonShow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
seckillTag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
grouponTag: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 组件样式
|
||||
const elStyles = computed(() => {
|
||||
return {
|
||||
background: props.background,
|
||||
'border-top-left-radius': props.topRadius + 'px',
|
||||
'border-top-right-radius': props.topRadius + 'px',
|
||||
'border-bottom-left-radius': props.bottomRadius + 'px',
|
||||
'border-bottom-right-radius': props.bottomRadius + 'px',
|
||||
};
|
||||
});
|
||||
|
||||
// 格式化销量、库存信息
|
||||
const salesAndStock = computed(() => {
|
||||
let text = [];
|
||||
if (props.goodsFields.salesCount?.show) {
|
||||
text.push(formatSales(props.data.sales_show_type, props.data.salesCount));
|
||||
}
|
||||
if (props.goodsFields.stock?.show) {
|
||||
text.push(formatStock(props.data.stock_show_type, props.data.stock));
|
||||
}
|
||||
return text.join(' | ');
|
||||
});
|
||||
|
||||
// 返回事件
|
||||
const emits = defineEmits(['click', 'getHeight']);
|
||||
|
||||
const onClick = () => {
|
||||
emits('click');
|
||||
};
|
||||
|
||||
// 获取卡片实时高度
|
||||
const { proxy } = getCurrentInstance();
|
||||
const elId = `sheep_${Math.ceil(Math.random() * 10e5).toString(36)}`;
|
||||
function getGoodsPriceCardWH() {
|
||||
if (props.size === 'md') {
|
||||
const view = uni.createSelectorQuery().in(proxy);
|
||||
view.select(`#${elId}`).fields({ size: true, scrollOffset: true });
|
||||
view.exec((data) => {
|
||||
let totalHeight = 0;
|
||||
const goodsPriceCard = data[0];
|
||||
if (props.data.image_wh) {
|
||||
totalHeight =
|
||||
(goodsPriceCard.width / props.data.image_wh.w) * props.data.image_wh.h +
|
||||
goodsPriceCard.height;
|
||||
} else {
|
||||
totalHeight = goodsPriceCard.width;
|
||||
}
|
||||
emits('getHeight', totalHeight);
|
||||
});
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
getGoodsPriceCardWH();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tag-icon-box {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
.tag-icon {
|
||||
width: 72rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
}
|
||||
.seckill-tag {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
width: 68rpx;
|
||||
height: 38rpx;
|
||||
background: linear-gradient(90deg, #ff5854 0%, #ff2621 100%);
|
||||
border-radius: 10rpx 0px 10rpx 0px;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
.groupon-tag {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
width: 68rpx;
|
||||
height: 38rpx;
|
||||
background: linear-gradient(90deg, #fe832a 0%, #ff6600 100%);
|
||||
border-radius: 10rpx 0px 10rpx 0px;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
.goods-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.price-unit {
|
||||
margin-right: -4px;
|
||||
}
|
||||
.sales-text {
|
||||
display: table;
|
||||
font-size: 24rpx;
|
||||
transform: scale(0.8);
|
||||
margin-left: 0rpx;
|
||||
color: #c4c4c4;
|
||||
}
|
||||
|
||||
.activity-tag {
|
||||
font-size: 20rpx;
|
||||
color: #ff0000;
|
||||
line-height: 30rpx;
|
||||
padding: 0 10rpx;
|
||||
border: 1px solid rgba(#ff0000, 0.25);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-origin-price {
|
||||
font-size: 20rpx;
|
||||
color: #c4c4c4;
|
||||
line-height: 36rpx;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
// xs
|
||||
.xs-goods-card {
|
||||
overflow: hidden;
|
||||
// max-width: 375rpx;
|
||||
background-color: $white;
|
||||
position: relative;
|
||||
|
||||
.xs-img-box {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.xs-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xs-goods-price {
|
||||
font-size: 30rpx;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
// sm
|
||||
.sm-goods-card {
|
||||
overflow: hidden;
|
||||
// width: 223rpx;
|
||||
// width: 100%;
|
||||
background-color: $white;
|
||||
position: relative;
|
||||
|
||||
.sm-img-box {
|
||||
// width: 228rpx;
|
||||
width: 100%;
|
||||
height: 208rpx;
|
||||
}
|
||||
.sm-goods-content {
|
||||
padding: 20rpx 16rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sm-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sm-goods-price {
|
||||
font-size: 30rpx;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
// md
|
||||
.md-goods-card {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: $white;
|
||||
position: relative;
|
||||
|
||||
.md-img-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.md-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
}
|
||||
.md-goods-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.md-goods-price {
|
||||
font-size: 30rpx;
|
||||
color: $red;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
|
||||
.cart-box {
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
background: linear-gradient(90deg, #fe8900, #ff5e00);
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
bottom: 50rpx;
|
||||
right: 20rpx;
|
||||
z-index: 2;
|
||||
|
||||
.cart-icon {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lg
|
||||
.lg-goods-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: $white;
|
||||
height: 280rpx;
|
||||
|
||||
.lg-img-box {
|
||||
width: 280rpx;
|
||||
height: 280rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.lg-goods-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
// line-height: 36rpx;
|
||||
// width: 410rpx;
|
||||
}
|
||||
.lg-goods-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
// line-height: 30rpx;
|
||||
// width: 410rpx;
|
||||
}
|
||||
|
||||
.lg-goods-price {
|
||||
font-size: 30rpx;
|
||||
color: $red;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
|
||||
.buy-box {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
z-index: 2;
|
||||
width: 120rpx;
|
||||
height: 50rpx;
|
||||
background: linear-gradient(90deg, #fe8900, #ff5e00);
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
.tag-box {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// sl
|
||||
|
||||
.sl-goods-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
.sl-goods-content {
|
||||
padding: 20rpx 20rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.sl-img-box {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
}
|
||||
|
||||
.sl-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.sl-goods-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
line-height: 30rpx;
|
||||
}
|
||||
|
||||
.sl-goods-price {
|
||||
font-size: 30rpx;
|
||||
color: $red;
|
||||
line-height: 36rpx;
|
||||
}
|
||||
|
||||
.buy-box {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
z-index: 2;
|
||||
width: 148rpx;
|
||||
height: 50rpx;
|
||||
background: linear-gradient(90deg, #fe8900, #ff5e00);
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
181
sheep/components/s-goods-item/s-goods-item.vue
Normal file
181
sheep/components/s-goods-item/s-goods-item.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<view>
|
||||
<view>
|
||||
<slot name="top"></slot>
|
||||
</view>
|
||||
<view
|
||||
class="ss-order-card-warp ss-flex ss-col-stretch ss-row-between bg-white"
|
||||
:style="[{ borderRadius: radius + 'rpx', marginBottom: marginBottom + 'rpx' }]"
|
||||
>
|
||||
<view class="img-box ss-m-r-24">
|
||||
<image class="order-img" :src="sheep.$url.cdn(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view
|
||||
class="box-right ss-flex-col ss-row-between"
|
||||
:style="[{ width: titleWidth ? titleWidth + 'rpx' : '' }]"
|
||||
>
|
||||
<view class="title-text ss-line-2" v-if="title">{{ title }}</view>
|
||||
<view v-if="skuString" class="spec-text ss-m-t-8 ss-m-b-12">{{ skuString }}</view>
|
||||
<view class="groupon-box">
|
||||
<slot name="groupon"></slot>
|
||||
</view>
|
||||
<view class="ss-flex">
|
||||
<view class="ss-flex ss-col-center">
|
||||
<view
|
||||
class="price-text ss-flex ss-col-center"
|
||||
:style="[{ color: priceColor }]"
|
||||
v-if="price && Number(price) > 0"
|
||||
>
|
||||
¥{{ fen2yuan(price) }}
|
||||
</view>
|
||||
<view v-if="num" class="total-text ss-flex ss-col-center">x {{ num }}</view>
|
||||
<slot name="priceSuffix"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tool-box">
|
||||
<slot name="tool"></slot>
|
||||
</view>
|
||||
<view>
|
||||
<slot name="rightBottom"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { computed } from 'vue';
|
||||
import { fen2yuan } from '@/sheep/hooks/useGoods';
|
||||
/**
|
||||
* 订单卡片
|
||||
*
|
||||
* @property {String} img - 图片
|
||||
* @property {String} title - 标题
|
||||
* @property {Number} titleWidth = 0 - 标题宽度,默认0,单位rpx
|
||||
* @property {String} skuText - 规格
|
||||
* @property {String | Number} price - 价格
|
||||
* @property {String} priceColor - 价格颜色
|
||||
* @property {Number | String} num - 数量
|
||||
*
|
||||
*/
|
||||
const props = defineProps({
|
||||
img: {
|
||||
type: String,
|
||||
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
titleWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
skuText: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
price: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
priceColor: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
num: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
score: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
radius: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
marginBottom: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const skuString = computed(() => {
|
||||
if (!props.skuText) {
|
||||
return '';
|
||||
}
|
||||
if (typeof props.skuText === 'object') {
|
||||
return props.skuText.join(',');
|
||||
}
|
||||
return props.skuText;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.score-img {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
.ss-order-card-warp {
|
||||
padding: 20rpx;
|
||||
|
||||
.img-box {
|
||||
width: 164rpx;
|
||||
height: 164rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.order-img {
|
||||
width: 164rpx;
|
||||
height: 164rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.box-right {
|
||||
flex: 1;
|
||||
// width: 500rpx;
|
||||
// height: 164rpx;
|
||||
position: relative;
|
||||
|
||||
.tool-box {
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
bottom: -10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.spec-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: $dark-9;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
line-height: 24rpx;
|
||||
color: $dark-9;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
33
sheep/components/s-goods-scroll/s-goods-scroll.vue
Normal file
33
sheep/components/s-goods-scroll/s-goods-scroll.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<!-- 商品组 - 横向滚动商品(目前暂时没用到) -->
|
||||
<template>
|
||||
<view class="goods-scroll-box">
|
||||
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
|
||||
<view class="goods-box ss-flex">
|
||||
<view v-for="(item, index) in list" :key="index">
|
||||
<s-goods-column
|
||||
class="goods-card ss-m-l-20"
|
||||
size="sm"
|
||||
:data="item"
|
||||
:titleWidth="200 - marginLeft - marginRight"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 商品组 - 横向滚动商品
|
||||
*/
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
147
sheep/components/s-goods-shelves/s-goods-shelves.vue
Normal file
147
sheep/components/s-goods-shelves/s-goods-shelves.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<!-- 装修商品组件:商品栏 -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 布局1. 两列商品,图片左文案右 -->
|
||||
<view
|
||||
v-if="layoutType === 'twoCol'"
|
||||
class="goods-xs-box ss-flex ss-flex-wrap"
|
||||
:style="[{ margin: '-' + data.space + 'rpx' }]"
|
||||
>
|
||||
<view
|
||||
class="goods-xs-list"
|
||||
v-for="item in goodsList"
|
||||
:key="item.id"
|
||||
:style="[
|
||||
{
|
||||
padding: data.space + 'rpx',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="xs"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
:titleWidth="(454 - marginRight * 2 - data.space * 2 - marginLeft * 2) / 2"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 布局. 三列商品:图片上文案下 -->
|
||||
<view
|
||||
v-if="layoutType === 'threeCol'"
|
||||
class="goods-sm-box ss-flex ss-flex-wrap"
|
||||
:style="[{ margin: '-' + data.space + 'rpx' }]"
|
||||
>
|
||||
<view
|
||||
v-for="item in goodsList"
|
||||
:key="item.id"
|
||||
class="goods-card-box"
|
||||
:style="[
|
||||
{
|
||||
padding: data.space + 'rpx',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="sm"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 布局3. 一行商品,水平滑动 -->
|
||||
<view v-if="layoutType === 'horizSwiper'" class="">
|
||||
<scroll-view class="scroll-box goods-scroll-box" scroll-x scroll-anchoring>
|
||||
<view class="goods-box ss-flex">
|
||||
<view
|
||||
class="goods-card-box"
|
||||
v-for="item in goodsList"
|
||||
:key="item.id"
|
||||
:style="[{ marginRight: data.space * 2 + 'rpx' }]"
|
||||
>
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="sm"
|
||||
:goodsFields="data.fields"
|
||||
:tagStyle="data.badge"
|
||||
:data="item"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:titleWidth="(750 - marginRight * 2 - data.space * 4 - marginLeft * 2) / 3"
|
||||
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 商品栏
|
||||
*/
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import SpuApi from "@/sheep/api/product/spu";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
const { layoutType, spuIds } = props.data;
|
||||
let { marginLeft, marginRight } = props.styles;
|
||||
const goodsList = ref([]);
|
||||
onMounted(async () => {
|
||||
if (spuIds.length > 0) {
|
||||
let { data } = await SpuApi.getSpuListByIds(spuIds.join(','));
|
||||
goodsList.value = data;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-xs-box {
|
||||
// margin: 0 auto;
|
||||
width: 100%;
|
||||
.goods-xs-list {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-sm-box {
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
.goods-card-box {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 33.3%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
.goods-scroll-box {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
171
sheep/components/s-groupon-block/s-groupon-block.vue
Normal file
171
sheep/components/s-groupon-block/s-groupon-block.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<!-- 装修组件 - 拼团 -->
|
||||
<template>
|
||||
<view>
|
||||
<view v-if="layoutType === 'threeCol'" class="goods-sm-box ss-flex ss-flex-wrap" :style="[{ margin: '-' + data.space + 'rpx' }]">
|
||||
<view v-for="product in productList" class="goods-card-box" :key="product.id" :style="[{padding: data.space + 'rpx',},]">
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="sm"
|
||||
:goodsFields="goodsFields"
|
||||
:tagStyle="badge"
|
||||
:data="product"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/groupon', { id: props.data.activityId, })">
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 样式2 一行一个 图片左 文案右 -->
|
||||
<view class="goods-box" v-if="layoutType === 'oneCol'">
|
||||
<view class="goods-list" v-for="(product, index) in productList" :key="index" :style="[{ marginBottom: space + 'px' }]">
|
||||
<s-goods-column
|
||||
class="goods-card" size="lg"
|
||||
:grouponTag="true"
|
||||
:goodsFields="goodsFields"
|
||||
:tagStyle="badge"
|
||||
:data="product"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/groupon', { id: props.data.activityId, })">
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy?.type === 'text' ? btnBuy.text : '去拼团' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 拼团
|
||||
*/
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref
|
||||
} from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import SpuApi from "@/sheep/api/product/spu";
|
||||
import CombinationApi from "@/sheep/api/promotion/combination";
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
// 设置相关信息是否显示
|
||||
const goodsFields = reactive({
|
||||
// 商品价格
|
||||
price: { show: true },
|
||||
// 库存
|
||||
stock: { show: true },
|
||||
// 商品名称
|
||||
name: { show: true },
|
||||
// 商品介绍
|
||||
introduction: { show: true },
|
||||
// 市场价
|
||||
marketPrice: { show: true },
|
||||
// 销量
|
||||
salesCount: { show: true },
|
||||
});
|
||||
|
||||
let {
|
||||
layoutType,
|
||||
badge,
|
||||
btnBuy,
|
||||
space,
|
||||
} = props.data;
|
||||
let {
|
||||
marginLeft,
|
||||
marginRight
|
||||
} = props.styles;
|
||||
|
||||
// 购买按钮样式(暂未实现)
|
||||
const buyStyle = computed(() => {
|
||||
let btnBuy = props.data.btnBuy;
|
||||
if (btnBuy?.type === 'text') {
|
||||
return {
|
||||
background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
|
||||
};
|
||||
}
|
||||
|
||||
if (btnBuy?.type === 'img') {
|
||||
return {
|
||||
width: '54rpx',
|
||||
height: '54rpx',
|
||||
background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const productList = ref([]);
|
||||
onMounted(async () => {
|
||||
// todo:@owen 与Yudao结构不一致,待重构
|
||||
const {
|
||||
data: activity
|
||||
} = await CombinationApi.getCombinationActivity(props.data.activityId);
|
||||
const {
|
||||
data: spu
|
||||
} = await SpuApi.getSpuDetail(activity.spuId)
|
||||
// 循环活动信息,赋值拼团最低价格
|
||||
activity.products.forEach((product) => {
|
||||
spu.price = Math.min(spu.price, product.combinationPrice); // 设置 SPU 的最低价格
|
||||
});
|
||||
productList.value = [spu];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.goods-list {
|
||||
position: relative;
|
||||
|
||||
.cart-btn {
|
||||
position: absolute;
|
||||
bottom: 10rpx;
|
||||
right: 20rpx;
|
||||
z-index: 11;
|
||||
height: 50rpx;
|
||||
line-height: 50rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
&:nth-last-of-type(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-sm-box {
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.goods-card-box {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 33.3%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
46
sheep/components/s-hotzone-block/s-hotzone-block.vue
Normal file
46
sheep/components/s-hotzone-block/s-hotzone-block.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- 装修图文组件:热区 -->
|
||||
<template>
|
||||
<view class="hotzone-wrap">
|
||||
<image :src="sheep.$url.cdn(data.imgUrl)" style="width: 100%" mode="widthFix"></image>
|
||||
<view
|
||||
class="hotzone-box"
|
||||
v-for="(item, index) in data.list"
|
||||
:key="index"
|
||||
:style="[
|
||||
{
|
||||
top: `${item.top}px`,
|
||||
left: `${item.left}px`,
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
},
|
||||
]"
|
||||
@tap.stop="sheep.$router.go(item.url)"
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hotzone-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.hotzone-box {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
44
sheep/components/s-image-banner/s-image-banner.vue
Normal file
44
sheep/components/s-image-banner/s-image-banner.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<!-- 装修图文组件:图片轮播 -->
|
||||
<template>
|
||||
<su-swiper
|
||||
:list="imgList"
|
||||
:dotStyle="data.indicator === 'dot' ? 'long' : 'tag'"
|
||||
imageMode="scaleToFill"
|
||||
dotCur="bg-mask-40"
|
||||
:seizeHeight="300"
|
||||
:autoplay="data.autoplay"
|
||||
:interval="data.interval * 1000"
|
||||
:mode="data.type"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 轮播图
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const imgList = computed(() =>
|
||||
props.data.items.map((item) => {
|
||||
const src = item.type === 'img' ? item.imgUrl : item.videoUrl;
|
||||
return {
|
||||
...item,
|
||||
type: item.type === 'img' ? 'image' : 'video',
|
||||
src: sheep.$url.cdn(src),
|
||||
poster: sheep.$url.cdn(item.imgUrl),
|
||||
};
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style></style>
|
27
sheep/components/s-image-block/s-image-block.vue
Normal file
27
sheep/components/s-image-block/s-image-block.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- 装修图文组件:图片展示 -->
|
||||
<template>
|
||||
<view @tap="sheep.$router.go(data?.url)">
|
||||
<su-image :src="sheep.$url.cdn(data.imgUrl)" mode="widthFix" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 图片组件
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
110
sheep/components/s-image-cube/s-image-cube.vue
Normal file
110
sheep/components/s-image-cube/s-image-cube.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<!-- 装修图文组件:广告魔方 -->
|
||||
<template>
|
||||
<view class="ss-cube-wrap" :style="[parseAdWrap]">
|
||||
<view v-for="(item, index) in data.list" :key="index">
|
||||
<view
|
||||
class="cube-img-wrap"
|
||||
:style="[parseImgStyle(item), { margin: data.space + 'px' }]"
|
||||
@tap="sheep.$router.go(item.url)"
|
||||
>
|
||||
<image class="cube-img" :src="sheep.$url.cdn(item.imgUrl)" mode="aspectFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
/**
|
||||
/**
|
||||
* 广告魔方
|
||||
*
|
||||
* @property {Array<Object>} list - 魔方列表
|
||||
* @property {Object} styles - 组件样式
|
||||
* @property {String} background - 组件背景色
|
||||
* @property {Number} topSpace - 组件顶部间距
|
||||
* @property {Number} bottomSpace - 组件底部间距
|
||||
* @property {Number} leftSpace - 容器左间距
|
||||
* @property {Number} rightSpace - 容器右间距
|
||||
* @property {Number} imgSpace - 图片间距
|
||||
* @property {Number} imgTopRadius - 图片上圆角
|
||||
* @property {Number} imgBottomRadius - 图片下圆角
|
||||
*
|
||||
*/
|
||||
|
||||
import { computed, inject, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
// 单元格大小
|
||||
const windowWidth = sheep.$platform.device.windowWidth;
|
||||
const cell = computed(() => {
|
||||
return (
|
||||
(windowWidth -
|
||||
((props.styles.marginLeft || 0) + (props.styles.marginRight || 0) + (props.styles.padding || 0) * 2)) /
|
||||
4
|
||||
);
|
||||
});
|
||||
|
||||
//包裹容器高度
|
||||
const parseAdWrap = computed(() => {
|
||||
let heightArr = props.data.list.reduce(
|
||||
(prev, cur) => (prev.includes(cur.height + cur.top) ? prev : [...prev, cur.height + cur.top]),
|
||||
[],
|
||||
);
|
||||
let heightMax = Math.max(...heightArr);
|
||||
return {
|
||||
height: heightMax * cell.value + 'px',
|
||||
width:
|
||||
windowWidth -
|
||||
(props.data?.style?.marginLeft +
|
||||
props.data?.style?.marginRight +
|
||||
props.styles.padding * 2) *
|
||||
2 +
|
||||
'px',
|
||||
};
|
||||
});
|
||||
|
||||
// 解析图片大小位置
|
||||
const parseImgStyle = (item) => {
|
||||
let obj = {
|
||||
width: item.width * cell.value - props.data.space + 'px',
|
||||
height: item.height * cell.value - props.data.space + 'px',
|
||||
left: item.left * cell.value + 'px',
|
||||
top: item.top * cell.value + 'px',
|
||||
'border-top-left-radius': props.data.borderRadiusTop + 'px',
|
||||
'border-top-right-radius': props.data.borderRadiusTop + 'px',
|
||||
'border-bottom-left-radius': props.data.borderRadiusBottom + 'px',
|
||||
'border-bottom-right-radius': props.data.borderRadiusBottom + 'px',
|
||||
};
|
||||
return obj;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-cube-wrap {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 750rpx;
|
||||
}
|
||||
|
||||
.cube-img-wrap {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cube-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
246
sheep/components/s-layout/s-layout.vue
Normal file
246
sheep/components/s-layout/s-layout.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<view
|
||||
class="page-app"
|
||||
:class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
|
||||
>
|
||||
<view class="page-main" :style="[bgMain]">
|
||||
<!-- 顶部导航栏-情况1:默认通用顶部导航栏 -->
|
||||
<su-navbar
|
||||
v-if="navbar === 'normal'"
|
||||
:title="title"
|
||||
statusBar
|
||||
:color="color"
|
||||
:tools="tools"
|
||||
:opacityBgUi="opacityBgUi"
|
||||
@search="(e) => emits('search', e)"
|
||||
:defaultSearch="defaultSearch"
|
||||
/>
|
||||
|
||||
<!-- 顶部导航栏-情况2:装修组件导航栏-标准 -->
|
||||
<s-custom-navbar
|
||||
v-else-if="navbar === 'custom' && navbarMode === 'normal'"
|
||||
:data="navbarStyle"
|
||||
:showLeftButton="showLeftButton"
|
||||
/>
|
||||
<view class="page-body" :style="[bgBody]">
|
||||
<!-- 顶部导航栏-情况3:沉浸式头部 -->
|
||||
<su-inner-navbar v-if="navbar === 'inner'" :title="title" />
|
||||
<view
|
||||
v-if="navbar === 'inner'"
|
||||
:style="[{ paddingTop: sheep.$platform.navbar + 'px' }]"
|
||||
></view>
|
||||
|
||||
<!-- 顶部导航栏-情况4:装修组件导航栏-沉浸式 -->
|
||||
<s-custom-navbar
|
||||
v-if="navbar === 'custom' && navbarMode === 'inner'"
|
||||
:data="navbarStyle"
|
||||
:showLeftButton="showLeftButton"
|
||||
/>
|
||||
|
||||
<!-- 页面内容插槽 -->
|
||||
<slot />
|
||||
|
||||
<!-- 底部导航 -->
|
||||
<s-tabbar v-if="tabbar !== ''" :path="tabbar" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="page-modal">
|
||||
<!-- 全局授权弹窗 -->
|
||||
<s-auth-modal />
|
||||
<!-- 全局分享弹窗 -->
|
||||
<s-share-modal :shareInfo="shareInfo" />
|
||||
<!-- 全局快捷入口 -->
|
||||
<s-menu-tools />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 模板组件 - 提供页面公共组件,属性,方法
|
||||
*/
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
// #ifdef MP-WEIXIN
|
||||
import { onShareAppMessage } from '@dcloudio/uni-app';
|
||||
// #endif
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
navbar: {
|
||||
type: String,
|
||||
default: 'normal',
|
||||
},
|
||||
opacityBgUi: {
|
||||
type: String,
|
||||
default: 'bg-white',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
tools: {
|
||||
type: String,
|
||||
default: 'title',
|
||||
},
|
||||
keyword: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
navbarStyle: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
styleType: '',
|
||||
type: '',
|
||||
color: '',
|
||||
src: '',
|
||||
list: [],
|
||||
alwaysShow: 0,
|
||||
}),
|
||||
},
|
||||
bgStyle: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
src: '',
|
||||
color: 'var(--ui-BG-1)',
|
||||
}),
|
||||
},
|
||||
tabbar: {
|
||||
type: [String, Boolean],
|
||||
default: '',
|
||||
},
|
||||
onShareAppMessage: {
|
||||
type: [Boolean, Object],
|
||||
default: true,
|
||||
},
|
||||
leftWidth: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
rightWidth: {
|
||||
type: [Number, String],
|
||||
default: 100,
|
||||
},
|
||||
defaultSearch: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
//展示返回按钮
|
||||
showLeftButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['search']);
|
||||
|
||||
const sysStore = sheep.$store('sys');
|
||||
const userStore = sheep.$store('user');
|
||||
const appStore = sheep.$store('app');
|
||||
const modalStore = sheep.$store('modal');
|
||||
const sys = computed(() => sysStore);
|
||||
|
||||
// 导航栏模式(因为有自定义导航栏 需要计算)
|
||||
const navbarMode = computed(() => {
|
||||
if (props.navbar === 'normal' || props.navbarStyle.styleType === 'normal') {
|
||||
return 'normal';
|
||||
}
|
||||
return 'inner';
|
||||
});
|
||||
|
||||
// 背景1
|
||||
const bgMain = computed(() => {
|
||||
if (navbarMode.value === 'inner') {
|
||||
return {
|
||||
background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
|
||||
props.bgStyle.backgroundImage,
|
||||
)}) no-repeat top center / 100% auto`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// 背景2
|
||||
const bgBody = computed(() => {
|
||||
if (navbarMode.value === 'normal') {
|
||||
return {
|
||||
background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
|
||||
props.bgStyle.backgroundImage,
|
||||
)}) no-repeat top center / 100% auto`,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// 分享信息
|
||||
const shareInfo = computed(() => {
|
||||
if (props.onShareAppMessage === true) {
|
||||
return sheep.$platform.share.getShareInfo();
|
||||
} else {
|
||||
if (!isEmpty(props.onShareAppMessage)) {
|
||||
sheep.$platform.share.updateShareInfo(props.onShareAppMessage);
|
||||
return props.onShareAppMessage;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序分享
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
title: shareInfo.value.title,
|
||||
path: shareInfo.value.path,
|
||||
imageUrl: shareInfo.value.image,
|
||||
};
|
||||
});
|
||||
// #endif
|
||||
|
||||
onShow(() => {
|
||||
if (!isEmpty(shareInfo.value)) {
|
||||
sheep.$platform.share.updateShareInfo(shareInfo.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-app {
|
||||
position: relative;
|
||||
color: var(--ui-TC);
|
||||
background-color: var(--ui-BG-1) !important;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
.page-main {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.page-body {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.page-img {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
15
sheep/components/s-line-block/s-line-block.vue
Normal file
15
sheep/components/s-line-block/s-line-block.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<!-- 装修基础组件:分割线 -->
|
||||
<template>
|
||||
<su-subline v-bind="data"></su-subline>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
144
sheep/components/s-live-block/s-live-block.vue
Normal file
144
sheep/components/s-live-block/s-live-block.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<view>
|
||||
<view
|
||||
v-if="mode === 2 && state.liveList.length"
|
||||
class="goods-md-wrap ss-flex ss-flex-wrap ss-col-top"
|
||||
:style="[{ margin: '-' + data.space + 'rpx' }]"
|
||||
>
|
||||
<view
|
||||
:style="[
|
||||
{
|
||||
padding: data.space + 'rpx',
|
||||
},
|
||||
]"
|
||||
class="goods-list-box"
|
||||
v-for="item in state.liveList"
|
||||
:key="item.id"
|
||||
>
|
||||
<s-live-card
|
||||
class="goods-md-box"
|
||||
size="md"
|
||||
:goodsFields="goodsFields"
|
||||
:data="item"
|
||||
:titleColor="goodsFields.name?.color"
|
||||
:subTitleColor="goodsFields.anchor_name?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="goRoom(item.roomid)"
|
||||
>
|
||||
</s-live-card>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="mode === 1 && state.liveList.length" class="goods-lg-box">
|
||||
<view
|
||||
class="goods-box"
|
||||
:style="[{ marginBottom: data.space + 'px' }]"
|
||||
v-for="item in state.liveList"
|
||||
:key="item.id"
|
||||
>
|
||||
<s-live-card
|
||||
class="goods-card"
|
||||
size="sl"
|
||||
:goodsFields="goodsFields"
|
||||
:data="item"
|
||||
:titleColor="goodsFields.name?.color"
|
||||
:subTitleColor="goodsFields.anchor_name.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@tap="goRoom(item.roomid)"
|
||||
>
|
||||
</s-live-card>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
const state = reactive({
|
||||
liveList: [],
|
||||
mpLink: '',
|
||||
});
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
const { mode, goodsFields, mpliveIds } = props.data ?? {};
|
||||
const { marginLeft, marginRight } = props.styles ?? {};
|
||||
|
||||
async function getLiveListByIds(ids) {
|
||||
const { data } = await sheep.$api.app.mplive.getRoomList(ids);
|
||||
return data;
|
||||
}
|
||||
function goRoom(id) {
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: `plugin-private://wx2b03c6e691cd7370/pages/live-player-plugin?room_id=${id}`,
|
||||
});
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
confirmText: '允许',
|
||||
content: '将打开小程序访问',
|
||||
success: async function (res) {
|
||||
if (res.confirm) {
|
||||
getMpLink();
|
||||
}
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
}
|
||||
|
||||
function goMpLink() {
|
||||
// #ifdef H5
|
||||
window.location = state.mpLink;
|
||||
// #endif
|
||||
// #ifdef APP-PLUS
|
||||
plus.runtime.openURL(state.mpLink);
|
||||
// #endif
|
||||
}
|
||||
|
||||
async function getMpLink() {
|
||||
// #ifndef MP-WEIXIN
|
||||
if (state.mpLink === '') {
|
||||
const { error, data } = await sheep.$api.app.mplive.getMpLink();
|
||||
if (error === 0) {
|
||||
state.mpLink = data;
|
||||
}
|
||||
}
|
||||
goMpLink();
|
||||
// #endif
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
state.liveList = await getLiveListByIds(mpliveIds);
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.goods-list-box {
|
||||
width: 50%;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-box {
|
||||
&:nth-last-of-type(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-md-box,
|
||||
.goods-sl-box {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
234
sheep/components/s-live-card/s-live-card.vue
Normal file
234
sheep/components/s-live-card/s-live-card.vue
Normal file
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- md卡片:竖向,一行放两个,图上内容下 -->
|
||||
<view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
|
||||
<view class="icon-box ss-flex">
|
||||
<image class="icon" :src="state.liveStatus[data.status].img"></image>
|
||||
<view class="title ss-m-l-16">{{ state.liveStatus[data.status].title }}</view>
|
||||
</view>
|
||||
<img class="md-img-box" :src="sheep.$url.cdn(data.feeds_img)" referrerpolicy="no-referrer">
|
||||
<view class="md-goods-content">
|
||||
<view class="md-goods-title ss-line-1" :style="[{ color: titleColor }]">
|
||||
{{ data.name }}
|
||||
</view>
|
||||
<view class="md-goods-subtitle ss-m-t-14 ss-line-1" :style="[{ color: subTitleColor }]">
|
||||
主播:{{ data.anchor_name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- sl卡片:竖向型,一行放一个,图片上内容下边 -->
|
||||
<view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
|
||||
<view class="icon-box ss-flex">
|
||||
<image class="icon" :src="state.liveStatus[data.status].img"></image>
|
||||
<view class="title ss-m-l-16">{{ state.liveStatus[data.status].title }}</view>
|
||||
</view>
|
||||
<img class="sl-img-box" :src="sheep.$url.cdn(data.feeds_img)" referrerpolicy="no-referrer">
|
||||
<view class="sl-goods-content">
|
||||
<view class="sl-goods-title ss-line-1" :style="[{ color: titleColor }]">
|
||||
{{ data.name }}
|
||||
</view>
|
||||
<view class="sl-goods-subtitle ss-m-t-14 ss-line-1" :style="[{ color: subTitleColor }]">
|
||||
主播:{{ data.anchor_name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
/**
|
||||
* 直播卡片
|
||||
*
|
||||
* @property {String} img - 图片
|
||||
* @property {String} title - 标题
|
||||
* @property {Number} titleWidth = 0 - 标题宽度,默认0,单位rpx
|
||||
* @property {String} skuText - 规格
|
||||
* @property {String | Number} score - 积分
|
||||
* @property {String | Number} price - 价格
|
||||
* @property {String | Number} originalPrice - 单购价
|
||||
* @property {String} priceColor - 价格颜色
|
||||
* @property {Number | String} num - 数量
|
||||
*
|
||||
*/
|
||||
const props = defineProps({
|
||||
goodsFields: {
|
||||
type: [Array, Object],
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
tagStyle: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'sl',
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
topRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
bottomRadius: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
titleColor: {
|
||||
type: String,
|
||||
default: '#333',
|
||||
},
|
||||
subTitleColor: {
|
||||
type: String,
|
||||
default: '#999999',
|
||||
},
|
||||
});
|
||||
// 组件样式
|
||||
const elStyles = computed(() => {
|
||||
return {
|
||||
background: props.background,
|
||||
'border-top-left-radius': props.topRadius + 'px',
|
||||
'border-top-right-radius': props.topRadius + 'px',
|
||||
'border-bottom-left-radius': props.bottomRadius + 'px',
|
||||
'border-bottom-right-radius': props.bottomRadius + 'px',
|
||||
};
|
||||
});
|
||||
const state = reactive({
|
||||
liveStatus: {
|
||||
101: {
|
||||
img: sheep.$url.static('/static/img/shop/app/mplive/living.png'),
|
||||
title: '直播中',
|
||||
},
|
||||
102: {
|
||||
img: sheep.$url.static('/static/img/shop/app/mplive/start.png'),
|
||||
title: '未开始',
|
||||
},
|
||||
103: {
|
||||
img: sheep.$url.static('/static/img/shop/app/mplive/ended.png'),
|
||||
title: '已结束',
|
||||
},
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['click', 'getHeight']);
|
||||
const onClick = () => {
|
||||
emits('click');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// md
|
||||
.md-goods-card {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 424rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: $white;
|
||||
.icon-box {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: 10rpx;
|
||||
width: 136rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(#000000, 0.5);
|
||||
border-radius: 20rpx;
|
||||
z-index: 1;
|
||||
.icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx 0px 20rpx 20rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.md-goods-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 20rpx;
|
||||
width: 100%;
|
||||
background: linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.02) 100%);
|
||||
}
|
||||
|
||||
.md-img-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.md-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
}
|
||||
.md-goods-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
.sl-goods-card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
background-color: $white;
|
||||
.icon-box {
|
||||
position: absolute;
|
||||
left: 20rpx;
|
||||
top: 10rpx;
|
||||
width: 136rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(#000000, 0.5);
|
||||
border-radius: 20rpx;
|
||||
z-index: 1;
|
||||
.icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 20rpx 0px 20rpx 20rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
.sl-goods-content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 20rpx;
|
||||
width: 100%;
|
||||
background: linear-gradient(360deg, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.02) 100%);
|
||||
}
|
||||
|
||||
.sl-img-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.sl-goods-title {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
}
|
||||
.sl-goods-subtitle {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
</style>
|
343
sheep/components/s-menu-button/s-menu-button.vue
Normal file
343
sheep/components/s-menu-button/s-menu-button.vue
Normal file
@@ -0,0 +1,343 @@
|
||||
<!-- 装修基础组件:菜单导航(金刚区) -->
|
||||
<template>
|
||||
<!-- 包裹层 -->
|
||||
<view class="ui-swiper" :class="[props.mode, props.ui]"
|
||||
:style="[bgStyle, { height: swiperHeight + (menuList.length > 1 ? 50 : 0) + 'rpx' }]">
|
||||
<!-- 轮播 -->
|
||||
<swiper :circular="props.circular" :current="state.cur" :autoplay="props.autoplay" :interval="props.interval"
|
||||
:duration="props.duration" :style="[{ height: swiperHeight + 'rpx' }]" @change="swiperChange">
|
||||
<swiper-item v-for="(arr, index) in menuList" :key="index" :class="{ cur: state.cur == index }">
|
||||
<!-- 宫格 -->
|
||||
<view class="grid-wrap">
|
||||
<view v-for="(item, index) in arr" :key="index"
|
||||
class="grid-item ss-flex ss-flex-col ss-col-center ss-row-center"
|
||||
:style="[{ width: `${100 * (1 / data.column)}%`, height: '200rpx' }]" hover-class="ss-hover-btn"
|
||||
@tap="sheep.$router.go(item.url)">
|
||||
<view class="menu-box ss-flex ss-flex-col ss-col-center ss-row-center">
|
||||
<view v-if="item.badge.show" class="tag-box"
|
||||
:style="[{ background: item.badge.bgColor, color: item.badge.textColor }]">
|
||||
{{ item.badge.text }}
|
||||
</view>
|
||||
<image v-if="item.iconUrl" class="menu-icon" :style="[
|
||||
{
|
||||
width: props.iconSize + 'rpx',
|
||||
height: props.iconSize + 'rpx',
|
||||
},
|
||||
]" :src="sheep.$url.cdn(item.iconUrl)" mode="aspectFill"></image>
|
||||
<view v-if="data.layout === 'iconText'" class="menu-title"
|
||||
:style="[{ color: item.titleColor }]">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 指示点 -->
|
||||
<template v-if="menuList.length > 1">
|
||||
<view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle != 'tag'">
|
||||
<view class="line-box" v-for="(item, index) in menuList.length" :key="index"
|
||||
:class="[state.cur == index ? 'cur' : '', props.dotCur]"></view>
|
||||
</view>
|
||||
<view class="ui-swiper-dot" :class="props.dotStyle" v-if="props.dotStyle == 'tag'">
|
||||
<view class="ui-tag radius" :class="[props.dotCur]" style="pointer-events: none">
|
||||
<view style="transform: scale(0.7)">{{ state.cur + 1 }} / {{ menuList.length }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 轮播menu
|
||||
*
|
||||
* @property {Boolean} circular = false - 是否采用衔接滑动,即播放到末尾后重新回到开头
|
||||
* @property {Boolean} autoplay = true - 是否自动切换
|
||||
* @property {Number} interval = 5000 - 自动切换时间间隔
|
||||
* @property {Number} duration = 500 - 滑动动画时长,app-nvue不支持
|
||||
* @property {Array} list = [] - 轮播数据
|
||||
* @property {String} ui = '' - 样式class
|
||||
* @property {String} mode - 模式
|
||||
* @property {String} dotStyle - 指示点样式
|
||||
* @property {String} dotCur= 'ui-BG-Main' - 当前指示点样式,默认主题色
|
||||
* @property {String} bg - 背景
|
||||
*
|
||||
* @property {String|Number} col = 4 - 一行数量
|
||||
* @property {String|Number} row = 1 - 几行
|
||||
* @property {String} hasBorder - 是否有边框
|
||||
* @property {String} borderColor - 边框颜色
|
||||
* @property {String} background - 背景
|
||||
* @property {String} hoverClass - 按压样式类
|
||||
* @property {String} hoverStayTime - 动画时间
|
||||
*
|
||||
* @property {Array} list - 导航列表
|
||||
* @property {Number} iconSize - 图标大小
|
||||
* @property {String} color - 标题颜色
|
||||
*
|
||||
*/
|
||||
|
||||
import {
|
||||
reactive,
|
||||
computed
|
||||
} from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
cur: 0,
|
||||
});
|
||||
|
||||
// 接收参数
|
||||
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
circular: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
interval: {
|
||||
type: Number,
|
||||
default: 5000,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 500,
|
||||
},
|
||||
ui: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
//default
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
dotStyle: {
|
||||
type: String,
|
||||
default: 'long', //default long tag
|
||||
},
|
||||
dotCur: {
|
||||
type: String,
|
||||
default: 'ui-BG-Main',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 300,
|
||||
},
|
||||
// 是否有边框
|
||||
hasBorder: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 边框颜色
|
||||
borderColor: {
|
||||
type: String,
|
||||
default: 'red',
|
||||
},
|
||||
background: {
|
||||
type: String,
|
||||
default: 'blue',
|
||||
},
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: 'ss-hover-class', //'none'为没有hover效果
|
||||
},
|
||||
// 一排宫格数
|
||||
col: {
|
||||
type: [Number, String],
|
||||
default: 3,
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 80,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#000',
|
||||
},
|
||||
});
|
||||
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const {
|
||||
bgType,
|
||||
bgImg,
|
||||
bgColor
|
||||
} = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img' ? `url(${bgImg}) no-repeat top center / 100% 100%` : bgColor
|
||||
};
|
||||
});
|
||||
|
||||
// 生成数据
|
||||
const menuList = computed(() => splitData(props.data.list, props.data.row * props.data.column));
|
||||
const swiperHeight = computed(() => props.data.row * (props.data.layout === 'iconText' ? 200 : 180));
|
||||
const windowWidth = sheep.$platform.device.windowWidth;
|
||||
|
||||
// current 改变时会触发 change 事件
|
||||
const swiperChange = (e) => {
|
||||
state.cur = e.detail.current;
|
||||
};
|
||||
|
||||
// 重组数据
|
||||
const splitData = (oArr = [], length = 1) => {
|
||||
let arr = [];
|
||||
let minArr = [];
|
||||
oArr.forEach((c) => {
|
||||
if (minArr.length === length) {
|
||||
minArr = [];
|
||||
}
|
||||
if (minArr.length === 0) {
|
||||
arr.push(minArr);
|
||||
}
|
||||
minArr.push(c);
|
||||
});
|
||||
|
||||
return arr;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.grid-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-box {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transform: translate(0, 0);
|
||||
|
||||
.tag-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: -6rpx;
|
||||
font-size: 2em;
|
||||
line-height: 1;
|
||||
padding: 0.4em 0.6em 0.3em;
|
||||
transform: scale(0.4) translateX(0.5em) translatey(-0.6em);
|
||||
transform-origin: 100% 0;
|
||||
border-radius: 200rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
transform: translate(0, 0);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.ui-swiper) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
.ui-swiper-dot {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
bottom: 20rpx;
|
||||
height: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
|
||||
&.default .line-box {
|
||||
display: inline-flex;
|
||||
border-radius: 50rpx;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border: 2px solid transparent;
|
||||
margin: 0 10rpx;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&.cur {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
opacity: 1;
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
||||
&.cur::after {
|
||||
content: '';
|
||||
border-radius: 50rpx;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.long .line-box {
|
||||
display: inline-block;
|
||||
border-radius: 100rpx;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin: 0 10rpx;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
|
||||
&.cur {
|
||||
width: 24rpx;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.cur::after {}
|
||||
}
|
||||
|
||||
&.line {
|
||||
bottom: 20rpx;
|
||||
|
||||
.line-box {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
opacity: 0.3;
|
||||
position: relative;
|
||||
|
||||
&.cur {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tag {
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
104
sheep/components/s-menu-grid/s-menu-grid.vue
Normal file
104
sheep/components/s-menu-grid/s-menu-grid.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<!-- 装修基础组件:宫格导航 -->
|
||||
<template>
|
||||
<view :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<uni-grid :showBorder="Boolean(data.border)" :column="data.column">
|
||||
<uni-grid-item v-for="(item, index) in data.list" :key="index" @tap="sheep.$router.go(item.url)">
|
||||
<view class="grid-item-box ss-flex ss-flex-col ss-row-center ss-col-center">
|
||||
<view class="img-box">
|
||||
<view class="tag-box" v-if="item.badge.show"
|
||||
:style="[{ background: item.badge.bgColor, color: item.badge.textColor }]">
|
||||
{{ item.badge.text }}
|
||||
</view>
|
||||
<image class="menu-image" :src="sheep.$url.cdn(item.iconUrl)"></image>
|
||||
</view>
|
||||
|
||||
<view class="title-box ss-flex ss-flex-col ss-row-center ss-col-center">
|
||||
<view class="grid-text" :style="[{ color: item.titleColor }]">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<view class="grid-tip" :style="[{ color: item.subtitleColor }]">
|
||||
{{ item.subtitle }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-grid-item>
|
||||
</uni-grid>
|
||||
</view>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import {
|
||||
computed
|
||||
} from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const {
|
||||
bgType,
|
||||
bgImg,
|
||||
bgColor
|
||||
} = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img' ? `url(${bgImg}) no-repeat top center / 100% 100%` : bgColor
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.grid-item-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
.img-box {
|
||||
position: relative;
|
||||
|
||||
.tag-box {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 2em;
|
||||
line-height: 1;
|
||||
padding: 0.4em 0.6em 0.3em;
|
||||
transform: scale(0.4) translateX(0.5em) translatey(-0.6em);
|
||||
transform-origin: 100% 0;
|
||||
border-radius: 200rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.title-box {
|
||||
.grid-tip {
|
||||
font-size: 24rpx;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
66
sheep/components/s-menu-list/s-menu-list.vue
Normal file
66
sheep/components/s-menu-list/s-menu-list.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<!-- 装修基础组件:列表导航 -->
|
||||
<template>
|
||||
<view class="menu-list-wrap">
|
||||
<uni-list :border="true">
|
||||
<uni-list-item
|
||||
v-for="(item, index) in data.list"
|
||||
:key="index"
|
||||
showArrow
|
||||
clickable
|
||||
@tap="sheep.$router.go(item.url)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<view class="ss-flex ss-col-center">
|
||||
<image
|
||||
v-if="item.iconUrl"
|
||||
class="list-icon"
|
||||
:src="sheep.$url.cdn(item.iconUrl)"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<view
|
||||
class="title-text ss-flex ss-row-center ss-col-center ss-m-l-20"
|
||||
:style="[{ color: item.titleColor }]"
|
||||
>
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<view
|
||||
class="notice-text ss-flex ss-row-center ss-col-center"
|
||||
:style="[{ color: item.subtitleColor }]"
|
||||
>
|
||||
{{ item.subtitle }}
|
||||
</view>
|
||||
</template>
|
||||
</uni-list-item>
|
||||
</uni-list>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* cell
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.list-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.notice-text {
|
||||
}
|
||||
.menu-list-wrap {
|
||||
::v-deep .uni-list {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
118
sheep/components/s-menu-tools/s-menu-tools.vue
Normal file
118
sheep/components/s-menu-tools/s-menu-tools.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<!-- 全局 - 快捷入口 -->
|
||||
<template>
|
||||
<su-popup :show="show" type="top" round="20" backgroundColor="#F0F0F0" @close="closeMenuTools">
|
||||
<su-status-bar />
|
||||
<view class="tools-wrap ss-m-x-30 ss-m-b-16">
|
||||
<view class="title ss-m-b-34 ss-p-t-20">快捷菜单</view>
|
||||
<view class="container-list ss-flex ss-flex-wrap">
|
||||
<view class="list-item ss-m-b-24" v-for="item in list" :key="item.title">
|
||||
<view class="ss-flex-col ss-col-center">
|
||||
<button
|
||||
class="ss-reset-button list-image ss-flex ss-row-center ss-col-center"
|
||||
@tap="onClick(item)"
|
||||
>
|
||||
<image v-if="show" :src="sheep.$url.static(item.icon)" class="list-icon" />
|
||||
</button>
|
||||
<view class="list-title ss-m-t-20">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
|
||||
|
||||
const show = computed(() => sheep.$store('modal').menu);
|
||||
|
||||
function onClick(item) {
|
||||
closeMenuTools();
|
||||
if (item.url) sheep.$router.go(item.url);
|
||||
}
|
||||
|
||||
const list = [
|
||||
{
|
||||
url: '/pages/tabbar/index',
|
||||
icon: '/static/img/shop/tools/home.png',
|
||||
title: '首页',
|
||||
},
|
||||
/* {
|
||||
url: '/pages/index/search',
|
||||
icon: '/static/img/shop/tools/search.png',
|
||||
title: '搜索',
|
||||
}, */
|
||||
{
|
||||
url: '/pages/clerk/fans/list',
|
||||
icon: '/static/img/shop/tools/collect.png',
|
||||
title: '我的收藏',
|
||||
},
|
||||
{
|
||||
url: '/pages/tabbar/index?index=3',
|
||||
icon: '/static/img/shop/tools/user.png',
|
||||
title: '个人中心',
|
||||
},
|
||||
/* {
|
||||
url: '/pages/index/cart',
|
||||
icon: '/static/img/shop/tools/cart.png',
|
||||
title: '购物车',
|
||||
}, */
|
||||
/* {
|
||||
url: '/pages/user/goods-log',
|
||||
icon: '/static/img/shop/tools/browse.png',
|
||||
title: '浏览记录',
|
||||
}, */
|
||||
{
|
||||
url: '/pages/chat/index',
|
||||
icon: '/static/img/shop/tools/service.png',
|
||||
title: '客服',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tools-wrap {
|
||||
// background: #F0F0F0;
|
||||
// box-shadow: 0px 0px 28rpx 7rpx rgba(0, 0, 0, 0.13);
|
||||
// opacity: 0.98;
|
||||
// border-radius: 0 0 20rpx 20rpx;
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
width: calc(25vw - 20rpx);
|
||||
|
||||
.list-image {
|
||||
width: 104rpx;
|
||||
height: 104rpx;
|
||||
border-radius: 52rpx;
|
||||
background: var(--ui-BG);
|
||||
|
||||
.list-icon {
|
||||
width: 54rpx;
|
||||
height: 54rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.uni-popup {
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
:deep(.button-hover) {
|
||||
background: #fafafa !important;
|
||||
}
|
||||
</style>
|
38
sheep/components/s-notice-block/s-notice-block.vue
Normal file
38
sheep/components/s-notice-block/s-notice-block.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<view class="ss-flex ss-col-center notice-wrap">
|
||||
<image class="icon-img" :src="sheep.$url.cdn(data.iconUrl)" mode="heightFix"></image>
|
||||
<!-- todo:@owen 暂时只支持一个公告 -->
|
||||
<su-notice-bar
|
||||
style="flex: 1"
|
||||
:showIcon="false"
|
||||
scrollable
|
||||
single
|
||||
:text="data.contents[0].text"
|
||||
:speed="50"
|
||||
:color="data.textColor"
|
||||
@tap="sheep.$router.go(data.contents[0].url)"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 通知栏
|
||||
*
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-wrap {
|
||||
.icon-img {
|
||||
height: 56rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
132
sheep/components/s-order-card/s-order-card.vue
Normal file
132
sheep/components/s-order-card/s-order-card.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<!-- 装修用户组件:用户订单 -->
|
||||
<template>
|
||||
<view class="ss-order-menu-wrap ss-flex ss-col-center" :style="[style, { marginLeft: `${data.space}px` }]">
|
||||
<view
|
||||
class="menu-item ss-flex-1 ss-flex-col ss-row-center ss-col-center"
|
||||
v-for="item in orderMap"
|
||||
:key="item.title"
|
||||
@tap="sheep.$router.go(item.path, { type: item.value })"
|
||||
>
|
||||
<uni-badge
|
||||
class="uni-badge-left-margin"
|
||||
:text="numData.orderCount[item.count]"
|
||||
absolute="rightTop"
|
||||
size="small"
|
||||
>
|
||||
<image class="item-icon" :src="sheep.$url.static(item.icon)" mode="aspectFit" />
|
||||
</uni-badge>
|
||||
<view class="menu-title ss-m-t-28">{{ item.title }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 订单菜单组
|
||||
*/
|
||||
import sheep from '@/sheep';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const orderMap = [
|
||||
{
|
||||
title: '待付款',
|
||||
value: '1',
|
||||
icon: '/static/img/shop/order/no_pay.png',
|
||||
path: '/pages/order/list',
|
||||
type: 'unpaid',
|
||||
count: 'unpaidCount',
|
||||
},
|
||||
{
|
||||
title: '待收货',
|
||||
value: '3',
|
||||
icon: '/static/img/shop/order/no_take.png',
|
||||
path: '/pages/order/list',
|
||||
type: 'noget',
|
||||
count: 'deliveredCount',
|
||||
},
|
||||
{
|
||||
title: '待评价',
|
||||
value: '4',
|
||||
icon: '/static/img/shop/order/no_comment.png',
|
||||
path: '/pages/order/list',
|
||||
type: 'nocomment',
|
||||
count: 'uncommentedCount',
|
||||
},
|
||||
{
|
||||
title: '售后单',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/change_order.png',
|
||||
path: '/pages/order/aftersale/list',
|
||||
type: 'aftersale',
|
||||
count: 'afterSaleCount',
|
||||
},
|
||||
{
|
||||
title: '全部订单',
|
||||
value: '0',
|
||||
icon: '/static/img/shop/order/all_order.png',
|
||||
path: '/pages/order/list',
|
||||
},
|
||||
];
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
// 设置角标
|
||||
const numData = computed(() => sheep.$store('user').numData);
|
||||
// 设置背景样式
|
||||
const style = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const { bgType, bgImg, bgColor } = props.styles;
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img'
|
||||
? `url(${bgImg}) no-repeat top center / 100% 100%`
|
||||
: bgColor
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-order-menu-wrap {
|
||||
.menu-item {
|
||||
height: 160rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
.menu-title {
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
.item-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
.num-icon {
|
||||
position: absolute;
|
||||
right: 18rpx;
|
||||
top: 18rpx;
|
||||
// width: 40rpx;
|
||||
padding: 0 8rpx;
|
||||
height: 26rpx;
|
||||
background: #ff4d4f;
|
||||
border-radius: 13rpx;
|
||||
color: #fefefe;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.num {
|
||||
font-size: 24rpx;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
85
sheep/components/s-popup-image/s-popup-image.vue
Normal file
85
sheep/components/s-popup-image/s-popup-image.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<view>
|
||||
<view v-for="(item, index) in popupList" :key="index">
|
||||
<su-popup
|
||||
v-if="index === currentIndex"
|
||||
:show="item.isShow"
|
||||
type="center"
|
||||
backgroundColor="none"
|
||||
round="0"
|
||||
:showClose="true"
|
||||
:isMaskClick="false"
|
||||
@close="onClose(index)"
|
||||
>
|
||||
<view class="img-box">
|
||||
<image
|
||||
class="modal-img"
|
||||
:src="sheep.$url.cdn(item.imgUrl)"
|
||||
mode="widthFix"
|
||||
@tap.stop="onPopup(item.url)"
|
||||
/>
|
||||
</view>
|
||||
</su-popup>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { computed, ref } from 'vue';
|
||||
import { saveAdvHistory } from '@/sheep/hooks/useModal';
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
}
|
||||
})
|
||||
|
||||
// const modalStore = sheep.$store('modal');
|
||||
const modalStore = JSON.parse(uni.getStorageSync('modal-store') || '{}');
|
||||
console.log(modalStore)
|
||||
const advHistory = modalStore.advHistory || [];
|
||||
const currentIndex = ref(0);
|
||||
const popupList = computed(() => {
|
||||
const list = props.data.list || [];
|
||||
const newList = [];
|
||||
if (list.length > 0) {
|
||||
list.forEach((adv) => {
|
||||
if (adv.showType === 'once' && advHistory.includes(adv.imgUrl)) {
|
||||
adv.isShow = false;
|
||||
} else {
|
||||
adv.isShow = true;
|
||||
newList.push(adv);
|
||||
}
|
||||
|
||||
// 记录弹窗已显示过
|
||||
saveAdvHistory(adv);
|
||||
});
|
||||
}
|
||||
return newList;
|
||||
});
|
||||
|
||||
// 跳转链接
|
||||
function onPopup(path) {
|
||||
sheep.$router.go(path);
|
||||
}
|
||||
|
||||
// 关闭
|
||||
function onClose(index) {
|
||||
currentIndex.value = index + 1;
|
||||
popupList.value[index].isShow = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.img-box {
|
||||
width: 610rpx;
|
||||
// height: 800rpx;
|
||||
}
|
||||
.modal-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
40
sheep/components/s-richtext-block/s-richtext-block.vue
Normal file
40
sheep/components/s-richtext-block/s-richtext-block.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<!-- 装修营销组件:营销文章 -->
|
||||
<template>
|
||||
<view
|
||||
:style="[
|
||||
{
|
||||
marginLeft: styles.marginLeft + 'px',
|
||||
marginRight: styles.marginRight + 'px',
|
||||
marginBottom: styles.marginBottom + 'px',
|
||||
marginTop: styles.marginTop + 'px',
|
||||
padding: styles.padding + 'px',
|
||||
},
|
||||
]"
|
||||
>
|
||||
<mp-html class="richtext" :content="state.content"></mp-html>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import ArticleApi from '@/sheep/api/promotion/article';
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
content: '',
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const { data } = await ArticleApi.getArticle(props.data.id);
|
||||
state.content = data.content;
|
||||
});
|
||||
</script>
|
164
sheep/components/s-search-block/s-search-block.vue
Normal file
164
sheep/components/s-search-block/s-search-block.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<view
|
||||
class="search-content ss-flex ss-col-center ss-row-between"
|
||||
@tap="click"
|
||||
:style="[
|
||||
{
|
||||
borderRadius: radius + 'px',
|
||||
background: elBackground,
|
||||
height: height + 'px',
|
||||
width: width,
|
||||
},
|
||||
]"
|
||||
:class="[{ 'border-content': navbar }]"
|
||||
>
|
||||
<view class="ss-flex ss-col-center" v-if="navbar">
|
||||
<view class="search-icon _icon-search ss-m-l-10" :style="[{ color: props.iconColor }]"></view>
|
||||
<view class="search-input ss-flex-1 ss-line-1" :style="[{ color: fontColor, width: width }]">
|
||||
{{ placeholder }}
|
||||
</view>
|
||||
</view>
|
||||
<uni-search-bar
|
||||
v-if="!navbar"
|
||||
class="ss-flex-1"
|
||||
:radius="data.borderRadius"
|
||||
:placeholder="data.placeholder"
|
||||
cancelButton="none"
|
||||
clearButton="none"
|
||||
@confirm="onSearch"
|
||||
v-model="state.searchVal"
|
||||
/>
|
||||
<view class="keyword-link ss-flex">
|
||||
<view v-for="(item, index) in data.hotKeywords" :key="index">
|
||||
<view
|
||||
class="ss-m-r-16"
|
||||
:style="[{ color: data.textColor }]"
|
||||
@tap.stop="sheep.$router.go('/pages/goods/list', { keyword: item })"
|
||||
>{{ item }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="data.hotKeywords && data.hotKeywords.length && navbar" class="ss-flex">
|
||||
<button
|
||||
class="ss-reset-button keyword-btn"
|
||||
v-for="(item, index) in data.hotKeywords"
|
||||
:key="index"
|
||||
:style="[{ color: data.textColor, marginRight: '10rpx' }]"
|
||||
>
|
||||
{{ item }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 基础组件 - 搜索栏
|
||||
*
|
||||
* @property {String} elBackground - 输入框背景色
|
||||
* @property {String} iconColor - 图标颜色
|
||||
* @property {String} fontColor - 字体颜色
|
||||
* @property {Number} placeholder - 默认placeholder
|
||||
* @property {Number} topRadius - 组件上圆角
|
||||
* @property {Number} bottomRadius - 组件下圆角
|
||||
*
|
||||
* @slot keywords - 关键字
|
||||
* @event {Function} click - 点击组件时触发
|
||||
*/
|
||||
|
||||
import { computed, reactive } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 组件数据
|
||||
const state = reactive({
|
||||
searchVal: '',
|
||||
});
|
||||
|
||||
// 事件页面
|
||||
const emits = defineEmits(['click']);
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 输入框背景色
|
||||
elBackground: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 36,
|
||||
},
|
||||
// 图标颜色
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#b0b3bf',
|
||||
},
|
||||
// 字体颜色
|
||||
fontColor: {
|
||||
type: String,
|
||||
default: '#b0b3bf',
|
||||
},
|
||||
// placeholder
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '这是一个搜索框',
|
||||
},
|
||||
radius: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%',
|
||||
},
|
||||
navbar: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 点击
|
||||
const click = () => {
|
||||
emits('click');
|
||||
};
|
||||
|
||||
function onSearch(e) {
|
||||
if (e.value) {
|
||||
sheep.$router.go('/pages/goods/list', { keyword: e.value });
|
||||
setTimeout(() => {
|
||||
state.searchVal = '';
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.border-content {
|
||||
border: 2rpx solid #eee;
|
||||
}
|
||||
|
||||
.search-content {
|
||||
flex: 1;
|
||||
// height: 80rpx;
|
||||
position: relative;
|
||||
|
||||
.search-icon {
|
||||
font-size: 38rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.keyword-link {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
top: 18rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
182
sheep/components/s-seckill-block/s-seckill-block.vue
Normal file
182
sheep/components/s-seckill-block/s-seckill-block.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<!-- 装修组件 - 秒杀 -->
|
||||
<template>
|
||||
<view>
|
||||
<!-- 样式一:三列 - 上图下文 -->
|
||||
<view v-if="layoutType === 'threeCol'" class="goods-sm-box ss-flex ss-flex-wrap" :style="[{ margin: '-' + data.space + 'rpx' }]">
|
||||
<view v-for="product in productList" :key="product.id" class="goods-card-box" :style="[{padding: data.space + 'rpx',},]">
|
||||
<s-goods-column
|
||||
class="goods-card" size="sm"
|
||||
:goodsFields="goodsFields"
|
||||
:tagStyle="badge"
|
||||
:data="product"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/seckill', { id: props.data.activityId, })">
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 样式二:一列 - 左图右文 -->
|
||||
<view class="goods-box" v-if="layoutType === 'oneCol'">
|
||||
<view class="goods-list" v-for="(product, index) in productList" :key="index"
|
||||
:style="[{ marginBottom: space + 'px' }]">
|
||||
<s-goods-column
|
||||
class="goods-card"
|
||||
size="lg"
|
||||
:goodsFields="goodsFields"
|
||||
:seckillTag="true"
|
||||
:tagStyle="badge"
|
||||
:data="product"
|
||||
:titleColor="data.fields.name?.color"
|
||||
:subTitleColor="data.fields.introduction?.color"
|
||||
:topRadius="data.borderRadiusTop"
|
||||
:bottomRadius="data.borderRadiusBottom"
|
||||
@click="sheep.$router.go('/pages/goods/seckill', { id: props.data.activityId, })">
|
||||
<template v-slot:cart>
|
||||
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
|
||||
{{ btnBuy?.type === 'text' ? btnBuy.text : '立即秒杀' }}
|
||||
</button>
|
||||
</template>
|
||||
</s-goods-column>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 秒杀商品列表
|
||||
*
|
||||
* @property {Array} list 商品列表
|
||||
*/
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref
|
||||
} from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import SeckillApi from "@/sheep/api/promotion/seckill";
|
||||
import SpuApi from "@/sheep/api/product/spu";
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
// 设置相关信息是否显示
|
||||
const goodsFields = reactive({
|
||||
// 商品价格
|
||||
price: { show: true },
|
||||
// 库存
|
||||
stock: { show: true },
|
||||
// 商品名称
|
||||
name: { show: true },
|
||||
// 商品介绍
|
||||
introduction: { show: true },
|
||||
// 市场价
|
||||
marketPrice: { show: true },
|
||||
// 销量
|
||||
salesCount: { show: true },
|
||||
});
|
||||
|
||||
let {
|
||||
layoutType,
|
||||
badge,
|
||||
btnBuy,
|
||||
space,
|
||||
} = props.data;
|
||||
let {
|
||||
marginLeft,
|
||||
marginRight
|
||||
} = props.styles;
|
||||
|
||||
// 购买按钮样式
|
||||
const buyStyle = computed(() => {
|
||||
let btnBuy = props.data.btnBuy;
|
||||
if (btnBuy?.type === 'text') {
|
||||
return {
|
||||
background: `linear-gradient(to right, ${btnBuy.bgBeginColor}, ${btnBuy.bgEndColor})`,
|
||||
};
|
||||
}
|
||||
if (btnBuy?.type === 'img') {
|
||||
return {
|
||||
width: '54rpx',
|
||||
height: '54rpx',
|
||||
background: `url(${sheep.$url.cdn(btnBuy.imgUrl)}) no-repeat`,
|
||||
backgroundSize: '100% 100%',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 商品列表
|
||||
const productList = ref([]);
|
||||
// 查询秒杀活动商品
|
||||
onMounted(async () => {
|
||||
// todo:@owen 与Yudao结构不一致,待重构
|
||||
const {
|
||||
data: activity
|
||||
} = await SeckillApi.getSeckillActivity(props.data.activityId);
|
||||
const {
|
||||
data: spu
|
||||
} = await SpuApi.getSpuDetail(activity.spuId)
|
||||
// 循环活动信息,赋值秒杀最低价格
|
||||
activity.products.forEach((product) => {
|
||||
spu.price = Math.min(spu.price, product.seckillPrice); // 设置 SPU 的最低价格
|
||||
});
|
||||
// 将活动库存赋值给商品库存
|
||||
spu.stock = activity.stock
|
||||
// 活动总库存 - 活动库存 = 销量
|
||||
spu.salesCount = activity.totalStock - activity.stock
|
||||
productList.value = [spu];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-box {
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
position: relative;
|
||||
|
||||
&:nth-last-child(1) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.cart-btn {
|
||||
position: absolute;
|
||||
bottom: 10rpx;
|
||||
right: 20rpx;
|
||||
z-index: 11;
|
||||
height: 50rpx;
|
||||
line-height: 50rpx;
|
||||
padding: 0 20rpx;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
background: linear-gradient(90deg, #ff6600 0%, #fe832a 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.goods-sm-box {
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.goods-card-box {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
width: 33.3%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
472
sheep/components/s-select-groupon-sku/s-select-groupon-sku.vue
Normal file
472
sheep/components/s-select-groupon-sku/s-select-groupon-sku.vue
Normal file
@@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<!-- 拼团商品规格弹窗 -->
|
||||
<su-popup :show="show" round="10" @close="emits('close')">
|
||||
<!-- SKU 信息 -->
|
||||
<view class="ss-modal-box bg-white ss-flex-col">
|
||||
<view class="modal-header ss-flex ss-col-center">
|
||||
<view class="header-left ss-m-r-30">
|
||||
<image class="sku-image" :src="sheep.$url.cdn(state.selectedSku.picUrl || goodsInfo.picUrl)" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="header-right ss-flex-col ss-row-between ss-flex-1">
|
||||
<view class="goods-title ss-line-2">
|
||||
<view class="tig ss-flex ss-col-center">
|
||||
<view class="tig-icon ss-flex ss-col-center ss-row-center">
|
||||
<view class="groupon-tag">
|
||||
<image :src="sheep.$url.static('/static/img/shop/goods/groupon-tag-white.png')" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="tig-title">拼团价</view>
|
||||
</view>
|
||||
<view class="info-title">
|
||||
{{ goodsInfo.name }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
|
||||
<view class="price-text"> {{ fen2yuan(state.selectedSku.price || goodsInfo.price || state.selectedSku.marketPrice) }}</view>
|
||||
|
||||
<view class="stock-text ss-m-l-20">
|
||||
库存{{ state.selectedSku.stock || goodsInfo.stock }}件
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-content ss-flex-1">
|
||||
<scroll-view scroll-y="true" class="modal-content-scroll">
|
||||
<view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
|
||||
<view class="label-text ss-m-b-20">{{ property.name }}</view>
|
||||
<view class="ss-flex ss-col-center ss-flex-wrap">
|
||||
<button class="ss-reset-button spec-btn" v-for="value in property.values" :class="[
|
||||
{
|
||||
'checked-btn': state.currentPropertyArray[property.id] === value.id,
|
||||
},
|
||||
{
|
||||
'disabled-btn': value.disabled === true,
|
||||
},
|
||||
]" :key="value.id" :disabled="value.disabled === true" @tap="onSelectSku(property.id, value.id)">
|
||||
{{ value.name }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="buy-num-box ss-flex ss-col-center ss-row-between">
|
||||
<view class="label-text">购买数量</view>
|
||||
<su-number-box :min="1" :max="state.selectedSku.stock" :step="1"
|
||||
v-model="state.selectedSku.count" @change="onNumberChange($event)" activity="groupon" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<view class="modal-footer ss-p-y-20">
|
||||
<view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
|
||||
<view class="ss-flex">
|
||||
<button class="ss-reset-button origin-price-btn ss-flex-col">
|
||||
<view class="btn-title">{{ grouponNum + '人团' }}</view>
|
||||
</button>
|
||||
<button class="ss-reset-button btn-tox ss-flex-col" @tap="onBuy">
|
||||
<view class="btn-price">{{ fen2yuan(state.selectedSku.price * state.selectedSku.count || goodsInfo.price * state.selectedSku.count || state.selectedSku.marketPrice * state.selectedSku.count || goodsInfo.price) }}</view>
|
||||
<view v-if="grouponAction === 'create'">立即开团</view>
|
||||
<view v-else-if="grouponAction === 'join'">参与拼团</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import {convertProductPropertyList, fen2yuan} from '@/sheep/hooks/useGoods';
|
||||
|
||||
const headerBg = sheep.$url.css('/static/img/shop/goods/groupon-btn-long.png');
|
||||
const emits = defineEmits(['change', 'addCart', 'buy', 'close', 'ladder']);
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
goodsInfo: {
|
||||
type: Object,
|
||||
default () {},
|
||||
},
|
||||
grouponAction: {
|
||||
type: String,
|
||||
default: 'create',
|
||||
},
|
||||
grouponNum: {
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
const state = reactive({
|
||||
selectedSku: {}, // 选中的 SKU
|
||||
currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
|
||||
grouponNum: props.grouponNum,
|
||||
});
|
||||
|
||||
const propertyList = convertProductPropertyList(props.goodsInfo.skus);
|
||||
|
||||
// SKU 列表
|
||||
const skuList = computed(() => {
|
||||
let skuPrices = props.goodsInfo.skus;
|
||||
for (let price of skuPrices) {
|
||||
price.value_id_array = price.properties.map((item) => item.valueId)
|
||||
}
|
||||
return skuPrices;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.selectedSku,
|
||||
(newVal) => {
|
||||
emits('change', newVal);
|
||||
}, {
|
||||
immediate: true, // 立即执行
|
||||
deep: true, // 深度监听
|
||||
},
|
||||
);
|
||||
|
||||
// 输入框改变数量
|
||||
function onNumberChange(e) {
|
||||
if (e === 0) return;
|
||||
if (state.selectedSku.count === e) return;
|
||||
state.selectedSku.count = e;
|
||||
}
|
||||
|
||||
// 点击购买
|
||||
function onBuy() {
|
||||
if (!state.selectedSku.id || state.selectedSku.id <= 0) {
|
||||
sheep.$helper.toast('请选择规格');
|
||||
return;
|
||||
}
|
||||
if (state.selectedSku.stock <= 0) {
|
||||
sheep.$helper.toast('库存不足');
|
||||
return;
|
||||
}
|
||||
emits('buy', state.selectedSku);
|
||||
}
|
||||
|
||||
// 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
|
||||
function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
|
||||
let newSkus = []; // 所有可以选择的 sku 数组
|
||||
if (isChecked) {
|
||||
// 情况一:选中 property
|
||||
// 获得当前点击选中 property 的、所有可用 SKU
|
||||
for (let price of skuList.value) {
|
||||
if (price.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (price.value_id_array.indexOf(valueId) >= 0) {
|
||||
newSkus.push(price);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 情况二:取消选中 property
|
||||
// 当前所选 property 下,所有可以选择的 SKU
|
||||
newSkus = getCanUseSkuList();
|
||||
}
|
||||
|
||||
// 所有存在并且有库存未选择的 SKU 的 value 属性值 id
|
||||
let noChooseValueIds = [];
|
||||
for (let price of newSkus) {
|
||||
noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
|
||||
}
|
||||
noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
|
||||
|
||||
if (isChecked) {
|
||||
// 去除当前选中的 value 属性值 id
|
||||
let index = noChooseValueIds.indexOf(valueId);
|
||||
noChooseValueIds.splice(index, 1);
|
||||
} else {
|
||||
// 循环去除当前已选择的 value 属性值 id
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId.toString() !== '') {
|
||||
return;
|
||||
}
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
let index = noChooseValueIds.indexOf(currentPropertyId);
|
||||
if (index >= 0) {
|
||||
// currentPropertyId 存在于 noChooseValueIds
|
||||
noChooseValueIds.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前已选择的 property 数组
|
||||
let choosePropertyIds = [];
|
||||
if (!isChecked) {
|
||||
// 当前已选择的 property
|
||||
state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyIds.push(currentValueId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 当前点击选择的 property
|
||||
choosePropertyIds = [propertyId];
|
||||
}
|
||||
|
||||
for (let propertyIndex in propertyList) {
|
||||
// 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
|
||||
if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
|
||||
continue;
|
||||
}
|
||||
// 如果当前 property id 不存在于有库存的 SKU 中,则禁用
|
||||
for (let valueIndex in propertyList[propertyIndex]['values']) {
|
||||
propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
|
||||
noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当前所选属性下,获取所有有库存的 SKU 们
|
||||
function getCanUseSkuList() {
|
||||
let newSkus = [];
|
||||
for (let sku of skuList.value) {
|
||||
if (sku.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
let isOk = true;
|
||||
state.currentPropertyArray.forEach((propertyId) => {
|
||||
// propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
|
||||
if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
|
||||
isOk = false;
|
||||
}
|
||||
});
|
||||
if (isOk) {
|
||||
newSkus.push(sku);
|
||||
}
|
||||
}
|
||||
return newSkus;
|
||||
}
|
||||
|
||||
// 选择规格
|
||||
function onSelectSku(propertyId, valueId) {
|
||||
// 清空已选择
|
||||
let isChecked = true; // 选中 or 取消选中
|
||||
if (state.currentPropertyArray[propertyId] !== undefined && state.currentPropertyArray[propertyId] === valueId) {
|
||||
// 点击已被选中的,删除并填充 ''
|
||||
isChecked = false;
|
||||
state.currentPropertyArray.splice(propertyId, 1, '');
|
||||
} else {
|
||||
// 选中
|
||||
state.currentPropertyArray[propertyId] = valueId;
|
||||
}
|
||||
|
||||
// 选中的 property 大类
|
||||
let choosePropertyId = [];
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyId.push(currentPropertyId);
|
||||
}
|
||||
});
|
||||
|
||||
// 当前所选 property 下,所有可以选择的 SKU 们
|
||||
let newSkuList = getCanUseSkuList();
|
||||
|
||||
// 判断所有 property 大类是否选择完成
|
||||
if (choosePropertyId.length === propertyList.length && newSkuList.length) {
|
||||
newSkuList[0].count = state.selectedSku.count || 1;
|
||||
state.selectedSku = newSkuList[0];
|
||||
} else {
|
||||
state.selectedSku = {};
|
||||
}
|
||||
|
||||
// 改变 property 禁用状态
|
||||
changeDisabled(isChecked, propertyId, valueId);
|
||||
}
|
||||
|
||||
changeDisabled(false);
|
||||
// TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 购买
|
||||
.buy-btn {
|
||||
margin: 0 20rpx;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(90deg, #ff6000, #fe832a);
|
||||
color: #fff;
|
||||
}
|
||||
.btn-tox {
|
||||
width: 382rpx;
|
||||
height: 80rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
margin-left: -50rpx;
|
||||
background-image: v-bind(headerBg);
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
color: #ffffff;
|
||||
line-height: normal;
|
||||
border-radius: 0px 40rpx 40rpx 0px;
|
||||
|
||||
.btn-price {
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
}
|
||||
}
|
||||
}
|
||||
.origin-price-btn {
|
||||
width: 370rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(#ff5651, 0.1);
|
||||
color: #ff6000;
|
||||
border-radius: 40rpx 0px 0px 40rpx;
|
||||
line-height: normal;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
|
||||
.btn-price {
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
}
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-modal-box {
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
max-height: 1000rpx;
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
padding: 80rpx 20rpx 40rpx;
|
||||
|
||||
.sku-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 20rpx;
|
||||
font-size: 46rpx;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 42rpx;
|
||||
position: relative;
|
||||
.tig {
|
||||
border: 2rpx solid #ff6000;
|
||||
border-radius: 4rpx;
|
||||
width: 126rpx;
|
||||
height: 38rpx;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
.tig-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: #ff6000;
|
||||
margin-left: -2rpx;
|
||||
border-radius: 4rpx 0 0 4rpx;
|
||||
|
||||
.groupon-tag {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.tig-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: #ff6000;
|
||||
width: 86rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.info-title {
|
||||
text-indent: 132rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $red;
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.stock-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0 20rpx;
|
||||
|
||||
.modal-content-scroll {
|
||||
max-height: 600rpx;
|
||||
|
||||
.label-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buy-num-box {
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.spec-btn {
|
||||
height: 60rpx;
|
||||
min-width: 100rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #f4f4f4;
|
||||
border-radius: 30rpx;
|
||||
color: #434343;
|
||||
font-size: 26rpx;
|
||||
margin-right: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.checked-btn {
|
||||
background: linear-gradient(90deg, #ff6000, #fe832a);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
font-weight: 400;
|
||||
color: #c6c6c6;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
431
sheep/components/s-select-seckill-sku/s-select-seckill-sku.vue
Normal file
431
sheep/components/s-select-seckill-sku/s-select-seckill-sku.vue
Normal file
@@ -0,0 +1,431 @@
|
||||
<!-- 秒杀商品的 SKU 选择,和 s-select-sku.vue 类似 -->
|
||||
<template>
|
||||
<!-- 规格弹窗 -->
|
||||
<su-popup :show="show" round="10" @close="emits('close')">
|
||||
<!-- SKU 信息 -->
|
||||
<view class="ss-modal-box bg-white ss-flex-col">
|
||||
<view class="modal-header ss-flex ss-col-center">
|
||||
<!-- 规格图片 -->
|
||||
<view class="header-left ss-m-r-30">
|
||||
<image
|
||||
class="sku-image"
|
||||
:src="sheep.$url.cdn(state.selectedSku.picUrl || state.goodsInfo.picUrl)"
|
||||
mode="aspectFill"
|
||||
>
|
||||
</image>
|
||||
</view>
|
||||
<view class="header-right ss-flex-col ss-row-between ss-flex-1">
|
||||
<!-- 名称 -->
|
||||
<view class="goods-title ss-line-2">{{ state.goodsInfo.name }}</view>
|
||||
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
|
||||
<!-- 价格 -->
|
||||
<view class="price-text">
|
||||
{{ fen2yuan(state.selectedSku.price || state.goodsInfo.price) }}
|
||||
</view>
|
||||
<!-- 秒杀价格标签 -->
|
||||
<view class="tig ss-flex ss-col-center">
|
||||
<view class="tig-icon ss-flex ss-col-center ss-row-center">
|
||||
<text class="cicon-alarm"></text>
|
||||
</view>
|
||||
<view class="tig-title">秒杀价</view>
|
||||
</view>
|
||||
<!-- 库存 -->
|
||||
<view class="stock-text ss-m-l-20">
|
||||
库存{{ state.selectedSku.stock || state.goodsInfo.stock }}件
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-content ss-flex-1">
|
||||
<scroll-view scroll-y="true" class="modal-content-scroll">
|
||||
<view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
|
||||
<view class="label-text ss-m-b-20">{{ property.name }}</view>
|
||||
<view class="ss-flex ss-col-center ss-flex-wrap">
|
||||
<button
|
||||
class="ss-reset-button spec-btn"
|
||||
v-for="value in property.values"
|
||||
:class="[
|
||||
{
|
||||
'checked-btn': state.currentPropertyArray[property.id] === value.id,
|
||||
},
|
||||
{
|
||||
'disabled-btn': value.disabled === true,
|
||||
},
|
||||
]"
|
||||
:key="value.id"
|
||||
:disabled="value.disabled === true"
|
||||
@tap="onSelectSku(property.id, value.id)"
|
||||
>
|
||||
{{ value.name }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="buy-num-box ss-flex ss-col-center ss-row-between">
|
||||
<view class="label-text">购买数量</view>
|
||||
<su-number-box
|
||||
:min="1"
|
||||
:max="min([singleLimitCount, state.selectedSku.stock])"
|
||||
:step="1"
|
||||
v-model="state.selectedSku.count"
|
||||
@change="onBuyCountChange($event)"
|
||||
activity="seckill"
|
||||
></su-number-box>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
|
||||
<button class="ss-reset-button buy-btn" @tap="onBuy">确认</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 秒杀活动SKU选择,
|
||||
* 与s-select-sku的区别:多一个秒杀价的标签、没有加入购物车按钮、立即购买按钮叫确认、秒杀有最大购买数量限制
|
||||
* 差别不大,可以考虑合并 todo @芋艿
|
||||
*/
|
||||
// 按钮状态: active,nostock
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { convertProductPropertyList, fen2yuan } from '@/sheep/hooks/useGoods';
|
||||
import { min } from 'lodash-es';
|
||||
const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 单次限购数量
|
||||
singleLimitCount: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
});
|
||||
const state = reactive({
|
||||
goodsInfo: computed(() => props.modelValue),
|
||||
selectedSku: {},
|
||||
currentPropertyArray: [],
|
||||
});
|
||||
|
||||
const propertyList = convertProductPropertyList(state.goodsInfo.skus);
|
||||
// SKU 列表
|
||||
const skuList = computed(() => {
|
||||
let skuPrices = state.goodsInfo.skus;
|
||||
for (let price of skuPrices) {
|
||||
price.value_id_array = price.properties.map((item) => item.valueId);
|
||||
}
|
||||
return skuPrices;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.selectedSku,
|
||||
(newVal) => {
|
||||
emits('change', newVal);
|
||||
},
|
||||
{
|
||||
immediate: true, // 立即执行
|
||||
deep: true, // 深度监听
|
||||
},
|
||||
);
|
||||
|
||||
const onBuy = () => {
|
||||
if (state.selectedSku.id) {
|
||||
if (state.selectedSku.stock <= 0) {
|
||||
sheep.$helper.toast('库存不足');
|
||||
} else {
|
||||
emits('buy', state.selectedSku);
|
||||
}
|
||||
} else {
|
||||
sheep.$helper.toast('请选择规格');
|
||||
}
|
||||
};
|
||||
|
||||
// 购买数量改变
|
||||
function onBuyCountChange(buyCount) {
|
||||
if (buyCount > 0 && state.selectedSku.count !== buyCount) {
|
||||
state.selectedSku.count = buyCount;
|
||||
}
|
||||
}
|
||||
|
||||
// 改变禁用状态
|
||||
const changeDisabled = (isChecked = false, propertyId = 0, valueId = 0) => {
|
||||
let newSkus = []; // 所有可以选择的 sku 数组
|
||||
if (isChecked) {
|
||||
// 情况一:选中 property
|
||||
// 获得当前点击选中 property 的、所有可用 SKU
|
||||
for (let price of skuList.value) {
|
||||
if (price.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (price.value_id_array.indexOf(valueId) >= 0) {
|
||||
newSkus.push(price);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 情况二:取消选中 property
|
||||
// 当前所选 property 下,所有可以选择的 SKU
|
||||
newSkus = getCanUseSkuList();
|
||||
}
|
||||
|
||||
// 所有存在并且有库存未选择的 SKU 的 value 属性值 id
|
||||
let noChooseValueIds = [];
|
||||
for (let price of newSkus) {
|
||||
noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
|
||||
}
|
||||
noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
|
||||
|
||||
if (isChecked) {
|
||||
// 去除当前选中的 value 属性值 id
|
||||
let index = noChooseValueIds.indexOf(valueId);
|
||||
noChooseValueIds.splice(index, 1);
|
||||
} else {
|
||||
// 循环去除当前已选择的 value 属性值 id
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId.toString() !== '') {
|
||||
return;
|
||||
}
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
let index = noChooseValueIds.indexOf(currentPropertyId);
|
||||
if (index >= 0) {
|
||||
// currentPropertyId 存在于 noChooseValueIds
|
||||
noChooseValueIds.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前已选择的 property 数组
|
||||
let choosePropertyIds = [];
|
||||
if (!isChecked) {
|
||||
// 当前已选择的 property
|
||||
state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyIds.push(currentValueId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 当前点击选择的 property
|
||||
choosePropertyIds = [propertyId];
|
||||
}
|
||||
|
||||
for (let propertyIndex in propertyList) {
|
||||
// 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
|
||||
if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
|
||||
continue;
|
||||
}
|
||||
// 如果当前 property id 不存在于有库存的 SKU 中,则禁用
|
||||
for (let valueIndex in propertyList[propertyIndex]['values']) {
|
||||
propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
|
||||
noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取可用的(有库存的)SKU 列表
|
||||
const getCanUseSkuList = () => {
|
||||
let newSkus = [];
|
||||
for (let sku of skuList.value) {
|
||||
if (sku.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
let isOk = true;
|
||||
state.currentPropertyArray.forEach((propertyId) => {
|
||||
// propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
|
||||
if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
|
||||
isOk = false;
|
||||
}
|
||||
});
|
||||
if (isOk) {
|
||||
newSkus.push(sku);
|
||||
}
|
||||
}
|
||||
return newSkus;
|
||||
};
|
||||
|
||||
// 选择规格
|
||||
const onSelectSku = (propertyId, valueId) => {
|
||||
// 清空已选择
|
||||
let isChecked = true; // 选中 or 取消选中
|
||||
if (
|
||||
state.currentPropertyArray[propertyId] !== undefined &&
|
||||
state.currentPropertyArray[propertyId] === valueId
|
||||
) {
|
||||
// 点击已被选中的,删除并填充 ''
|
||||
isChecked = false;
|
||||
state.currentPropertyArray.splice(propertyId, 1, '');
|
||||
} else {
|
||||
// 选中
|
||||
state.currentPropertyArray[propertyId] = valueId;
|
||||
}
|
||||
|
||||
// 选中的 property 大类
|
||||
let choosePropertyId = [];
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyId.push(currentPropertyId);
|
||||
}
|
||||
});
|
||||
|
||||
// 当前所选 property 下,所有可以选择的 SKU 们
|
||||
let newSkuList = getCanUseSkuList();
|
||||
|
||||
// 判断所有 property 大类是否选择完成
|
||||
if (choosePropertyId.length === propertyList.length && newSkuList.length) {
|
||||
newSkuList[0].count = state.selectedSku.count || 1;
|
||||
state.selectedSku = newSkuList[0];
|
||||
} else {
|
||||
state.selectedSku = {};
|
||||
}
|
||||
|
||||
// 改变 property 禁用状态
|
||||
changeDisabled(isChecked, propertyId, valueId);
|
||||
};
|
||||
|
||||
changeDisabled(false);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 购买
|
||||
.buy-box {
|
||||
padding: 10rpx 20rpx;
|
||||
|
||||
.buy-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(90deg, #ff5854, #ff2621);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-modal-box {
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
max-height: 1000rpx;
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
padding: 80rpx 20rpx 40rpx;
|
||||
|
||||
.sku-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 20rpx;
|
||||
font-size: 46rpx;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $red;
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 24rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.stock-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0 20rpx;
|
||||
|
||||
.modal-content-scroll {
|
||||
max-height: 600rpx;
|
||||
|
||||
.label-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buy-num-box {
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.spec-btn {
|
||||
height: 60rpx;
|
||||
min-width: 100rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #f4f4f4;
|
||||
border-radius: 30rpx;
|
||||
color: #434343;
|
||||
font-size: 26rpx;
|
||||
margin-right: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.checked-btn {
|
||||
background: linear-gradient(90deg, #ff5854, #ff2621);
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
font-weight: 400;
|
||||
color: #c6c6c6;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tig {
|
||||
border: 2rpx solid #ff5854;
|
||||
border-radius: 4rpx;
|
||||
width: 126rpx;
|
||||
height: 38rpx;
|
||||
|
||||
.tig-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: #ff5854;
|
||||
border-radius: 4rpx 0 0 4rpx;
|
||||
|
||||
.cicon-alarm {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.tig-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
line-height: normal;
|
||||
color: #ff6000;
|
||||
width: 86rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
406
sheep/components/s-select-sku/s-select-sku.vue
Normal file
406
sheep/components/s-select-sku/s-select-sku.vue
Normal file
@@ -0,0 +1,406 @@
|
||||
<template>
|
||||
<!-- 规格弹窗 -->
|
||||
<su-popup :show="show" round="10" @close="emits('close')">
|
||||
<!-- SKU 信息 -->
|
||||
<view class="ss-modal-box bg-white ss-flex-col">
|
||||
<view class="modal-header ss-flex ss-col-center">
|
||||
<view class="header-left ss-m-r-30">
|
||||
<image class="sku-image" :src="state.selectedSku.picUrl || goodsInfo.picUrl" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="header-right ss-flex-col ss-row-between ss-flex-1">
|
||||
<view class="goods-title ss-line-2">{{ goodsInfo.name }}</view>
|
||||
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
|
||||
<view class="ss-flex">
|
||||
<view class="price-text">
|
||||
{{ fen2yuan( state.selectedSku.price || goodsInfo.price) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="stock-text ss-m-l-20">
|
||||
{{ formatStock('exact', state.selectedSku.stock || goodsInfo.stock) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 属性选择 -->
|
||||
<view class="modal-content ss-flex-1">
|
||||
<scroll-view scroll-y="true" class="modal-content-scroll" @touchmove.stop>
|
||||
<view class="sku-item ss-m-b-20" v-for="property in propertyList" :key="property.id">
|
||||
<view class="label-text ss-m-b-20">{{ property.name }}</view>
|
||||
<view class="ss-flex ss-col-center ss-flex-wrap">
|
||||
<button class="ss-reset-button spec-btn" v-for="value in property.values" :class="[
|
||||
{
|
||||
'ui-BG-Main-Gradient': state.currentPropertyArray[property.id] === value.id,
|
||||
},
|
||||
{
|
||||
'disabled-btn': value.disabled === true,
|
||||
},
|
||||
]" :key="value.id" :disabled="value.disabled === true" @tap="onSelectSku(property.id, value.id)">
|
||||
{{ value.name }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="buy-num-box ss-flex ss-col-center ss-row-between ss-m-b-40">
|
||||
<view class="label-text">购买数量</view>
|
||||
<su-number-box :min="1" :max="state.selectedSku.stock" :step="1"
|
||||
v-model="state.selectedSku.goods_num" @change="onNumberChange($event)" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 操作区 -->
|
||||
<view class="modal-footer border-top">
|
||||
<view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center">
|
||||
<button class="ss-reset-button add-btn ui-Shadow-Main" @tap="onAddCart">加入购物车</button>
|
||||
<button class="ss-reset-button buy-btn ui-Shadow-Main" @tap="onBuy">立即购买</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { formatStock, convertProductPropertyList, fen2yuan } from '@/sheep/hooks/useGoods';
|
||||
|
||||
const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
|
||||
const props = defineProps({
|
||||
goodsInfo: {
|
||||
type: Object,
|
||||
default () {},
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
selectedSku: {}, // 选中的 SKU
|
||||
currentPropertyArray: [], // 当前选中的属性,实际是个 Map。key 是 property 编号,value 是 value 编号
|
||||
});
|
||||
|
||||
const propertyList = convertProductPropertyList(props.goodsInfo.skus);
|
||||
|
||||
// SKU 列表
|
||||
const skuList = computed(() => {
|
||||
let skuPrices = props.goodsInfo.skus;
|
||||
for (let price of skuPrices) {
|
||||
price.value_id_array = price.properties.map((item) => item.valueId)
|
||||
}
|
||||
return skuPrices;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => state.selectedSku,
|
||||
(newVal) => {
|
||||
emits('change', newVal);
|
||||
}, {
|
||||
immediate: true, // 立即执行
|
||||
deep: true, // 深度监听
|
||||
},
|
||||
);
|
||||
|
||||
// 输入框改变数量
|
||||
function onNumberChange(e) {
|
||||
if (e === 0) return;
|
||||
if (state.selectedSku.goods_num === e) return;
|
||||
state.selectedSku.goods_num = e;
|
||||
}
|
||||
|
||||
// 加入购物车
|
||||
function onAddCart() {
|
||||
if (state.selectedSku.id <= 0) {
|
||||
sheep.$helper.toast('请选择规格');
|
||||
return;
|
||||
}
|
||||
if (state.selectedSku.stock <= 0) {
|
||||
sheep.$helper.toast('库存不足');
|
||||
return;
|
||||
}
|
||||
|
||||
emits('addCart', state.selectedSku);
|
||||
}
|
||||
|
||||
// 立即购买
|
||||
function onBuy() {
|
||||
if (state.selectedSku.id <= 0) {
|
||||
sheep.$helper.toast('请选择规格');
|
||||
return;
|
||||
}
|
||||
if (state.selectedSku.stock <= 0) {
|
||||
sheep.$helper.toast('库存不足');
|
||||
return;
|
||||
}
|
||||
emits('buy', state.selectedSku);
|
||||
}
|
||||
|
||||
// 改变禁用状态:计算每个 property 属性值的按钮,是否禁用
|
||||
function changeDisabled(isChecked = false, propertyId = 0, valueId = 0) {
|
||||
let newSkus = []; // 所有可以选择的 sku 数组
|
||||
if (isChecked) {
|
||||
// 情况一:选中 property
|
||||
// 获得当前点击选中 property 的、所有可用 SKU
|
||||
for (let price of skuList.value) {
|
||||
if (price.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (price.value_id_array.indexOf(valueId) >= 0) {
|
||||
newSkus.push(price);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 情况二:取消选中 property
|
||||
// 当前所选 property 下,所有可以选择的 SKU
|
||||
newSkus = getCanUseSkuList();
|
||||
}
|
||||
|
||||
// 所有存在并且有库存未选择的 SKU 的 value 属性值 id
|
||||
let noChooseValueIds = [];
|
||||
for (let price of newSkus) {
|
||||
noChooseValueIds = noChooseValueIds.concat(price.value_id_array);
|
||||
}
|
||||
noChooseValueIds = Array.from(new Set(noChooseValueIds)); // 去重
|
||||
|
||||
if (isChecked) {
|
||||
// 去除当前选中的 value 属性值 id
|
||||
let index = noChooseValueIds.indexOf(valueId);
|
||||
noChooseValueIds.splice(index, 1);
|
||||
} else {
|
||||
// 循环去除当前已选择的 value 属性值 id
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId.toString() !== '') {
|
||||
return;
|
||||
}
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
let index = noChooseValueIds.indexOf(currentPropertyId);
|
||||
if (index >= 0) {
|
||||
// currentPropertyId 存在于 noChooseValueIds
|
||||
noChooseValueIds.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 当前已选择的 property 数组
|
||||
let choosePropertyIds = [];
|
||||
if (!isChecked) {
|
||||
// 当前已选择的 property
|
||||
state.currentPropertyArray.forEach((currentPropertyId, currentValueId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyIds.push(currentValueId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 当前点击选择的 property
|
||||
choosePropertyIds = [propertyId];
|
||||
}
|
||||
|
||||
for (let propertyIndex in propertyList) {
|
||||
// 当前点击的 property、或者取消选择时候,已选中的 property 不进行处理
|
||||
if (choosePropertyIds.indexOf(propertyList[propertyIndex]['id']) >= 0) {
|
||||
continue;
|
||||
}
|
||||
// 如果当前 property id 不存在于有库存的 SKU 中,则禁用
|
||||
for (let valueIndex in propertyList[propertyIndex]['values']) {
|
||||
propertyList[propertyIndex]['values'][valueIndex]['disabled'] =
|
||||
noChooseValueIds.indexOf(propertyList[propertyIndex]['values'][valueIndex]['id']) < 0; // true 禁用 or false 不禁用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 当前所选属性下,获取所有有库存的 SKU 们
|
||||
function getCanUseSkuList() {
|
||||
let newSkus = [];
|
||||
for (let sku of skuList.value) {
|
||||
if (sku.stock <= 0) {
|
||||
continue;
|
||||
}
|
||||
let isOk = true;
|
||||
state.currentPropertyArray.forEach((propertyId) => {
|
||||
// propertyId 不为空,并且,这个 条 sku 没有被选中,则排除
|
||||
if (propertyId.toString() !== '' && sku.value_id_array.indexOf(propertyId) < 0) {
|
||||
isOk = false;
|
||||
}
|
||||
});
|
||||
if (isOk) {
|
||||
newSkus.push(sku);
|
||||
}
|
||||
}
|
||||
return newSkus;
|
||||
}
|
||||
|
||||
// 选择规格
|
||||
function onSelectSku(propertyId, valueId) {
|
||||
// 清空已选择
|
||||
let isChecked = true; // 选中 or 取消选中
|
||||
if (state.currentPropertyArray[propertyId] !== undefined && state.currentPropertyArray[propertyId] === valueId) {
|
||||
// 点击已被选中的,删除并填充 ''
|
||||
isChecked = false;
|
||||
state.currentPropertyArray.splice(propertyId, 1, '');
|
||||
} else {
|
||||
// 选中
|
||||
state.currentPropertyArray[propertyId] = valueId;
|
||||
}
|
||||
|
||||
// 选中的 property 大类
|
||||
let choosePropertyId = [];
|
||||
state.currentPropertyArray.forEach((currentPropertyId) => {
|
||||
if (currentPropertyId !== '') {
|
||||
// currentPropertyId 为空是反选 填充的
|
||||
choosePropertyId.push(currentPropertyId);
|
||||
}
|
||||
});
|
||||
|
||||
// 当前所选 property 下,所有可以选择的 SKU 们
|
||||
let newSkuList = getCanUseSkuList();
|
||||
|
||||
// 判断所有 property 大类是否选择完成
|
||||
if (choosePropertyId.length === propertyList.length && newSkuList.length) {
|
||||
newSkuList[0].goods_num = state.selectedSku.goods_num || 1;
|
||||
state.selectedSku = newSkuList[0];
|
||||
} else {
|
||||
state.selectedSku = {};
|
||||
}
|
||||
|
||||
// 改变 property 禁用状态
|
||||
changeDisabled(isChecked, propertyId, valueId);
|
||||
}
|
||||
|
||||
changeDisabled(false);
|
||||
// TODO 芋艿:待讨论的优化点:1)单规格,要不要默认选中;2)默认要不要选中第一个规格
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 购买
|
||||
.buy-box {
|
||||
padding: 10rpx 0;
|
||||
|
||||
.add-btn {
|
||||
width: 356rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx 0 0 40rpx;
|
||||
background-color: var(--ui-BG-Main-light);
|
||||
color: var(--ui-BG-Main);
|
||||
}
|
||||
|
||||
.buy-btn {
|
||||
width: 356rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 0 40rpx 40rpx 0;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.score-btn {
|
||||
width: 100%;
|
||||
margin: 0 20rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.ss-modal-box {
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
max-height: 1000rpx;
|
||||
|
||||
.modal-header {
|
||||
position: relative;
|
||||
padding: 80rpx 20rpx 40rpx;
|
||||
|
||||
.sku-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 10rpx;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
height: 160rpx;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 20rpx;
|
||||
font-size: 46rpx;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.goods-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.score-img {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $red;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $red;
|
||||
font-family: OPPOSANS;
|
||||
|
||||
&::before {
|
||||
content: '¥';
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
|
||||
.stock-text {
|
||||
font-size: 26rpx;
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0 20rpx;
|
||||
|
||||
.modal-content-scroll {
|
||||
max-height: 600rpx;
|
||||
|
||||
.label-text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.buy-num-box {
|
||||
height: 100rpx;
|
||||
}
|
||||
|
||||
.spec-btn {
|
||||
height: 60rpx;
|
||||
min-width: 100rpx;
|
||||
padding: 0 30rpx;
|
||||
background: #f4f4f4;
|
||||
border-radius: 30rpx;
|
||||
color: #434343;
|
||||
font-size: 26rpx;
|
||||
margin-right: 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
font-weight: 400;
|
||||
color: #c6c6c6;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
168
sheep/components/s-share-modal/canvas-poster/index.vue
Normal file
168
sheep/components/s-share-modal/canvas-poster/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<!-- 海报弹窗 -->
|
||||
<template>
|
||||
<su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
|
||||
<view class="ss-flex-col ss-col-center ss-row-center">
|
||||
<image
|
||||
v-if="!!painterImageUrl"
|
||||
class="poster-img"
|
||||
:src="painterImageUrl"
|
||||
:style="{
|
||||
height: poster.css.height+ 'px',
|
||||
width: poster.css.width + 'px',
|
||||
}"
|
||||
:show-menu-by-longpress="true"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
|
||||
v-if="!!painterImageUrl"
|
||||
>
|
||||
<button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
|
||||
<button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
|
||||
{{
|
||||
['wechatOfficialAccount', 'H5'].includes(sheep.$platform.name)
|
||||
? '长按图片保存'
|
||||
: '保存图片'
|
||||
}}
|
||||
</button>
|
||||
</view>
|
||||
<!-- 海报画板:默认隐藏只用来生成海报。生成方式为主动调用 -->
|
||||
<l-painter
|
||||
isCanvasToTempFilePath
|
||||
pathType="url"
|
||||
@success="setPainterImageUrl"
|
||||
hidden
|
||||
ref="painterRef"
|
||||
/>
|
||||
</su-popup>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 海报生成和展示
|
||||
* 提示:小程序码默认跳转首页,由首页进行 spm 参数解析后跳转到对应的分享页面
|
||||
* @description 用于生成分享海报,如:分享商品海报。
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=2389
|
||||
* @property {Boolean} show 弹出层控制
|
||||
* @property {Object} shareInfo 分享信息
|
||||
*/
|
||||
import { reactive, ref, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shareInfo: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const poster = reactive({
|
||||
css: {
|
||||
// 根节点若无尺寸,自动获取父级节点
|
||||
width: sheep.$platform.device.windowWidth * 0.9,
|
||||
height: 600,
|
||||
},
|
||||
views: [],
|
||||
});
|
||||
|
||||
const emits = defineEmits(['success', 'close']);
|
||||
|
||||
const onClosePoster = () => {
|
||||
emits('close');
|
||||
};
|
||||
|
||||
const painterRef = ref(); // 海报画板
|
||||
const painterImageUrl = ref(); // 海报 url
|
||||
// 渲染海报
|
||||
const renderPoster = async () => {
|
||||
await painterRef.value.render(unref(poster));
|
||||
};
|
||||
// 获得生成的图片
|
||||
const setPainterImageUrl = (path) => {
|
||||
painterImageUrl.value = path;
|
||||
};
|
||||
// 保存海报图片
|
||||
const onSavePoster = () => {
|
||||
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
|
||||
sheep.$helper.toast('请长按图片保存');
|
||||
return;
|
||||
}
|
||||
|
||||
// 非H5 保存到相册
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: painterImageUrl.value,
|
||||
success: (res) => {
|
||||
onClosePoster();
|
||||
sheep.$helper.toast('保存成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
sheep.$helper.toast('保存失败');
|
||||
console.log('图片保存失败:', err);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 获得海报数据
|
||||
async function getPoster() {
|
||||
painterImageUrl.value = undefined
|
||||
poster.views = await getPosterData({
|
||||
width: poster.css.width,
|
||||
shareInfo: props.shareInfo,
|
||||
});
|
||||
await renderPoster();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getPoster,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster-title {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
// 分享海报
|
||||
.poster-btn-box {
|
||||
width: 600rpx;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -80rpx;
|
||||
|
||||
.cancel-btn {
|
||||
width: 240rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
background: $white;
|
||||
border-radius: 35rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: $dark-9;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 240rpx;
|
||||
height: 70rpx;
|
||||
line-height: 70rpx;
|
||||
border-radius: 35rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-img {
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
</style>
|
125
sheep/components/s-share-modal/canvas-poster/poster/goods.js
Normal file
125
sheep/components/s-share-modal/canvas-poster/poster/goods.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import sheep from '@/sheep';
|
||||
import { formatImageUrlProtocol, getWxaQrcode } from './index';
|
||||
|
||||
const goods = async (poster) => {
|
||||
const width = poster.width;
|
||||
const userInfo = sheep.$store('user').userInfo;
|
||||
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
|
||||
return [
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
|
||||
css: {
|
||||
width,
|
||||
position: 'fixed',
|
||||
'object-fit': 'contain',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: userInfo.nickname,
|
||||
css: {
|
||||
color: '#333',
|
||||
fontSize: 16,
|
||||
fontFamily: 'sans-serif',
|
||||
position: 'fixed',
|
||||
top: width * 0.06,
|
||||
left: width * 0.22,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.04,
|
||||
top: width * 0.04,
|
||||
width: width * 0.14,
|
||||
height: width * 0.14,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.03,
|
||||
top: width * 0.21,
|
||||
width: width * 0.94,
|
||||
height: width * 0.94,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: poster.shareInfo.poster.title,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.04,
|
||||
top: width * 1.18,
|
||||
color: '#333',
|
||||
fontSize: 14,
|
||||
lineHeight: 15,
|
||||
maxWidth: width * 0.91,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '¥' + poster.shareInfo.poster.price,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.04,
|
||||
top: width * 1.31,
|
||||
fontSize: 20,
|
||||
fontFamily: 'OPPOSANS',
|
||||
color: '#333',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text:
|
||||
poster.shareInfo.poster.original_price > 0
|
||||
? '¥' + poster.shareInfo.poster.original_price
|
||||
: '',
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.3,
|
||||
top: width * 1.33,
|
||||
color: '#999',
|
||||
fontSize: 10,
|
||||
fontFamily: 'OPPOSANS',
|
||||
textDecoration: 'line-through',
|
||||
},
|
||||
},
|
||||
// #ifndef MP-WEIXIN
|
||||
{
|
||||
type: 'qrcode',
|
||||
text: poster.shareInfo.link,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.75,
|
||||
top: width * 1.3,
|
||||
width: width * 0.2,
|
||||
height: width * 0.2,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
{
|
||||
type: 'image',
|
||||
src: wxa_qrcode,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.75,
|
||||
top: width * 1.3,
|
||||
width: width * 0.2,
|
||||
height: width * 0.2,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
];
|
||||
};
|
||||
|
||||
export default goods;
|
122
sheep/components/s-share-modal/canvas-poster/poster/groupon.js
Normal file
122
sheep/components/s-share-modal/canvas-poster/poster/groupon.js
Normal file
@@ -0,0 +1,122 @@
|
||||
import sheep from '@/sheep';
|
||||
import { formatImageUrlProtocol, getWxaQrcode } from './index';
|
||||
|
||||
const groupon = async (poster) => {
|
||||
const width = poster.width;
|
||||
const userInfo = sheep.$store('user').userInfo;
|
||||
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
|
||||
return [
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.groupon_bg)),
|
||||
css: {
|
||||
width,
|
||||
position: 'fixed',
|
||||
'object-fit': 'contain',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: userInfo.nickname,
|
||||
css: {
|
||||
color: '#333',
|
||||
fontSize: 16,
|
||||
fontFamily: 'sans-serif',
|
||||
position: 'fixed',
|
||||
top: width * 0.06,
|
||||
left: width * 0.22,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.04,
|
||||
top: width * 0.04,
|
||||
width: width * 0.14,
|
||||
height: width * 0.14,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.03,
|
||||
top: width * 0.21,
|
||||
width: width * 0.94,
|
||||
height: width * 0.94,
|
||||
borderRadius: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: poster.shareInfo.poster.title,
|
||||
css: {
|
||||
color: '#333',
|
||||
fontSize: 14,
|
||||
position: 'fixed',
|
||||
top: width * 1.18,
|
||||
left: width * 0.04,
|
||||
maxWidth: width * 0.91,
|
||||
lineHeight: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '¥' + poster.shareInfo.poster.price,
|
||||
css: {
|
||||
color: '#ff0000',
|
||||
fontSize: 20,
|
||||
fontFamily: 'OPPOSANS',
|
||||
position: 'fixed',
|
||||
top: width * 1.3,
|
||||
left: width * 0.04,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '2人团',
|
||||
css: {
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
fontFamily: 'OPPOSANS',
|
||||
position: 'fixed',
|
||||
left: width * 0.84,
|
||||
top: width * 1.3,
|
||||
},
|
||||
},
|
||||
// #ifndef MP-WEIXIN
|
||||
{
|
||||
type: 'qrcode',
|
||||
text: poster.shareInfo.link,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.5,
|
||||
top: width * 1.3,
|
||||
width: width * 0.2,
|
||||
height: width * 0.2,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
{
|
||||
type: 'image',
|
||||
src: wxa_qrcode,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.75,
|
||||
top: width * 1.3,
|
||||
width: width * 0.2,
|
||||
height: width * 0.2,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
];
|
||||
};
|
||||
|
||||
export default groupon;
|
39
sheep/components/s-share-modal/canvas-poster/poster/index.js
Normal file
39
sheep/components/s-share-modal/canvas-poster/poster/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import user from './user';
|
||||
import goods from './goods';
|
||||
import groupon from './groupon';
|
||||
import SocialApi from '@/sheep/api/member/social';
|
||||
|
||||
export function getPosterData(options) {
|
||||
switch (options.shareInfo.poster.type) {
|
||||
case 'user':
|
||||
return user(options);
|
||||
case 'goods':
|
||||
return goods(options);
|
||||
case 'groupon':
|
||||
return groupon(options);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatImageUrlProtocol(url) {
|
||||
// #ifdef H5
|
||||
// H5平台 https协议下需要转换
|
||||
if (window.location.protocol === 'https:' && url.indexOf('http:') === 0) {
|
||||
url = url.replace('http:', 'https:');
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
// 小程序平台 需要强制转换为https协议
|
||||
if (url.indexOf('http:') === 0) {
|
||||
url = url.replace('http:', 'https:');
|
||||
}
|
||||
// #endif
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
// 获得微信小程序码 (Base64 image)
|
||||
export async function getWxaQrcode(path, query) {
|
||||
const res = await SocialApi.getWxaQrcode(path, query);
|
||||
return 'data:image/png;base64,' + res.data;
|
||||
}
|
74
sheep/components/s-share-modal/canvas-poster/poster/user.js
Normal file
74
sheep/components/s-share-modal/canvas-poster/poster/user.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import sheep from '@/sheep';
|
||||
import { formatImageUrlProtocol, getWxaQrcode } from './index';
|
||||
|
||||
const user = async (poster) => {
|
||||
const width = poster.width;
|
||||
const userInfo = sheep.$store('user').userInfo;
|
||||
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
|
||||
return [
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.user_bg)),
|
||||
css: {
|
||||
width,
|
||||
position: 'fixed',
|
||||
'object-fit': 'contain',
|
||||
top: '0',
|
||||
left: '0',
|
||||
zIndex: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: userInfo.nickname,
|
||||
css: {
|
||||
color: '#333',
|
||||
fontSize: 14,
|
||||
textAlign: 'center',
|
||||
fontFamily: 'sans-serif',
|
||||
position: 'fixed',
|
||||
top: width * 0.4,
|
||||
left: width / 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'image',
|
||||
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.4,
|
||||
top: width * 0.16,
|
||||
width: width * 0.2,
|
||||
height: width * 0.2,
|
||||
},
|
||||
},
|
||||
// #ifndef MP-WEIXIN
|
||||
{
|
||||
type: 'qrcode',
|
||||
text: poster.shareInfo.link,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.35,
|
||||
top: width * 0.84,
|
||||
width: width * 0.3,
|
||||
height: width * 0.3,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
{
|
||||
type: 'image',
|
||||
src: wxa_qrcode,
|
||||
css: {
|
||||
position: 'fixed',
|
||||
left: width * 0.35,
|
||||
top: width * 0.84,
|
||||
width: width * 0.3,
|
||||
height: width * 0.3,
|
||||
},
|
||||
},
|
||||
// #endif
|
||||
];
|
||||
};
|
||||
|
||||
export default user;
|
196
sheep/components/s-share-modal/s-share-modal.vue
Normal file
196
sheep/components/s-share-modal/s-share-modal.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<!-- 全局分享弹框 -->
|
||||
<template>
|
||||
<view>
|
||||
<su-popup :show="state.showShareGuide" :showClose="false" @close="onCloseGuide" />
|
||||
<view v-if="state.showShareGuide" class="guide-wrap">
|
||||
<image class="guide-image" :src="sheep.$url.static('/static/img/shop/share/share_guide.png')" />
|
||||
</view>
|
||||
|
||||
<su-popup :show="show" round="10" :showClose="false" @close="closeShareModal">
|
||||
<!-- 分享 tools -->
|
||||
<view class="share-box">
|
||||
<view class="share-list-box ss-flex">
|
||||
<!-- 操作 ①:发送给微信好友 -->
|
||||
<button
|
||||
v-if="shareConfig.methods.includes('forward')"
|
||||
class="share-item share-btn ss-flex-col ss-col-center"
|
||||
open-type="share"
|
||||
@tap="onShareByForward"
|
||||
>
|
||||
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_wx.png')" mode="" />
|
||||
<text class="share-title">微信好友</text>
|
||||
</button>
|
||||
|
||||
<!-- 操作 ②:生成海报图片 -->
|
||||
<button
|
||||
v-if="shareConfig.methods.includes('poster')"
|
||||
class="share-item share-btn ss-flex-col ss-col-center"
|
||||
@tap="onShareByPoster"
|
||||
>
|
||||
<image
|
||||
class="share-img"
|
||||
:src="sheep.$url.static('/static/img/shop/share/share_poster.png')"
|
||||
mode=""
|
||||
/>
|
||||
<text class="share-title">生成海报</text>
|
||||
</button>
|
||||
|
||||
<!-- 操作 ③:生成链接 -->
|
||||
<button
|
||||
v-if="shareConfig.methods.includes('link')"
|
||||
class="share-item share-btn ss-flex-col ss-col-center"
|
||||
@tap="onShareByCopyLink"
|
||||
>
|
||||
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_link.png')" mode="" />
|
||||
<text class="share-title">复制链接</text>
|
||||
</button>
|
||||
</view>
|
||||
<view class="share-foot ss-flex ss-row-center ss-col-center" @tap="closeShareModal">
|
||||
取消
|
||||
</view>
|
||||
</view>
|
||||
</su-popup>
|
||||
|
||||
<!-- 分享海报,对应操作 ② -->
|
||||
<canvas-poster
|
||||
ref="SharePosterRef"
|
||||
:show="state.showPosterModal"
|
||||
:shareInfo="shareInfo"
|
||||
@close="state.showPosterModal = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
/**
|
||||
* 分享弹窗
|
||||
*/
|
||||
import { ref, unref, reactive, computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import canvasPoster from './canvas-poster/index.vue';
|
||||
import { closeShareModal, showAuthModal } from '@/sheep/hooks/useModal';
|
||||
|
||||
const show = computed(() => sheep.$store('modal').share);
|
||||
const shareConfig = computed(() => sheep.$store('app').platform.share);
|
||||
const SharePosterRef = ref('');
|
||||
|
||||
const props = defineProps({
|
||||
shareInfo: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
showShareGuide: false, // H5 的指引
|
||||
showPosterModal: false, // 海报弹窗
|
||||
});
|
||||
|
||||
// 操作 ②:生成海报分享
|
||||
const onShareByPoster = () => {
|
||||
closeShareModal();
|
||||
if (!sheep.$store('user').isLogin) {
|
||||
showAuthModal();
|
||||
return;
|
||||
}
|
||||
console.log(props.shareInfo);
|
||||
unref(SharePosterRef).getPoster();
|
||||
state.showPosterModal = true;
|
||||
};
|
||||
|
||||
// 操作 ①:直接转发分享
|
||||
const onShareByForward = () => {
|
||||
closeShareModal();
|
||||
|
||||
// #ifdef H5
|
||||
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
|
||||
state.showShareGuide = true;
|
||||
return;
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
uni.share({
|
||||
provider: 'weixin',
|
||||
scene: 'WXSceneSession',
|
||||
type: 0,
|
||||
href: props.shareInfo.link,
|
||||
title: props.shareInfo.title,
|
||||
summary: props.shareInfo.desc,
|
||||
imageUrl: props.shareInfo.image,
|
||||
success: (res) => {
|
||||
console.log('success:' + JSON.stringify(res));
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('fail:' + JSON.stringify(err));
|
||||
},
|
||||
});
|
||||
// #endif
|
||||
};
|
||||
|
||||
// 操作 ③:复制链接分享
|
||||
const onShareByCopyLink = () => {
|
||||
sheep.$helper.copyText(props.shareInfo.link);
|
||||
closeShareModal();
|
||||
};
|
||||
|
||||
function onCloseGuide() {
|
||||
state.showShareGuide = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.guide-image {
|
||||
right: 30rpx;
|
||||
top: 0;
|
||||
position: fixed;
|
||||
width: 580rpx;
|
||||
height: 430rpx;
|
||||
z-index: 10080;
|
||||
}
|
||||
|
||||
// 分享tool
|
||||
.share-box {
|
||||
background: $white;
|
||||
width: 750rpx;
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
padding-top: 30rpx;
|
||||
|
||||
.share-foot {
|
||||
font-size: 24rpx;
|
||||
color: $gray-b;
|
||||
height: 80rpx;
|
||||
border-top: 1rpx solid $gray-e;
|
||||
}
|
||||
|
||||
.share-list-box {
|
||||
.share-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.share-item {
|
||||
flex: 1;
|
||||
padding-bottom: 20rpx;
|
||||
|
||||
.share-img {
|
||||
width: 70rpx;
|
||||
height: 70rpx;
|
||||
background: $gray-f;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.share-title {
|
||||
font-size: 24rpx;
|
||||
color: $dark-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
10
sheep/components/s-statusbar/s-statusbar.vue
Normal file
10
sheep/components/s-statusbar/s-statusbar.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<template>
|
||||
<view class="status_bar" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.status_bar {
|
||||
height: var(--status-bar-height);
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
90
sheep/components/s-tabbar/s-tabbar.vue
Normal file
90
sheep/components/s-tabbar/s-tabbar.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view class="u-page__item" v-if="tabbar?.items?.length > 0">
|
||||
<su-tabbar
|
||||
:value="path"
|
||||
:fixed="true"
|
||||
:placeholder="true"
|
||||
:safeAreaInsetBottom="true"
|
||||
:inactiveColor="tabbar.style.color"
|
||||
:activeColor="tabbar.style.activeColor"
|
||||
:midTabBar="tabbar.mode === 2"
|
||||
:customStyle="tabbarStyle"
|
||||
>
|
||||
<su-tabbar-item
|
||||
v-for="(item, index) in tabbar.items"
|
||||
:key="item.text"
|
||||
:text="item.text"
|
||||
:name="item.url"
|
||||
:isCenter="getTabbarCenter(index)"
|
||||
:centerImage="sheep.$url.cdn(item.iconUrl)"
|
||||
@tap="sheep.$router.go(item.url)"
|
||||
>
|
||||
<template v-slot:active-icon>
|
||||
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.activeIconUrl)"></image>
|
||||
</template>
|
||||
<template v-slot:inactive-icon>
|
||||
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.iconUrl)"></image>
|
||||
</template>
|
||||
</su-tabbar-item>
|
||||
</su-tabbar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, unref } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
const tabbar = computed(() => {
|
||||
return sheep.$store('app').template.basic?.tabbar;
|
||||
});
|
||||
|
||||
const tabbarStyle = computed(() => {
|
||||
const backgroundStyle = tabbar.value.style;
|
||||
if (backgroundStyle.bgType === 'color') {
|
||||
return { background: backgroundStyle.bgColor };
|
||||
}
|
||||
if (backgroundStyle.bgType === 'img')
|
||||
return {
|
||||
background: `url(${sheep.$url.cdn(
|
||||
backgroundStyle.bgImg,
|
||||
)}) no-repeat top center / 100% auto`,
|
||||
};
|
||||
});
|
||||
|
||||
const getTabbarCenter = (index) => {
|
||||
if (unref(tabbar).mode !== 2) return false;
|
||||
return unref(tabbar).items % 2 > 0
|
||||
? Math.ceil(unref(tabbar).items.length / 2) === index + 1
|
||||
: false;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
path: String,
|
||||
default: '',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.u-page {
|
||||
padding: 0;
|
||||
|
||||
&__item {
|
||||
&__title {
|
||||
color: var(--textSize);
|
||||
background-color: #fff;
|
||||
padding: 15px;
|
||||
font-size: 15px;
|
||||
|
||||
&__slot-title {
|
||||
color: var(--textSize);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__slot-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
108
sheep/components/s-title-block/s-title-block.vue
Normal file
108
sheep/components/s-title-block/s-title-block.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<!-- 装修商品组件:标题栏 -->
|
||||
<template>
|
||||
<view class="ss-title-wrap ss-flex ss-col-center" :class="[state.typeMap[data.textAlign]]" :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<view class="title-content">
|
||||
<!-- 主标题 -->
|
||||
<view v-if="data.title" class="title-text" :style="[titleStyles]">{{ data.title }}</view>
|
||||
<!-- 副标题 -->
|
||||
<view v-if="data.description" :style="[descStyles]" class="sub-title-text">{{ data.description }}</view>
|
||||
</view>
|
||||
<!-- 查看更多 -->
|
||||
<view v-if="data.more?.show" class="more-box ss-flex ss-col-center" @tap="sheep.$router.go(data.more.url)"
|
||||
:style="{color: data.descriptionColor}">
|
||||
<view class="more-text" v-if="data.more.type !== 'icon'">{{ data.more.text }} </view>
|
||||
<text class="_icon-forward" v-if="data.more.type !== 'text'"></text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 标题栏
|
||||
*/
|
||||
import {
|
||||
reactive,
|
||||
computed
|
||||
} from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
|
||||
// 数据
|
||||
const state = reactive({
|
||||
typeMap: {
|
||||
left: 'ss-row-left',
|
||||
center: 'ss-row-center',
|
||||
},
|
||||
});
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const {
|
||||
bgType,
|
||||
bgImg,
|
||||
bgColor
|
||||
} = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img' ? `url(${bgImg}) no-repeat top center / 100% 100%` : bgColor
|
||||
};
|
||||
});
|
||||
|
||||
// 标题样式
|
||||
const titleStyles = {
|
||||
color: props.data.titleColor,
|
||||
fontSize: `${props.data.titleSize}px`,
|
||||
textAlign: props.data.textAlign
|
||||
};
|
||||
|
||||
// 副标题
|
||||
const descStyles = {
|
||||
color: props.data.descriptionColor,
|
||||
textAlign: props.data.textAlign,
|
||||
fontSize: `${props.data.descriptionSize}px`,
|
||||
fontWeight: `${props.data.descriptionWeight}px`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-title-wrap {
|
||||
height: 80rpx;
|
||||
position: relative;
|
||||
|
||||
.title-content {
|
||||
.title-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sub-title-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.more-box {
|
||||
white-space: nowrap;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: 20rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
213
sheep/components/s-uploader/choose-and-upload-file.js
Normal file
213
sheep/components/s-uploader/choose-and-upload-file.js
Normal file
@@ -0,0 +1,213 @@
|
||||
'use strict';
|
||||
import FileApi from '@/sheep/api/infra/file';
|
||||
|
||||
const ERR_MSG_OK = 'chooseAndUploadFile:ok';
|
||||
const ERR_MSG_FAIL = 'chooseAndUploadFile:fail';
|
||||
|
||||
function chooseImage(opts) {
|
||||
const {
|
||||
count,
|
||||
sizeType = ['original', 'compressed'],
|
||||
sourceType = ['album', 'camera'],
|
||||
extension,
|
||||
} = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseImage({
|
||||
count,
|
||||
sizeType,
|
||||
sourceType,
|
||||
extension,
|
||||
success(res) {
|
||||
resolve(normalizeChooseAndUploadFileRes(res, 'image'));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function chooseVideo(opts) {
|
||||
const { camera, compressed, maxDuration, sourceType = ['album', 'camera'], extension } = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.chooseVideo({
|
||||
camera,
|
||||
compressed,
|
||||
maxDuration,
|
||||
sourceType,
|
||||
extension,
|
||||
success(res) {
|
||||
const { tempFilePath, duration, size, height, width } = res;
|
||||
resolve(
|
||||
normalizeChooseAndUploadFileRes(
|
||||
{
|
||||
errMsg: 'chooseVideo:ok',
|
||||
tempFilePaths: [tempFilePath],
|
||||
tempFiles: [
|
||||
{
|
||||
name: (res.tempFile && res.tempFile.name) || '',
|
||||
path: tempFilePath,
|
||||
size,
|
||||
type: (res.tempFile && res.tempFile.type) || '',
|
||||
width,
|
||||
height,
|
||||
duration,
|
||||
fileType: 'video',
|
||||
cloudPath: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
'video',
|
||||
),
|
||||
);
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function chooseAll(opts) {
|
||||
const { count, extension } = opts;
|
||||
return new Promise((resolve, reject) => {
|
||||
let chooseFile = uni.chooseFile;
|
||||
if (typeof wx !== 'undefined' && typeof wx.chooseMessageFile === 'function') {
|
||||
chooseFile = wx.chooseMessageFile;
|
||||
}
|
||||
if (typeof chooseFile !== 'function') {
|
||||
return reject({
|
||||
errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。',
|
||||
});
|
||||
}
|
||||
chooseFile({
|
||||
type: 'all',
|
||||
count,
|
||||
extension,
|
||||
success(res) {
|
||||
resolve(normalizeChooseAndUploadFileRes(res));
|
||||
},
|
||||
fail(res) {
|
||||
reject({
|
||||
errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL),
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeChooseAndUploadFileRes(res, fileType) {
|
||||
res.tempFiles.forEach((item, index) => {
|
||||
if (!item.name) {
|
||||
item.name = item.path.substring(item.path.lastIndexOf('/') + 1);
|
||||
}
|
||||
if (fileType) {
|
||||
item.fileType = fileType;
|
||||
}
|
||||
item.cloudPath = Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.'));
|
||||
});
|
||||
if (!res.tempFilePaths) {
|
||||
res.tempFilePaths = res.tempFiles.map((file) => file.path);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function uploadCloudFiles(files, max = 5, onUploadProgress) {
|
||||
files = JSON.parse(JSON.stringify(files));
|
||||
const len = files.length;
|
||||
let count = 0;
|
||||
let self = this;
|
||||
return new Promise((resolve) => {
|
||||
while (count < max) {
|
||||
next();
|
||||
}
|
||||
|
||||
function next() {
|
||||
let cur = count++;
|
||||
if (cur >= len) {
|
||||
!files.find((item) => !item.url && !item.errMsg) && resolve(files);
|
||||
return;
|
||||
}
|
||||
const fileItem = files[cur];
|
||||
const index = self.files.findIndex((v) => v.uuid === fileItem.uuid);
|
||||
fileItem.url = '';
|
||||
delete fileItem.errMsg;
|
||||
|
||||
uniCloud
|
||||
.uploadFile({
|
||||
filePath: fileItem.path,
|
||||
cloudPath: fileItem.cloudPath,
|
||||
fileType: fileItem.fileType,
|
||||
onUploadProgress: (res) => {
|
||||
res.index = index;
|
||||
onUploadProgress && onUploadProgress(res);
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
fileItem.url = res.fileID;
|
||||
fileItem.index = index;
|
||||
if (cur < len) {
|
||||
next();
|
||||
}
|
||||
})
|
||||
.catch((res) => {
|
||||
fileItem.errMsg = res.errMsg || res.message;
|
||||
fileItem.index = index;
|
||||
if (cur < len) {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFiles(choosePromise, { onChooseFile, onUploadProgress }) {
|
||||
return choosePromise
|
||||
.then((res) => {
|
||||
if (onChooseFile) {
|
||||
const customChooseRes = onChooseFile(res);
|
||||
if (typeof customChooseRes !== 'undefined') {
|
||||
return Promise.resolve(customChooseRes).then((chooseRes) =>
|
||||
typeof chooseRes === 'undefined' ? res : chooseRes,
|
||||
);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then((res) => {
|
||||
if (res === false) {
|
||||
return {
|
||||
errMsg: ERR_MSG_OK,
|
||||
tempFilePaths: [],
|
||||
tempFiles: [],
|
||||
};
|
||||
}
|
||||
return res;
|
||||
})
|
||||
.then(async (files) => {
|
||||
for (let file of files.tempFiles) {
|
||||
const { data } = await FileApi.uploadFile(file.path);
|
||||
file.url = data;
|
||||
}
|
||||
return files;
|
||||
});
|
||||
}
|
||||
|
||||
function chooseAndUploadFile(
|
||||
opts = {
|
||||
type: 'all',
|
||||
},
|
||||
) {
|
||||
if (opts.type === 'image') {
|
||||
return uploadFiles(chooseImage(opts), opts);
|
||||
} else if (opts.type === 'video') {
|
||||
return uploadFiles(chooseVideo(opts), opts);
|
||||
}
|
||||
return uploadFiles(chooseAll(opts), opts);
|
||||
}
|
||||
|
||||
export { chooseAndUploadFile, uploadCloudFiles };
|
675
sheep/components/s-uploader/s-uploader.vue
Normal file
675
sheep/components/s-uploader/s-uploader.vue
Normal file
@@ -0,0 +1,675 @@
|
||||
<!-- 文件上传,基于 upload-file 和 upload-image 实现 -->
|
||||
<template>
|
||||
<view class="uni-file-picker">
|
||||
<view v-if="title" class="uni-file-picker__header">
|
||||
<text class="file-title">{{ title }}</text>
|
||||
<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text>
|
||||
</view>
|
||||
<view v-if="subtitle" class="file-subtitle">
|
||||
<view>{{ subtitle }}</view>
|
||||
</view>
|
||||
<upload-image
|
||||
v-if="fileMediatype === 'image' && showType === 'grid'"
|
||||
:readonly="readonly"
|
||||
:image-styles="imageStyles"
|
||||
:files-list="url"
|
||||
:limit="limitLength"
|
||||
:disablePreview="disablePreview"
|
||||
:delIcon="delIcon"
|
||||
@uploadFiles="uploadFiles"
|
||||
@choose="choose"
|
||||
@delFile="delFile"
|
||||
>
|
||||
<slot>
|
||||
<view class="is-add">
|
||||
<image :src="imgsrc" class="add-icon"></image>
|
||||
</view>
|
||||
</slot>
|
||||
</upload-image>
|
||||
<upload-file
|
||||
v-if="fileMediatype !== 'image' || showType !== 'grid'"
|
||||
:readonly="readonly"
|
||||
:list-styles="listStyles"
|
||||
:files-list="filesList"
|
||||
:showType="showType"
|
||||
:delIcon="delIcon"
|
||||
@uploadFiles="uploadFiles"
|
||||
@choose="choose"
|
||||
@delFile="delFile"
|
||||
>
|
||||
<slot><button type="primary" size="mini">选择文件</button></slot>
|
||||
</upload-file>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { chooseAndUploadFile, uploadCloudFiles } from './choose-and-upload-file.js';
|
||||
import {
|
||||
get_file_ext,
|
||||
get_extname,
|
||||
get_files_and_is_max,
|
||||
get_file_info,
|
||||
get_file_data,
|
||||
} from './utils.js';
|
||||
import uploadImage from './upload-image.vue';
|
||||
import uploadFile from './upload-file.vue';
|
||||
import sheep from '@/sheep';
|
||||
let fileInput = null;
|
||||
/**
|
||||
* FilePicker 文件选择上传
|
||||
* @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=4079
|
||||
* @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定
|
||||
* @property {String|Array} url url数据
|
||||
* @property {Boolean} disabled = [true|false] 组件禁用
|
||||
* @value true 禁用
|
||||
* @value false 取消禁用
|
||||
* @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮
|
||||
* @value true 只读
|
||||
* @value false 取消只读
|
||||
* @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效
|
||||
* @value true 禁用图片预览
|
||||
* @value false 取消禁用图片预览
|
||||
* @property {Boolean} del-icon = [true|false] 是否显示删除按钮
|
||||
* @value true 显示删除按钮
|
||||
* @value false 不显示删除按钮
|
||||
* @property {Boolean} auto-upload = [true|false] 是否自动上传,值为true则只触发@select,可自行上传
|
||||
* @value true 自动上传
|
||||
* @value false 取消自动上传
|
||||
* @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分
|
||||
* @property {String} title 组件标题,右侧显示上传计数
|
||||
* @property {String} mode = [list|grid] 选择文件后的文件列表样式
|
||||
* @value list 列表显示
|
||||
* @value grid 宫格显示
|
||||
* @property {String} file-mediatype = [image|video|all] 选择文件类型
|
||||
* @value image 只选择图片
|
||||
* @value video 只选择视频
|
||||
* @value all 选择所有文件
|
||||
* @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同
|
||||
* @property {Object} list-style mode:list 时的样式
|
||||
* @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同
|
||||
* @event {Function} select 选择文件后触发
|
||||
* @event {Function} progress 文件上传时触发
|
||||
* @event {Function} success 上传成功触发
|
||||
* @event {Function} fail 上传失败触发
|
||||
* @event {Function} delete 文件从列表移除时触发
|
||||
*/
|
||||
export default {
|
||||
name: 'sUploader',
|
||||
components: {
|
||||
uploadImage,
|
||||
uploadFile,
|
||||
},
|
||||
options: {
|
||||
virtualHost: true,
|
||||
},
|
||||
emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'update:url'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: [Array, Object],
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: [Array, String],
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disablePreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 自动上传
|
||||
autoUpload: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 最大选择个数 ,h5只能限制单选或是多选
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9,
|
||||
},
|
||||
// 列表样式 grid | list | list-card
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'grid',
|
||||
},
|
||||
// 选择文件类型 image/video/all
|
||||
fileMediatype: {
|
||||
type: String,
|
||||
default: 'image',
|
||||
},
|
||||
// 文件类型筛选
|
||||
fileExtname: {
|
||||
type: [Array, String],
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
listStyles: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
// 是否显示边框
|
||||
border: true,
|
||||
// 是否显示分隔线
|
||||
dividline: true,
|
||||
// 线条样式
|
||||
borderStyle: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
imageStyles: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
};
|
||||
},
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
sizeType: {
|
||||
type: Array,
|
||||
default() {
|
||||
return ['original', 'compressed'];
|
||||
},
|
||||
},
|
||||
driver: {
|
||||
type: String,
|
||||
default: 'local', // local=本地 | oss | unicloud
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
localValue: [],
|
||||
imgsrc: sheep.$url.static('/static/img/shop/upload-camera.png'),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(newVal, oldVal) {
|
||||
this.setValue(newVal, oldVal);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
returnType() {
|
||||
if (this.limit > 1) {
|
||||
return 'array';
|
||||
}
|
||||
return 'object';
|
||||
},
|
||||
filesList() {
|
||||
let files = [];
|
||||
this.files.forEach((v) => {
|
||||
files.push(v);
|
||||
});
|
||||
return files;
|
||||
},
|
||||
showType() {
|
||||
if (this.fileMediatype === 'image') {
|
||||
return this.mode;
|
||||
}
|
||||
return 'list';
|
||||
},
|
||||
limitLength() {
|
||||
if (this.returnType === 'object') {
|
||||
return 1;
|
||||
}
|
||||
if (!this.limit) {
|
||||
return 1;
|
||||
}
|
||||
if (this.limit >= 9) {
|
||||
return 9;
|
||||
}
|
||||
return this.limit;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.driver === 'local') {
|
||||
uniCloud.chooseAndUploadFile = chooseAndUploadFile;
|
||||
}
|
||||
this.form = this.getForm('uniForms');
|
||||
this.formItem = this.getForm('uniFormsItem');
|
||||
if (this.form && this.formItem) {
|
||||
if (this.formItem.name) {
|
||||
this.rename = this.formItem.name;
|
||||
this.form.inputChildrens.push(this);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 公开用户使用,清空文件
|
||||
* @param {Object} index
|
||||
*/
|
||||
clearFiles(index) {
|
||||
if (index !== 0 && !index) {
|
||||
this.files = [];
|
||||
this.$nextTick(() => {
|
||||
this.setEmit();
|
||||
});
|
||||
} else {
|
||||
this.files.splice(index, 1);
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.setEmit();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 公开用户使用,继续上传
|
||||
*/
|
||||
upload() {
|
||||
let files = [];
|
||||
this.files.forEach((v, index) => {
|
||||
if (v.status === 'ready' || v.status === 'error') {
|
||||
files.push(Object.assign({}, v));
|
||||
}
|
||||
});
|
||||
return this.uploadFiles(files);
|
||||
},
|
||||
async setValue(newVal, oldVal) {
|
||||
const newData = async (v) => {
|
||||
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
|
||||
let url = '';
|
||||
if (v.fileID) {
|
||||
url = v.fileID;
|
||||
} else {
|
||||
url = v.url;
|
||||
}
|
||||
if (reg.test(url)) {
|
||||
v.fileID = url;
|
||||
v.url = await this.getTempFileURL(url);
|
||||
}
|
||||
if (v.url) v.path = v.url;
|
||||
return v;
|
||||
};
|
||||
if (this.returnType === 'object') {
|
||||
if (newVal) {
|
||||
await newData(newVal);
|
||||
} else {
|
||||
newVal = {};
|
||||
}
|
||||
} else {
|
||||
if (!newVal) newVal = [];
|
||||
for (let i = 0; i < newVal.length; i++) {
|
||||
let v = newVal[i];
|
||||
await newData(v);
|
||||
}
|
||||
}
|
||||
this.localValue = newVal;
|
||||
if (this.form && this.formItem && !this.is_reset) {
|
||||
this.is_reset = false;
|
||||
this.formItem.setValue(this.localValue);
|
||||
}
|
||||
let filesData = Object.keys(newVal).length > 0 ? newVal : [];
|
||||
this.files = [].concat(filesData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件
|
||||
*/
|
||||
choose() {
|
||||
if (this.disabled) return;
|
||||
if (
|
||||
this.files.length >= Number(this.limitLength) &&
|
||||
this.showType !== 'grid' &&
|
||||
this.returnType === 'array'
|
||||
) {
|
||||
uni.showToast({
|
||||
title: `您最多选择 ${this.limitLength} 个文件`,
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.chooseFiles();
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件并上传
|
||||
*/
|
||||
chooseFiles() {
|
||||
const _extname = get_extname(this.fileExtname);
|
||||
// 获取后缀
|
||||
uniCloud
|
||||
.chooseAndUploadFile({
|
||||
type: this.fileMediatype,
|
||||
compressed: false,
|
||||
sizeType: this.sizeType,
|
||||
// TODO 如果为空,video 有问题
|
||||
extension: _extname.length > 0 ? _extname : undefined,
|
||||
count: this.limitLength - this.files.length, //默认9
|
||||
onChooseFile: this.chooseFileCallback,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
this.setProgress(progressEvent, progressEvent.index);
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.setSuccessAndError(result.tempFiles);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('选择失败', err);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择文件回调
|
||||
* @param {Object} res
|
||||
*/
|
||||
async chooseFileCallback(res) {
|
||||
const _extname = get_extname(this.fileExtname);
|
||||
const is_one =
|
||||
(Number(this.limitLength) === 1 && this.disablePreview && !this.disabled) ||
|
||||
this.returnType === 'object';
|
||||
// 如果这有一个文件 ,需要清空本地缓存数据
|
||||
if (is_one) {
|
||||
this.files = [];
|
||||
}
|
||||
|
||||
let { filePaths, files } = get_files_and_is_max(res, _extname);
|
||||
if (!(_extname && _extname.length > 0)) {
|
||||
filePaths = res.tempFilePaths;
|
||||
files = res.tempFiles;
|
||||
}
|
||||
|
||||
let currentData = [];
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (this.limitLength - this.files.length <= 0) break;
|
||||
files[i].uuid = Date.now();
|
||||
let filedata = await get_file_data(files[i], this.fileMediatype);
|
||||
filedata.progress = 0;
|
||||
filedata.status = 'ready';
|
||||
this.files.push(filedata);
|
||||
currentData.push({
|
||||
...filedata,
|
||||
file: files[i],
|
||||
});
|
||||
}
|
||||
this.$emit('select', {
|
||||
tempFiles: currentData,
|
||||
tempFilePaths: filePaths,
|
||||
});
|
||||
res.tempFiles = files;
|
||||
// 停止自动上传
|
||||
if (!this.autoUpload) {
|
||||
res.tempFiles = [];
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 批传
|
||||
* @param {Object} e
|
||||
*/
|
||||
uploadFiles(files) {
|
||||
files = [].concat(files);
|
||||
return uploadCloudFiles
|
||||
.call(this, files, 5, (res) => {
|
||||
this.setProgress(res, res.index, true);
|
||||
})
|
||||
.then((result) => {
|
||||
this.setSuccessAndError(result);
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 成功或失败
|
||||
*/
|
||||
async setSuccessAndError(res, fn) {
|
||||
let successData = [];
|
||||
let errorData = [];
|
||||
let tempFilePath = [];
|
||||
let errorTempFilePath = [];
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
const item = res[i];
|
||||
const index = item.uuid ? this.files.findIndex((p) => p.uuid === item.uuid) : item.index;
|
||||
|
||||
if (index === -1 || !this.files) break;
|
||||
if (item.errMsg === 'request:fail') {
|
||||
this.files[index].url = item.path;
|
||||
this.files[index].status = 'error';
|
||||
this.files[index].errMsg = item.errMsg;
|
||||
// this.files[index].progress = -1
|
||||
errorData.push(this.files[index]);
|
||||
errorTempFilePath.push(this.files[index].url);
|
||||
} else {
|
||||
this.files[index].errMsg = '';
|
||||
this.files[index].fileID = item.url;
|
||||
const reg = /cloud:\/\/([\w.]+\/?)\S*/;
|
||||
if (reg.test(item.url)) {
|
||||
this.files[index].url = await this.getTempFileURL(item.url);
|
||||
} else {
|
||||
this.files[index].url = item.url;
|
||||
}
|
||||
|
||||
this.files[index].status = 'success';
|
||||
this.files[index].progress += 1;
|
||||
successData.push(this.files[index]);
|
||||
tempFilePath.push(this.files[index].fileID);
|
||||
}
|
||||
}
|
||||
|
||||
if (successData.length > 0) {
|
||||
this.setEmit();
|
||||
// 状态改变返回
|
||||
this.$emit('success', {
|
||||
tempFiles: this.backObject(successData),
|
||||
tempFilePaths: tempFilePath,
|
||||
});
|
||||
}
|
||||
|
||||
if (errorData.length > 0) {
|
||||
this.$emit('fail', {
|
||||
tempFiles: this.backObject(errorData),
|
||||
tempFilePaths: errorTempFilePath,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取进度
|
||||
* @param {Object} progressEvent
|
||||
* @param {Object} index
|
||||
* @param {Object} type
|
||||
*/
|
||||
setProgress(progressEvent, index, type) {
|
||||
const fileLenth = this.files.length;
|
||||
const percentNum = (index / fileLenth) * 100;
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
let idx = index;
|
||||
if (!type) {
|
||||
idx = this.files.findIndex((p) => p.uuid === progressEvent.tempFile.uuid);
|
||||
}
|
||||
if (idx === -1 || !this.files[idx]) return;
|
||||
// fix by mehaotian 100 就会消失,-1 是为了让进度条消失
|
||||
this.files[idx].progress = percentCompleted - 1;
|
||||
// 上传中
|
||||
this.$emit('progress', {
|
||||
index: idx,
|
||||
progress: parseInt(percentCompleted),
|
||||
tempFile: this.files[idx],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {Object} index
|
||||
*/
|
||||
delFile(index) {
|
||||
this.$emit('delete', {
|
||||
tempFile: this.files[index],
|
||||
tempFilePath: this.files[index].url,
|
||||
});
|
||||
this.files.splice(index, 1);
|
||||
this.$nextTick(() => {
|
||||
this.setEmit();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取文件名和后缀
|
||||
* @param {Object} name
|
||||
*/
|
||||
getFileExt(name) {
|
||||
const last_len = name.lastIndexOf('.');
|
||||
const len = name.length;
|
||||
return {
|
||||
name: name.substring(0, last_len),
|
||||
ext: name.substring(last_len + 1, len),
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理返回事件
|
||||
*/
|
||||
setEmit() {
|
||||
let data = [];
|
||||
let updateUrl = [];
|
||||
if (this.returnType === 'object') {
|
||||
data = this.backObject(this.files)[0];
|
||||
this.localValue = data ? data : null;
|
||||
updateUrl = data ? data.url : '';
|
||||
} else {
|
||||
data = this.backObject(this.files);
|
||||
if (!this.localValue) {
|
||||
this.localValue = [];
|
||||
}
|
||||
this.localValue = [...data];
|
||||
if (this.localValue.length > 0) {
|
||||
this.localValue.forEach((item) => {
|
||||
updateUrl.push(item.url);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.$emit('update:modelValue', this.localValue);
|
||||
this.$emit('update:url', updateUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理返回参数
|
||||
* @param {Object} files
|
||||
*/
|
||||
backObject(files) {
|
||||
let newFilesData = [];
|
||||
files.forEach((v) => {
|
||||
newFilesData.push({
|
||||
extname: v.extname,
|
||||
fileType: v.fileType,
|
||||
image: v.image,
|
||||
name: v.name,
|
||||
path: v.path,
|
||||
size: v.size,
|
||||
fileID: v.fileID,
|
||||
url: v.url,
|
||||
});
|
||||
});
|
||||
return newFilesData;
|
||||
},
|
||||
async getTempFileURL(fileList) {
|
||||
fileList = {
|
||||
fileList: [].concat(fileList),
|
||||
};
|
||||
const urls = await uniCloud.getTempFileURL(fileList);
|
||||
return urls.fileList[0].tempFileURL || '';
|
||||
},
|
||||
/**
|
||||
* 获取父元素实例
|
||||
*/
|
||||
getForm(name = 'uniForms') {
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uni-file-picker {
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
/* width: 100%; */
|
||||
/* #endif */
|
||||
/* flex: 1; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uni-file-picker__header {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 10px;
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-count {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.is-add {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.add-icon {
|
||||
width: 57rpx;
|
||||
height: 49rpx;
|
||||
}
|
||||
.file-subtitle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: 0;
|
||||
width: 140rpx;
|
||||
height: 36rpx;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
background: rgba(#000, 0.3);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
335
sheep/components/s-uploader/upload-file.vue
Normal file
335
sheep/components/s-uploader/upload-file.vue
Normal file
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<view class="uni-file-picker__files">
|
||||
<view v-if="!readonly" class="files-button" @click="choose">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<!-- :class="{'is-text-box':showType === 'list'}" -->
|
||||
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle">
|
||||
<!-- ,'is-list-card':showType === 'list-card' -->
|
||||
|
||||
<view
|
||||
class="uni-file-picker__lists-box"
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
:class="{
|
||||
'files-border': index !== 0 && styles.dividline,
|
||||
}"
|
||||
:style="index !== 0 && styles.dividline && borderLineStyle"
|
||||
>
|
||||
<view class="uni-file-picker__item">
|
||||
<!-- :class="{'is-text-image':showType === 'list'}" -->
|
||||
<!-- <view class="files__image is-text-image">
|
||||
<image class="header-image" :src="item.logo" mode="aspectFit"></image>
|
||||
</view> -->
|
||||
<view class="files__name">{{ item.name }}</view>
|
||||
<view v-if="delIcon && !readonly" class="icon-del-box icon-files" @click="delFile(index)">
|
||||
<view class="icon-del icon-files"></view>
|
||||
<view class="icon-del rotate"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="(item.progress && item.progress !== 100) || item.progress === 0"
|
||||
class="file-picker__progress"
|
||||
>
|
||||
<progress
|
||||
class="file-picker__progress-item"
|
||||
:percent="item.progress === -1 ? 0 : item.progress"
|
||||
stroke-width="4"
|
||||
:backgroundColor="item.errMsg ? '#ff5a5f' : '#EBEBEB'"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
v-if="item.status === 'error'"
|
||||
class="file-picker__mask"
|
||||
@click.stop="uploadFiles(item, index)"
|
||||
>
|
||||
点击重试
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'uploadFile',
|
||||
emits: ['uploadFiles', 'choose', 'delFile'],
|
||||
props: {
|
||||
filesList: {
|
||||
type: Array,
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9,
|
||||
},
|
||||
showType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
listStyles: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
// 是否显示边框
|
||||
border: true,
|
||||
// 是否显示分隔线
|
||||
dividline: true,
|
||||
// 线条样式
|
||||
borderStyle: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
let files = [];
|
||||
this.filesList.forEach((v) => {
|
||||
files.push(v);
|
||||
});
|
||||
return files;
|
||||
},
|
||||
styles() {
|
||||
let styles = {
|
||||
border: true,
|
||||
dividline: true,
|
||||
'border-style': {},
|
||||
};
|
||||
return Object.assign(styles, this.listStyles);
|
||||
},
|
||||
borderStyle() {
|
||||
let { borderStyle, border } = this.styles;
|
||||
let obj = {};
|
||||
if (!border) {
|
||||
obj.border = 'none';
|
||||
} else {
|
||||
let width = (borderStyle && borderStyle.width) || 1;
|
||||
width = this.value2px(width);
|
||||
let radius = (borderStyle && borderStyle.radius) || 5;
|
||||
radius = this.value2px(radius);
|
||||
obj = {
|
||||
'border-width': width,
|
||||
'border-style': (borderStyle && borderStyle.style) || 'solid',
|
||||
'border-color': (borderStyle && borderStyle.color) || '#eee',
|
||||
'border-radius': radius,
|
||||
};
|
||||
}
|
||||
let classles = '';
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
borderLineStyle() {
|
||||
let obj = {};
|
||||
let { borderStyle } = this.styles;
|
||||
if (borderStyle && borderStyle.color) {
|
||||
obj['border-color'] = borderStyle.color;
|
||||
}
|
||||
if (borderStyle && borderStyle.width) {
|
||||
let width = (borderStyle && borderStyle.width) || 1;
|
||||
let style = (borderStyle && borderStyle.style) || 0;
|
||||
if (typeof width === 'number') {
|
||||
width += 'px';
|
||||
} else {
|
||||
width = width.indexOf('px') ? width : width + 'px';
|
||||
}
|
||||
obj['border-width'] = width;
|
||||
|
||||
if (typeof style === 'number') {
|
||||
style += 'px';
|
||||
} else {
|
||||
style = style.indexOf('px') ? style : style + 'px';
|
||||
}
|
||||
obj['border-top-style'] = style;
|
||||
}
|
||||
let classles = '';
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
uploadFiles(item, index) {
|
||||
this.$emit('uploadFiles', {
|
||||
item,
|
||||
index,
|
||||
});
|
||||
},
|
||||
choose() {
|
||||
this.$emit('choose');
|
||||
},
|
||||
delFile(index) {
|
||||
this.$emit('delFile', index);
|
||||
},
|
||||
value2px(value) {
|
||||
if (typeof value === 'number') {
|
||||
value += 'px';
|
||||
} else {
|
||||
value = value.indexOf('px') !== -1 ? value : value + 'px';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.uni-file-picker__files {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.files-button {
|
||||
// border: 1px red solid;
|
||||
}
|
||||
|
||||
.uni-file-picker__lists {
|
||||
position: relative;
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-picker__mask {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.uni-file-picker__lists-box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.uni-file-picker__item {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
padding-right: 5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.files-border {
|
||||
border-top: 1px #eee solid;
|
||||
}
|
||||
|
||||
.files__name {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-right: 25px;
|
||||
/* #ifndef APP-NVUE */
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.icon-files {
|
||||
/* #ifndef APP-NVUE */
|
||||
position: static;
|
||||
background-color: initial;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
// .icon-files .icon-del {
|
||||
// background-color: #333;
|
||||
// width: 12px;
|
||||
// height: 1px;
|
||||
// }
|
||||
|
||||
.is-list-card {
|
||||
border: 1px #eee solid;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.files__image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.header-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-text-box {
|
||||
border: 1px #eee solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.is-text-image {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.icon-del-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
margin: auto 0;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
bottom: 0;
|
||||
right: 5px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
// border-radius: 50%;
|
||||
// background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-del {
|
||||
width: 15px;
|
||||
height: 1px;
|
||||
background-color: #333;
|
||||
// border-radius: 1px;
|
||||
}
|
||||
|
||||
/* #ifdef H5 */
|
||||
@media all and (min-width: 768px) {
|
||||
.uni-file-picker__files {
|
||||
max-width: 375px;
|
||||
}
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
</style>
|
306
sheep/components/s-uploader/upload-image.vue
Normal file
306
sheep/components/s-uploader/upload-image.vue
Normal file
@@ -0,0 +1,306 @@
|
||||
<template>
|
||||
<view class="uni-file-picker__container">
|
||||
<view class="file-picker__box" v-for="(url, index) in list" :key="index" :style="boxStyle">
|
||||
<view class="file-picker__box-content" :style="borderStyle">
|
||||
<image
|
||||
class="file-image"
|
||||
:src="getImageUrl(url)"
|
||||
mode="aspectFill"
|
||||
@click.stop="previewImage(url, index)"
|
||||
></image>
|
||||
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)">
|
||||
<view class="icon-del"></view>
|
||||
<view class="icon-del rotate"></view>
|
||||
</view>
|
||||
<!-- <view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item, index)">
|
||||
点击重试
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="list.length < limit && !readonly" class="file-picker__box" :style="boxStyle">
|
||||
<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose">
|
||||
<slot>
|
||||
<view class="icon-add"></view>
|
||||
<view class="icon-add rotate"></view>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import sheep from '@/sheep';
|
||||
export default {
|
||||
name: 'uploadImage',
|
||||
emits: ['uploadFiles', 'choose', 'delFile'],
|
||||
props: {
|
||||
filesList: {
|
||||
type: [Array, String],
|
||||
default() {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disablePreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
limit: {
|
||||
type: [Number, String],
|
||||
default: 9,
|
||||
},
|
||||
imageStyles: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
border: {},
|
||||
};
|
||||
},
|
||||
},
|
||||
delIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
list() {
|
||||
if (typeof this.filesList === 'string') {
|
||||
if (this.filesList) {
|
||||
return [this.filesList];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return this.filesList;
|
||||
},
|
||||
styles() {
|
||||
let styles = {
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
border: {},
|
||||
};
|
||||
return Object.assign(styles, this.imageStyles);
|
||||
},
|
||||
boxStyle() {
|
||||
const { width = 'auto', height = 'auto' } = this.styles;
|
||||
let obj = {};
|
||||
if (height === 'auto') {
|
||||
if (width !== 'auto') {
|
||||
obj.height = this.value2px(width);
|
||||
obj['padding-top'] = 0;
|
||||
} else {
|
||||
obj.height = 0;
|
||||
}
|
||||
} else {
|
||||
obj.height = this.value2px(height);
|
||||
obj['padding-top'] = 0;
|
||||
}
|
||||
|
||||
if (width === 'auto') {
|
||||
if (height !== 'auto') {
|
||||
obj.width = this.value2px(height);
|
||||
} else {
|
||||
obj.width = '33.3%';
|
||||
}
|
||||
} else {
|
||||
obj.width = this.value2px(width);
|
||||
}
|
||||
|
||||
let classles = '';
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
borderStyle() {
|
||||
let { border } = this.styles;
|
||||
let obj = {};
|
||||
const widthDefaultValue = 1;
|
||||
const radiusDefaultValue = 3;
|
||||
if (typeof border === 'boolean') {
|
||||
obj.border = border ? '1px #eee solid' : 'none';
|
||||
} else {
|
||||
let width = (border && border.width) || widthDefaultValue;
|
||||
width = this.value2px(width);
|
||||
let radius = (border && border.radius) || radiusDefaultValue;
|
||||
radius = this.value2px(radius);
|
||||
obj = {
|
||||
'border-width': width,
|
||||
'border-style': (border && border.style) || 'solid',
|
||||
'border-color': (border && border.color) || '#eee',
|
||||
'border-radius': radius,
|
||||
};
|
||||
}
|
||||
let classles = '';
|
||||
for (let i in obj) {
|
||||
classles += `${i}:${obj[i]};`;
|
||||
}
|
||||
return classles;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getImageUrl(url) {
|
||||
if ('blob:http:' === url.substr(0, 10)) {
|
||||
return url;
|
||||
} else {
|
||||
return sheep.$url.cdn(url);
|
||||
}
|
||||
},
|
||||
uploadFiles(item, index) {
|
||||
this.$emit('uploadFiles', item);
|
||||
},
|
||||
choose() {
|
||||
this.$emit('choose');
|
||||
},
|
||||
delFile(index) {
|
||||
this.$emit('delFile', index);
|
||||
},
|
||||
previewImage(img, index) {
|
||||
let urls = [];
|
||||
if (Number(this.limit) === 1 && this.disablePreview && !this.disabled) {
|
||||
this.$emit('choose');
|
||||
}
|
||||
if (this.disablePreview) return;
|
||||
this.list.forEach((i) => {
|
||||
urls.push(this.getImageUrl(i));
|
||||
});
|
||||
|
||||
uni.previewImage({
|
||||
urls: urls,
|
||||
current: index,
|
||||
});
|
||||
},
|
||||
value2px(value) {
|
||||
if (typeof value === 'number') {
|
||||
value += 'px';
|
||||
} else {
|
||||
if (value.indexOf('%') === -1) {
|
||||
value = value.indexOf('px') !== -1 ? value : value + 'px';
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.uni-file-picker__container {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
flex-wrap: wrap;
|
||||
margin: -5px;
|
||||
}
|
||||
|
||||
.file-picker__box {
|
||||
position: relative;
|
||||
// flex: 0 0 33.3%;
|
||||
width: 33.3%;
|
||||
height: 0;
|
||||
padding-top: 33.33%;
|
||||
/* #ifndef APP-NVUE */
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.file-picker__box-content {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 5px;
|
||||
border: 1px #eee solid;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.file-picker__progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* border: 1px red solid; */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.file-picker__progress-item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-picker__mask {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.file-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.is-add {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon-add {
|
||||
width: 50px;
|
||||
height: 5px;
|
||||
background-color: #f1f1f1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.rotate {
|
||||
position: absolute;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.icon-del-box {
|
||||
/* #ifndef APP-NVUE */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.icon-del {
|
||||
width: 15px;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
110
sheep/components/s-uploader/utils.js
Normal file
110
sheep/components/s-uploader/utils.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 获取文件名和后缀
|
||||
* @param {String} name
|
||||
*/
|
||||
export const get_file_ext = (name) => {
|
||||
const last_len = name.lastIndexOf('.');
|
||||
const len = name.length;
|
||||
return {
|
||||
name: name.substring(0, last_len),
|
||||
ext: name.substring(last_len + 1, len),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取扩展名
|
||||
* @param {Array} fileExtname
|
||||
*/
|
||||
export const get_extname = (fileExtname) => {
|
||||
if (!Array.isArray(fileExtname)) {
|
||||
let extname = fileExtname.replace(/(\[|\])/g, '');
|
||||
return extname.split(',');
|
||||
} else {
|
||||
return fileExtname;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取文件和检测是否可选
|
||||
*/
|
||||
export const get_files_and_is_max = (res, _extname) => {
|
||||
let filePaths = [];
|
||||
let files = [];
|
||||
if (!_extname || _extname.length === 0) {
|
||||
return {
|
||||
filePaths,
|
||||
files,
|
||||
};
|
||||
}
|
||||
res.tempFiles.forEach((v) => {
|
||||
let fileFullName = get_file_ext(v.name);
|
||||
const extname = fileFullName.ext.toLowerCase();
|
||||
if (_extname.indexOf(extname) !== -1) {
|
||||
files.push(v);
|
||||
filePaths.push(v.path);
|
||||
}
|
||||
});
|
||||
if (files.length !== res.tempFiles.length) {
|
||||
uni.showToast({
|
||||
title: `当前选择了${res.tempFiles.length}个文件 ,${
|
||||
res.tempFiles.length - files.length
|
||||
} 个文件格式不正确`,
|
||||
icon: 'none',
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
filePaths,
|
||||
files,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取图片信息
|
||||
* @param {Object} filepath
|
||||
*/
|
||||
export const get_file_info = (filepath) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.getImageInfo({
|
||||
src: filepath,
|
||||
success(res) {
|
||||
resolve(res);
|
||||
},
|
||||
fail(err) {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
/**
|
||||
* 获取封装数据
|
||||
*/
|
||||
export const get_file_data = async (files, type = 'image') => {
|
||||
// 最终需要上传数据库的数据
|
||||
let fileFullName = get_file_ext(files.name);
|
||||
const extname = fileFullName.ext.toLowerCase();
|
||||
let filedata = {
|
||||
name: files.name,
|
||||
uuid: files.uuid,
|
||||
extname: extname || '',
|
||||
cloudPath: files.cloudPath,
|
||||
fileType: files.fileType,
|
||||
url: files.path || files.path,
|
||||
size: files.size, //单位是字节
|
||||
image: {},
|
||||
path: files.path,
|
||||
video: {},
|
||||
};
|
||||
if (type === 'image') {
|
||||
const imageinfo = await get_file_info(files.path);
|
||||
delete filedata.video;
|
||||
filedata.image.width = imageinfo.width;
|
||||
filedata.image.height = imageinfo.height;
|
||||
filedata.image.location = imageinfo.path;
|
||||
} else {
|
||||
delete filedata.image;
|
||||
}
|
||||
return filedata;
|
||||
};
|
185
sheep/components/s-user-card/s-user-card.vue
Normal file
185
sheep/components/s-user-card/s-user-card.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<!-- 装修用户组件:用户卡片 -->
|
||||
<template>
|
||||
<view class="ss-user-info-wrap ss-p-t-50" :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<view class="ss-flex ss-col-center ss-row-between ss-m-b-20">
|
||||
<view class="left-box ss-flex ss-col-center ss-m-l-36">
|
||||
<view class="avatar-box ss-m-r-24">
|
||||
<image class="avatar-img" :src="
|
||||
isLogin
|
||||
? sheep.$url.cdn(userInfo.avatar)
|
||||
: sheep.$url.static('/static/img/shop/default_avatar.png')"
|
||||
mode="aspectFill" @tap="sheep.$router.go('/pages/user/info')">
|
||||
</image>
|
||||
</view>
|
||||
<view>
|
||||
<view class="nickname-box ss-flex ss-col-center">
|
||||
<view class="nick-name ss-m-r-20">{{ userInfo?.nickname || nickname }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="right-box ss-m-r-52">
|
||||
<button class="ss-reset-button" @tap="showShareModal">
|
||||
<text class="sicon-qrcode"></text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 提示绑定手机号 先隐藏 yudao 需要再修改 -->
|
||||
<view
|
||||
class="bind-mobile-box ss-flex ss-row-between ss-col-center"
|
||||
v-if="isLogin && !userInfo.mobile"
|
||||
>
|
||||
<view class="ss-flex">
|
||||
<text class="cicon-mobile-o" />
|
||||
<view class="mobile-title ss-m-l-20"> 点击绑定手机号确保账户安全</view>
|
||||
</view>
|
||||
<button class="ss-reset-button bind-btn" @tap="onBind">去绑定</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 用户卡片
|
||||
*
|
||||
* @property {Number} leftSpace - 容器左间距
|
||||
* @property {Number} rightSpace - 容器右间距
|
||||
*
|
||||
* @property {String} avatar - 头像
|
||||
* @property {String} nickname - 昵称
|
||||
* @property {String} vip - 等级
|
||||
* @property {String} collectNum - 收藏数
|
||||
* @property {String} likeNum - 点赞数
|
||||
*
|
||||
*
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import {
|
||||
showShareModal,
|
||||
showAuthModal,
|
||||
} from '@/sheep/hooks/useModal';
|
||||
|
||||
// 用户信息
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
console.log('用户信息', userInfo);
|
||||
|
||||
// 是否登录
|
||||
const isLogin = computed(() => sheep.$store('user').isLogin);
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 头像
|
||||
avatar: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
nickname: {
|
||||
type: String,
|
||||
default: '请先登录',
|
||||
},
|
||||
vip: {
|
||||
type: [String, Number],
|
||||
default: '1',
|
||||
},
|
||||
collectNum: {
|
||||
type: [String, Number],
|
||||
default: '1',
|
||||
},
|
||||
likeNum: {
|
||||
type: [String, Number],
|
||||
default: '1',
|
||||
},
|
||||
});
|
||||
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const { bgType, bgImg, bgColor } = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img'
|
||||
? `url(${bgImg}) no-repeat top center / 100% 100%`
|
||||
: bgColor,
|
||||
};
|
||||
});
|
||||
|
||||
// 绑定手机号
|
||||
function onBind() {
|
||||
showAuthModal('changeMobile');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-user-info-wrap {
|
||||
box-sizing: border-box;
|
||||
|
||||
.avatar-box {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.nick-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.vip-img {
|
||||
width: 30rpx;
|
||||
height: 30rpx;
|
||||
}
|
||||
|
||||
.sicon-qrcode {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.bind-mobile-box {
|
||||
width: 100%;
|
||||
height: 84rpx;
|
||||
padding: 0 34rpx 0 44rpx;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
box-shadow: 0px -8rpx 9rpx 0px rgba(#e0e0e0, 0.3);
|
||||
|
||||
.cicon-mobile-o {
|
||||
font-size: 30rpx;
|
||||
color: #ff690d;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ff690d;
|
||||
}
|
||||
|
||||
.bind-btn {
|
||||
width: 100rpx;
|
||||
height: 50rpx;
|
||||
background: #ff6100;
|
||||
border-radius: 25rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
32
sheep/components/s-video-block/s-video-block.vue
Normal file
32
sheep/components/s-video-block/s-video-block.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<!-- 装修图文组件:视频播放 -->
|
||||
<template>
|
||||
<su-video
|
||||
class="sss"
|
||||
:uid="guid()"
|
||||
:src="sheep.$url.cdn(data.videoUrl)"
|
||||
:poster="sheep.$url.cdn(data.posterUrl)"
|
||||
:height="styles.height * 2"
|
||||
:autoplay="data.autoplay"
|
||||
></su-video>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { guid } from '@/sheep/helper';
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
styles: {
|
||||
type: Object,
|
||||
default() {},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sss {
|
||||
z-index: -100;
|
||||
}
|
||||
</style>
|
119
sheep/components/s-wallet-card/s-wallet-card.vue
Normal file
119
sheep/components/s-wallet-card/s-wallet-card.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<!-- 装修用户组件:用户资产 -->
|
||||
<template>
|
||||
<view class="ss-wallet-menu-wrap ss-flex ss-col-center" :style="[bgStyle, { marginLeft: `${data.space}px` }]">
|
||||
<view class="menu-item ss-flex-1 ss-flex-col ss-row-center ss-col-center"
|
||||
@tap="sheep.$router.go('/pages/user/wallet/money')">
|
||||
<view class="value-box ss-flex ss-col-bottom">
|
||||
<view class="value-text ss-line-1">{{ fen2yuan(userWallet.balance) || '0.00' }}</view>
|
||||
<view class="unit-text ss-m-l-6">元</view>
|
||||
</view>
|
||||
<view class="menu-title ss-m-t-28">账户余额</view>
|
||||
</view>
|
||||
<view class="menu-item ss-flex-1 ss-flex-col ss-row-center ss-col-center"
|
||||
@tap="sheep.$router.go('/pages/user/wallet/score')">
|
||||
<view class="value-box ss-flex ss-col-bottom">
|
||||
<view class="value-text">{{ userInfo.point || 0 }}</view>
|
||||
<view class="unit-text ss-m-l-6">个</view>
|
||||
</view>
|
||||
<view class="menu-title ss-m-t-28">积分</view>
|
||||
</view>
|
||||
<view class="menu-item ss-flex-1 ss-flex-col ss-row-center ss-col-center" @tap="
|
||||
sheep.$router.go('/pages/coupon/list', {
|
||||
type: 'geted',
|
||||
})
|
||||
">
|
||||
<view class="value-box ss-flex ss-col-bottom">
|
||||
<view class="value-text">{{ numData.unusedCouponCount }}</view>
|
||||
<view class="unit-text ss-m-l-6">张</view>
|
||||
</view>
|
||||
<view class="menu-title ss-m-t-28">优惠券</view>
|
||||
</view>
|
||||
<view class="menu-item ss-flex-col ss-row-center ss-col-center menu-wallet"
|
||||
@tap="sheep.$router.go('/pages/user/wallet/money')">
|
||||
<image class="item-icon" :src="sheep.$url.static('/static/img/shop/user/wallet_icon.png')" mode="aspectFit" />
|
||||
<view class="menu-title ss-m-t-30">我的钱包</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 装修组件 - 订单菜单组
|
||||
*/
|
||||
import { computed } from 'vue';
|
||||
import sheep from '@/sheep';
|
||||
import { fen2yuan } from '../../hooks/useGoods';
|
||||
|
||||
// 接收参数
|
||||
const props = defineProps({
|
||||
// 装修数据
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
// 装修样式
|
||||
styles: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
// 设置背景样式
|
||||
const bgStyle = computed(() => {
|
||||
// 直接从 props.styles 解构
|
||||
const { bgType, bgImg, bgColor } = props.styles;
|
||||
|
||||
// 根据 bgType 返回相应的样式
|
||||
return {
|
||||
background: bgType === 'img'
|
||||
? `url(${bgImg}) no-repeat top center / 100% 100%`
|
||||
: bgColor
|
||||
};
|
||||
});
|
||||
|
||||
const userWallet = computed(() => sheep.$store('user').userWallet);
|
||||
const userInfo = computed(() => sheep.$store('user').userInfo);
|
||||
const numData = computed(() => sheep.$store('user').numData);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ss-wallet-menu-wrap {
|
||||
.menu-wallet {
|
||||
width: 144rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 160rpx;
|
||||
|
||||
.menu-title {
|
||||
font-size: 24rpx;
|
||||
line-height: 24rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
}
|
||||
|
||||
.value-box {
|
||||
height: 50rpx;
|
||||
text-align: center;
|
||||
|
||||
.value-text {
|
||||
font-size: 28rpx;
|
||||
color: #000000;
|
||||
line-height: 28rpx;
|
||||
vertical-align: text-bottom;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
|
||||
.unit-text {
|
||||
font-size: 24rpx;
|
||||
color: #343434;
|
||||
line-height: 24rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
198
sheep/components/s-worker-item/s-worker-item.vue
Normal file
198
sheep/components/s-worker-item/s-worker-item.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<view>
|
||||
<view>
|
||||
<slot name="top"></slot>
|
||||
</view>
|
||||
<view
|
||||
class="ss-order-card-warp ss-flex ss-col-stretch ss-row-between bg-white"
|
||||
:style="[{ borderRadius: radius + 'rpx', marginBottom: marginBottom + 'rpx' }]"
|
||||
>
|
||||
<view class="img-box ss-m-r-24">
|
||||
<image class="order-img" :src="sheep.$url.cdn(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view
|
||||
class="box-right ss-flex-col ss-row-between"
|
||||
:style="[{ width: titleWidth ? titleWidth + 'rpx' : '' }]"
|
||||
>
|
||||
<view class="nickname-box">
|
||||
<view class="title-text ss-line-2" v-if="title">{{ title }}</view>
|
||||
<view v-if="skuString" class="spec-text">{{ skuString }}</view>
|
||||
</view>
|
||||
<view class="time-text">开始服务时间:</view>
|
||||
<view class="time-text">剩余时间:</view>
|
||||
<view class="groupon-box">
|
||||
<slot name="groupon"></slot>
|
||||
</view>
|
||||
<view class="ss-flex">
|
||||
<view class="ss-flex ss-col-center">
|
||||
<view
|
||||
class="price-text ss-flex ss-col-center"
|
||||
:style="[{ color: priceColor }]"
|
||||
v-if="price && Number(price) > 0"
|
||||
>
|
||||
¥{{ fen2yuan(price) }}
|
||||
</view>
|
||||
<view v-if="num" class="total-text ss-flex ss-col-center">x {{ num }}</view>
|
||||
<slot name="priceSuffix"></slot>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tool-box">
|
||||
<slot name="tool"></slot>
|
||||
</view>
|
||||
<view>
|
||||
<slot name="rightBottom"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import sheep from '@/sheep';
|
||||
import { computed } from 'vue';
|
||||
import { fen2yuan } from '@/sheep/hooks/useGoods';
|
||||
/**
|
||||
* 订单卡片
|
||||
*
|
||||
* @property {String} img - 图片
|
||||
* @property {String} title - 标题
|
||||
* @property {Number} titleWidth = 0 - 标题宽度,默认0,单位rpx
|
||||
* @property {String} skuText - 规格
|
||||
* @property {String | Number} price - 价格
|
||||
* @property {String} priceColor - 价格颜色
|
||||
* @property {Number | String} num - 数量
|
||||
*
|
||||
*/
|
||||
const props = defineProps({
|
||||
img: {
|
||||
type: String,
|
||||
default: 'https://img1.baidu.com/it/u=1601695551,235775011&fm=26&fmt=auto',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
titleWidth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
skuText: {
|
||||
type: [String, Array],
|
||||
default: '',
|
||||
},
|
||||
price: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
priceColor: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
num: {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
score: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
radius: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
marginBottom: {
|
||||
type: [String],
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
const skuString = computed(() => {
|
||||
if (!props.skuText) {
|
||||
return '';
|
||||
}
|
||||
if (typeof props.skuText === 'object') {
|
||||
return props.skuText.join(',');
|
||||
}
|
||||
return props.skuText;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nickname-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.spec-text {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.score-img {
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin: 0 4rpx;
|
||||
}
|
||||
.ss-order-card-warp {
|
||||
padding: 20rpx;
|
||||
|
||||
.img-box {
|
||||
width: 164rpx;
|
||||
height: 164rpx;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.order-img {
|
||||
width: 164rpx;
|
||||
height: 164rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.box-right {
|
||||
flex: 1;
|
||||
// width: 500rpx;
|
||||
// height: 164rpx;
|
||||
position: relative;
|
||||
|
||||
.tool-box {
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
bottom: -10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.title-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.spec-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: $dark-9;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
font-family: OPPOSANS;
|
||||
}
|
||||
|
||||
.total-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
line-height: 24rpx;
|
||||
color: $dark-9;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user