发布v1.0.3

This commit is contained in:
kele
2022-11-22 15:45:36 +08:00
commit 01f091fcd8
525 changed files with 80922 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import request from '@/sheep/request';
export default {
myGroupon: (params) =>
request({
url: 'activity/groupon/myGroupons',
method: 'GET',
params,
}),
getGrouponList: (params) =>
request({
url: 'activity/groupon',
method: 'GET',
params,
}),
grouponDetail: (id) =>
request({
url: 'activity/groupon/' + id,
method: 'GET',
}),
signList: (params) =>
request({
url: 'activity/signin',
method: 'GET',
params,
}),
signAdd: () =>
request({
url: 'activity/signin',
method: 'POST',
}),
replenish: (data) =>
request({
url: 'activity/signin/replenish',
method: 'POST',
data,
}),
activity: (id) =>
request({
url: 'activity/activity/' + id,
method: 'GET',
}),
};
+111
View File
@@ -0,0 +1,111 @@
import request from '@/sheep/request';
import { baseUrl } from '@/sheep/config';
export default {
// 系统初始化
init: (templateId) =>
request({
url: 'init',
params: {
templateId,
},
custom: {
showError: false,
showLoading: false,
},
}),
// 同步客户端页面到后端
pageSync: (pages) =>
request({
url: 'pageSync',
method: 'POST',
data: {
pages,
},
custom: {
showError: false,
showLoading: false,
},
}),
// 发送短信
sendSms: (data) =>
request({
url: 'sendSms',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '发送中',
},
}),
//意见反馈
feedback: (data) =>
request({
url: 'feedback',
method: 'POST',
data,
}),
// 自定义页面
page: (id) =>
request({
url: 'page/' + id,
method: 'GET',
}),
//积分商城
scoreShop: (params) =>
request({
url: 'app/scoreShop',
method: 'GET',
params,
}),
scoreShopIds: (params = {}) =>
request({
url: 'app/scoreShop/ids',
method: 'GET',
params,
}),
scoreShopDetail: (id) =>
request({
url: 'app/scoreShop/' + id,
method: 'GET',
}),
//上传
upload: (file, group = 'ugc', callback) => {
const token = uni.getStorageSync('token');
uni.showLoading({
title: '上传中',
});
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + '/file/api/upload',
filePath: file,
name: 'file',
formData: {
group,
},
header: {
Accept: 'text/json',
Authorization: token,
},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data);
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
});
} else {
return resolve(result.data);
}
},
fail: (error) => {
console.log('上传失败:', error);
return resolve(false);
},
complete: () => {
uni.hideLoading();
},
});
});
},
};
+41
View File
@@ -0,0 +1,41 @@
import request from '@/sheep/request';
export default {
list: (data) =>
request({
url: 'cart',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
}),
append: (data) =>
request({
url: 'cart',
method: 'POST',
custom: {
showSuccess: true,
successMsg: '已添加到购物车~',
},
data: {
...data,
type: 'inc',
},
}),
// 删除购物车
delete: (ids) =>
request({
url: 'cart/' + ids,
method: 'DELETE',
}),
update: (data) =>
request({
url: 'cart',
method: 'POST',
data: {
...data,
type: 'cover',
},
}),
};
+10
View File
@@ -0,0 +1,10 @@
import request from '@/sheep/request';
export default {
list: (params) =>
request({
url: 'category',
method: 'GET',
params,
}),
};
+13
View File
@@ -0,0 +1,13 @@
import request from '@/sheep/request';
export default {
// 获取聊天token
unifiedToken: () =>
request({
url: 'unifiedToken',
custom: {
showError: false,
showLoading: false,
},
}),
};
+65
View File
@@ -0,0 +1,65 @@
import request from '@/sheep/request';
export default {
// 分销商详情
agent: () =>
request({
url: 'commission/agent',
method: 'GET',
custom: {
showLoading: false,
showError: false,
},
}),
// 分销表单
form: () =>
request({
url: 'commission/agent/form',
method: 'GET',
}),
// 申请分销商
apply: (data) =>
request({
url: 'commission/agent/apply',
method: 'POST',
data,
custom: {
showSuccess: true,
},
}),
// 分销动态
log: (params) =>
request({
url: 'commission/log',
method: 'GET',
params,
}),
// 分销订单
order: (params) =>
request({
url: 'commission/order',
method: 'GET',
params,
}),
// 分销商品
goods: (params) =>
request({
url: 'commission/goods',
method: 'GET',
params,
}),
// 我的团队
team: (params) =>
request({
url: 'commission/agent/team',
method: 'GET',
params,
}),
// 佣金转余额
transfer: (data) =>
request({
url: 'commission/agent/transfer',
method: 'POST',
data,
}),
};
+38
View File
@@ -0,0 +1,38 @@
import request from '@/sheep/request';
export default {
// 我的拼团
list: (params) =>
request({
url: 'coupon',
method: 'GET',
params,
custom: {
showLoading: false,
},
}),
userCoupon: (params) =>
request({
url: 'user/coupon',
method: 'GET',
params,
}),
detail: (id, user_coupon_id) =>
request({
url: 'coupon/' + id,
method: 'GET',
params: {
user_coupon_id,
},
}),
get: (id) =>
request({
url: 'coupon/get/' + id,
method: 'POST',
}),
listByGoods: (id) =>
request({
url: 'coupon/listByGoods/' + id,
method: 'GET',
}),
};
+19
View File
@@ -0,0 +1,19 @@
import request from '@/sheep/request';
export default {
area: () =>
request({
url: 'data/area',
method: 'GET',
}),
faq: () =>
request({
url: 'data/faq',
method: 'GET',
}),
richtext: (id) =>
request({
url: 'data/richtext/' + id,
method: 'GET',
}),
};
+79
View File
@@ -0,0 +1,79 @@
import request from '@/sheep/request';
export default {
// 商品详情
detail: (id, params = {}) =>
request({
url: 'goods/goods/' + id,
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
}),
// 商品列表
list: (params) =>
request({
url: 'goods/goods',
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
}),
// 商品查询
ids: (params = {}) =>
request({
url: 'goods/goods/ids',
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
}),
// 商品评价列表
comment: (id, params = {}) =>
request({
url: 'goods/comment/' + id,
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
}),
// 商品评价类型
getType: (id) =>
request({
url: 'goods/comment/getType/' + id,
method: 'GET',
custom: {
showLoading: false,
showError: false,
},
}),
// 活动商品查询
// 商品查询
activity: (params = {}) =>
request({
url: 'goods/goods/activity',
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
}),
activityList: (params = {}) =>
request({
url: 'goods/goods/activityList',
method: 'GET',
params,
}),
};
+10
View File
@@ -0,0 +1,10 @@
const files = import.meta.globEager('./*.js');
let api = {};
Object.keys(files).forEach((key) => {
api = {
...api,
[key.replace(/(.*\/)*([^.]+).*/gi, '$2')]: files[key].default,
};
});
export default api;
+130
View File
@@ -0,0 +1,130 @@
import request from '@/sheep/request';
export default {
// 订单详情
detail: (id) =>
request({
url: 'order/order/' + id,
method: 'GET',
}),
// 发票详情
invoice: (id) =>
request({
url: 'order/invoice/' + id,
method: 'GET',
}),
// 获取支付结果
payResult: (id) =>
request({
url: 'order/order/' + id,
method: 'GET',
custom: {
showLoading: false,
},
}),
// 订单列表
list: (params) =>
request({
url: 'order/order',
method: 'GET',
params,
custom: {
showLoading: false,
},
}),
// 计算订单信息
calc: (data) =>
request({
url: 'order/order/calc',
method: 'POST',
data,
}),
// 创建订单
create: (data) =>
request({
url: 'order/order/create',
method: 'POST',
data,
}),
//订单可用优惠券
coupons: (data) =>
request({
url: 'order/order/coupons',
method: 'POST',
data,
}),
// 确认收货
confirm: (id) =>
request({
url: 'order/order/confirm/' + id,
method: 'PUT',
}),
// 评价订单
comment: (id, data) =>
request({
url: 'order/order/comment/' + id,
method: 'POST',
data,
}),
// 申请退款
applyRefund: (id) =>
request({
url: 'order/order/applyRefund/' + id,
method: 'PUT',
}),
// 取消订单
cancel: (id) =>
request({
url: 'order/order/cancel/' + id,
method: 'PUT',
}),
// 删除订单
delete: (id) =>
request({
url: 'order/order/' + id,
method: 'DELETE',
}),
// 售后
aftersale: {
// 申请售后
apply: (data) =>
request({
url: 'order/aftersale',
method: 'POST',
data,
}),
list: (params) =>
request({
url: 'order/aftersale',
method: 'GET',
params,
custom: {
showLoading: false,
},
}),
//取消售后
cancel: (id) =>
request({
url: 'order/aftersale/cancel/' + id,
method: 'PUT',
}),
//删除售后单
delete: (id) =>
request({
url: 'order/aftersale/' + id,
method: 'DELETE',
}),
// 售后详情
detail: (id) =>
request({
url: 'order/aftersale/' + id,
method: 'GET',
}),
},
//订单包裹
express: (id, orderId) =>
request({
url: 'order/express/' + id + `${orderId ? '/' + orderId : ''}`,
method: 'GET',
}),
};
+44
View File
@@ -0,0 +1,44 @@
import request from '@/sheep/request';
export default {
// 预支付
prepay: (data) =>
request({
url: 'pay/prepay',
method: 'POST',
data,
custom: {
loadingMsg: '支付中',
},
}),
// 发起提现
withdraw: {
list: (params) =>
request({
url: 'withdraw',
method: 'GET',
params,
custom: {
auth: true,
},
}),
rules: () =>
request({
url: 'withdraw/rules',
method: 'GET',
custom: {
auth: true,
},
}),
apply: (data) =>
request({
url: 'withdraw/apply',
method: 'POST',
data,
custom: {
loadingMsg: '申请中',
auth: true,
},
}),
},
};
+127
View File
@@ -0,0 +1,127 @@
import request from '@/sheep/request';
import { baseUrl, apiPath } from '@/sheep/config';
export default {
// 微信相关
wechat: {
// 第三方登录
login: (data) =>
request({
url: 'third/wechat/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
// 绑定微信
bind: (data) =>
request({
url: 'third/wechat/bind',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '绑定中',
},
}),
// 解除绑定微信
unbind: (data) =>
request({
url: 'third/wechat/unbind',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '解除绑定',
},
}),
// 公众号授权
oauthLogin: (data) =>
request({
url: 'third/wechat/oauthLogin',
method: 'GET',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
// 获取小程序sessionKey(后端不会给前端返回真实的sessionKey)
getSessionId: (data) =>
request({
url: 'third/wechat/getSessionId',
method: 'POST',
data,
custom: {
showLoading: false,
},
}),
// 微信小程序 绑定一键获取的手机号
bindUserPhoneNumber: (data) =>
request({
url: 'third/wechat/bindUserPhoneNumber',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '获取中',
},
}),
// 网页jssdk
jssdk: (data) =>
request({
url: 'third/wechat/jssdk',
method: 'GET',
data,
custom: {
showError: false,
showLoading: false,
},
}),
// 小程序订阅消息
subscribeTemplate: (params) =>
request({
url: 'third/wechat/subscribeTemplate',
method: 'GET',
params: {
platform: 'miniProgram',
},
custom: {
showError: false,
showLoading: false,
},
}),
// 获取微信小程序码
getWxacode: (path) =>
`${baseUrl}${apiPath}third/wechat/wxacode?platform=miniProgram&payload=${encodeURIComponent(
JSON.stringify({
path,
}),
)}`,
},
// 苹果相关
apple: {
// 第三方登录
login: (data) =>
request({
url: 'third/apple/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
},
};
+37
View File
@@ -0,0 +1,37 @@
import request from '@/sheep/request';
export default {
order: (id) =>
request({
url: 'trade/order/' + id,
method: 'GET',
custom: {
showLoading: false,
},
}),
orderLog: (params) =>
request({
url: 'trade/order',
method: 'GET',
params,
custom: {
showLoading: false,
},
}),
rechargeRules: () =>
request({
url: 'trade/order/rechargeRules',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
}),
recharge: (data) =>
request({
url: 'trade/order/recharge',
method: 'POST',
data,
}),
};
+323
View File
@@ -0,0 +1,323 @@
import request from '@/sheep/request';
import $platform from '@/sheep/platform';
export default {
profile: () =>
request({
url: '/user/api/user/profile',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
}),
update: (data) =>
request({
url: '/user/api/user/update',
method: 'POST',
custom: {
showSuccess: true,
auth: true,
},
data,
}),
// 账号登录
accountLogin: (data) =>
request({
url: '/user/api/user/accountLogin',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
},
}),
// 短信登录
smsLogin: (data) =>
request({
url: '/user/api/user/smsLogin',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
},
}),
// 短信注册
smsRegister: (data) =>
request({
url: '/user/api/user/smsRegister',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '正在注册',
},
}),
// 重置密码
resetPassword: (data) =>
request({
url: '/user/api/user/resetPassword',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '验证中',
},
}),
// 修改密码
changePassword: (data) =>
request({
url: '/user/api/user/changePassword',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '验证中',
},
}),
// 绑定、更换手机号
changeMobile: (data) =>
request({
url: '/user/api/user/changeMobile',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '验证中',
},
}),
// 修改用户名
changeUsername: (data) =>
request({
url: '/user/api/user/changeUsername',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '验证中',
},
}),
// 第三方授权信息
thirdOauthInfo: () =>
request({
url: '/user/api/user/thirdOauth',
method: 'GET',
params: {
provider: $platform.provider,
platform: $platform.platform,
},
custom: {
showLoading: false,
},
}),
// 添加分享记录
addShareLog: (data) =>
request({
url: 'share/add',
method: 'POST',
data,
custom: {
showError: false,
},
}),
share: {
list: (params) =>
request({
url: 'share/list',
method: 'GET',
params,
}),
},
// 账号登出
logout: (data) =>
request({
url: '/user/api/user/logout',
method: 'POST',
data,
}),
// 账号注销
logoff: (data) =>
request({
url: '/user/api/user/logoff',
method: 'POST',
data,
}),
address: {
default: () =>
request({
url: 'user/address/default',
method: 'GET',
custom: {
showError: false,
},
}),
list: () =>
request({
url: 'user/address',
method: 'GET',
custom: {},
}),
create: (data) =>
request({
url: 'user/address',
method: 'POST',
data,
custom: {
showSuccess: true,
},
}),
update: (id, data) =>
request({
url: 'user/address/' + id,
method: 'PUT',
data,
custom: {
showSuccess: true,
},
}),
detail: (id) =>
request({
url: 'user/address/' + id,
method: 'GET',
}),
delete: (id) =>
request({
url: 'user/address/' + id,
method: 'DELETE',
}),
},
invoice: {
list: () =>
request({
url: 'user/invoice',
method: 'GET',
custom: {},
}),
create: (data) =>
request({
url: 'user/invoice',
method: 'POST',
data,
custom: {
showSuccess: true,
},
}),
update: (id, data) =>
request({
url: 'user/invoice/' + id,
method: 'PUT',
data,
custom: {
showSuccess: true,
},
}),
detail: (id) =>
request({
url: 'user/invoice/' + id,
method: 'GET',
}),
delete: (id) =>
request({
url: 'user/invoice/' + id,
method: 'DELETE',
}),
},
favorite: {
list: (params) =>
request({
url: 'user/goodsLog/favorite',
method: 'GET',
params,
}),
do: (id) =>
request({
url: 'user/goodsLog/favorite',
method: 'POST',
data: {
goods_id: id,
},
custom: {
showSuccess: true,
auth: true,
},
}),
cancel: (id) =>
request({
url: 'user/goodsLog/favorite',
method: 'POST',
data: {
goods_ids: id,
},
custom: {
showSuccess: true,
auth: true,
},
}),
},
view: {
list: (params) =>
request({
url: 'user/goodsLog/views',
method: 'GET',
params,
custom: {},
}),
delete: (data) =>
request({
url: 'user/goodsLog/viewDel',
method: 'DELETE',
data,
custom: {
showSuccess: true,
},
}),
},
wallet: {
log: (params) =>
request({
url: '/user/api/walletLog',
method: 'GET',
params,
custom: {},
}),
},
account: {
info: (params) =>
request({
url: 'user/account',
method: 'GET',
params,
custom: {
showError: false,
auth: true,
},
}),
save: (data) =>
request({
url: 'user/account',
method: 'POST',
data,
custom: {
showSuccess: true,
auth: true,
},
}),
},
//数量接口
data: () =>
request({
url: 'user/user/data',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
}),
};
@@ -0,0 +1,96 @@
<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">
<!-- <uni-list :border="false">
<uni-list-item showArrow clickable @tap="sheep.$router.go('/pages/goods/list')">
<template v-slot:header>
<view class="model-content-tag ss-flex ss-row-center">{{ item.type_text }}</view>
</template>
<template v-slot:body>
<view class="ss-m-l-20 model-content-title">
<view class="ss-m-b-24" v-for="text in item.texts" :key="text">
{{ text }}
</view>
</view>
</template>
</uni-list-item>
</uni-list> -->
<view class="ss-flex ss-col-top ss-m-b-40" @tap="onGoodsList(item)">
<view class="model-content-tag ss-flex ss-row-center">{{ item.type_text }}</view>
<view class="ss-m-l-20 model-content-title ss-flex-1">
<view class="ss-m-b-24" v-for="text in item.texts" :key="text">
{{ text }}
</view>
</view>
<text class="cicon-forward"></text>
</view>
</view>
</scroll-view>
</view>
</su-popup>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive } from 'vue';
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['close']);
const state = reactive({
activityInfo: computed(() => props.modelValue),
});
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>
@@ -0,0 +1,105 @@
<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.is_default"
text="默认"
></uni-tag>
{{ props.item.province_name }} {{ props.item.city_name }} {{ props.item.district_name }}
</view>
<view class="address-text">{{ props.item.address }}</view>
<view class="person-text">{{ props.item.consignee }} {{ 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')"></image>
</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';
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>
@@ -0,0 +1,108 @@
<!-- 账号密码登录 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 ss-m-r-40 head-title-animation">账号登录</view>
<view class="head-title-active head-title-line" @tap="showAuthModal('smsLogin')">
短信登录
</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="account" label="账号">
<uni-easyinput placeholder="请输入账号" v-model="state.model.account" :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>
<button class="ss-reset-button type-btn" @tap="showAuthModal('smsRegister')">
立即注册
</button>
</uni-forms>
</view>
</template>
<script setup>
import { computed, watch, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { account, password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
const accountLoginRef = ref(null);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
model: {
account: '', // 账号
password: '', // 密码
},
rules: {
account,
password,
},
});
// 1.账号登录
async function accountLoginSubmit() {
// 表单验证
const validate = await unref(accountLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
// 同意协议
if (!props.agreeStatus) {
sheep.$helper.toast('请勾选同意');
return;
}
// 提交数据
sheep.$api.user.accountLogin(state.model).then((res) => {
if (res.error === 0) {
// sheep.$store('user').getInfo();
closeAuthModal();
}
});
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,120 @@
<!-- 绑定/更换手机号 changeMobile -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20">
{{ userInfo.verification.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, watch, 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';
const changeMobileRef = ref(null);
const userInfo = computed(() => sheep.$store('user').userInfo);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 5.绑定手机号
async function changeMobileSubmit() {
const validate = await unref(changeMobileRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
sheep.$api.user.changeMobile(state.model).then((res) => {
if (res.error === 0) {
sheep.$store('user').getInfo();
closeAuthModal();
}
});
}
// 使用微信手机号
async function getPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
} else {
let result = await sheep.$platform.useProvider().bindUserPhoneNumber(e.detail);
if (result) {
sheep.$store('user').getInfo();
closeAuthModal();
}
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,121 @@
<!-- 修改密码 - changePassword -->
<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="oldPassword" label="旧密码">
<uni-easyinput
placeholder="请输入旧密码"
v-model="state.model.oldPassword"
type="text"
:inputBorder="false"
/>
</uni-forms-item>
<uni-forms-item name="newPassword" label="新密码">
<uni-easyinput
type="password"
placeholder="请输入新密码"
v-model="state.model.newPassword"
:inputBorder="false"
/>
</uni-forms-item>
<uni-forms-item name="reNewPassword" label="确认密码">
<uni-easyinput
type="password"
placeholder="请重新输入您的新密码"
v-model="state.model.reNewPassword"
: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>
<view class="editPwd-btn-box ss-m-t-80">
<button class="ss-reset-button forgot-btn" @tap="showAuthModal('resetPassword')">
忘记密码
</button>
</view>
</view>
</template>
<script setup>
import { computed, watch, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
const changePasswordRef = ref(null);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
oldPassword: '', //旧密码
newPassword: '', //新密码
reNewPassword: '', //确认密码
},
rules: {
oldPassword: password,
newPassword: password,
reNewPassword: {
rules: [
{
required: true,
errorMessage: '请确认密码',
},
{
validateFunction: function (rule, value, data, callback) {
if (value !== state.model.newPassword) {
callback('两次输入的密码不一致');
}
if (value === state.model.oldPassword) {
callback('新密码不能与旧密码相同');
}
return true;
},
},
],
},
},
});
// 6.更改密码
async function changePasswordSubmit() {
const validate = await unref(changePasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
sheep.$api.user.changePassword(state.model).then((res) => {
if (res.error === 0) {
sheep.$store('user').getInfo();
closeAuthModal();
}
});
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,72 @@
<!-- 修改用户名 changeUsername -->
<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="formRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="username" label="用户名">
<uni-easyinput
placeholder="请输入用户名"
v-model="state.model.username"
:inputBorder="false"
></uni-easyinput>
</uni-forms-item>
<view class="editPwd-btn-box ss-m-t-80">
<button class="ss-reset-button save-btn ui-Shadow-Main" @tap="changeUsernameSubmit">
保存
</button>
</view>
</uni-forms>
</view>
</template>
<script setup>
import { computed, watch, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { username } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
const formRef = ref(null);
// 数据
const state = reactive({
model: {
username: '',
},
rules: {
username,
},
});
// 7.修改用户名
async function changeUsernameSubmit() {
const validate = await unref(formRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
sheep.$api.user.changeUsername(state.model).then((res) => {
if (res.error === 0) {
sheep.$store('user').getInfo();
closeAuthModal();
}
});
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,114 @@
<!-- 重置密码 - resetPassword-- -->
<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-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="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, watch, 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';
const resetPasswordRef = ref(null);
const isLogin = computed(() => sheep.$store('user').isLogin);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', //手机号
code: '', //验证码
password: '', //密码
},
rules: {
code,
mobile,
password,
},
});
// 4.重置密码
const resetPasswordSubmit = async () => {
const validate = await unref(resetPasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
sheep.$api.user.resetPassword(state.model).then((res) => {
if (res.error === 0) {
sheep.$store('user').getInfo();
closeAuthModal();
}
});
};
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,115 @@
<!-- 短信登录 - smsLogin -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="ss-flex ss-m-b-20">
<view class="head-title-active ss-m-r-40" @tap="showAuthModal('accountLogin')"
>账号登录</view
>
<view class="head-title head-title-line head-title-animation">短信登录</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>
<button class="ss-reset-button type-btn" @tap="showAuthModal('smsRegister')"> 立即注册 </button>
</view>
</template>
<script setup>
import { computed, watch, 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 throttle from '@/sheep/helper/throttle';
const smsLoginRef = ref(null);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
codeText: '获取验证码',
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 2.短信登录
async function smsLoginSubmit() {
const validate = await unref(smsLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
if (!props.agreeStatus) {
sheep.$helper.toast('请勾选同意');
return;
}
const { error } = await sheep.$api.user.smsLogin(state.model);
if (error === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
@@ -0,0 +1,126 @@
<!-- 短信注册 - smsRegister -->
<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="smsRegisterRef"
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('smsRegister', state.model.mobile)"
>
{{ getSmsTimer('smsRegister') }}
</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"
></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="smsRegisterSubmit"> 注册 </button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button 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';
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
const smsRegisterRef = ref(null);
const isLogin = computed(() => sheep.$store('user').isLogin);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
password: '', // 密码
},
rules: {
code,
mobile,
password,
},
});
// 3.短信注册
async function smsRegisterSubmit() {
const validate = await unref(smsRegisterRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
if (!props.agreeStatus) {
sheep.$helper.toast('请勾选同意');
return;
}
const { error } = await sheep.$api.user.smsRegister({
...state.model,
shareInfo: uni.getStorageSync('shareLog') || {},
});
if (error === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>
+151
View 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;
}
}
@@ -0,0 +1,172 @@
<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"
></account-login>
<!-- 2.短信登录 smsLogin -->
<sms-login v-if="authType === 'smsLogin'" :agreeStatus="state.protocol"></sms-login>
<!-- 3.短信注册 smsRegister-->
<sms-register v-if="authType === 'smsRegister'" :agreeStatus="state.protocol"></sms-register>
<!-- 4.忘记密码 resetPassword-->
<reset-password v-if="authType === 'resetPassword'"></reset-password>
<!-- 5.绑定手机号 changeMobile -->
<change-mobile v-if="authType === 'changeMobile'"></change-mobile>
<!-- 6.修改密码 changePassword-->
<change-passwrod v-if="authType === 'changePassword'"></change-passwrod>
<!-- 7.修改用户名 changeUsername-->
<change-username v-if="authType === 'changeUsername'"></change-username>
<!-- 第三方登录 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="auto-login-box ss-flex ss-row-center ss-col-center"
>
<!-- 微信小程序登录 -->
<button
v-if="sheep.$platform.name === 'WechatMiniProgram'"
open-type="getPhoneNumber"
@getphonenumber="thirdLogin('wechat', $event)"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
></image>
</button>
<!-- 公众号|App微信登录 -->
<button
v-if="
['WechatOfficialAccount', 'App'].includes(sheep.$platform.name) &&
sheep.$platform.isWechatInstalled
"
@tap="thirdLogin('wechat')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
></image>
</button>
<!-- iOS登录 -->
<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')"
></image>
</button>
</view>
<view
v-if="['accountLogin', 'smsLogin', 'smsRegister'].includes(authType)"
class="agreement-box ss-flex ss-row-center"
>
<label class="radio ss-flex" @tap="onChange">
<radio
:checked="state.protocol"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
/>
<view class="agreement-text ss-flex ss-col-center">
我已阅读并遵守
<view
class="tcp-text"
@tap.stop="onProtocol(appInfo.user_protocol.id, appInfo.user_protocol.title)"
>
《{{ appInfo.user_protocol.title }}》
</view>
<view class="agreement-text">与</view>
<view
class="tcp-text"
@tap.stop="onProtocol(appInfo.privacy_protocol.id, appInfo.privacy_protocol.title)"
>
《{{ appInfo.privacy_protocol.title }}》
</view>
</view>
</label>
</view>
<view class="safe-box"></view>
</view>
</su-popup>
</template>
<script setup>
import { computed, reactive } from 'vue';
import sheep from '@/sheep';
import accountLogin from './components/account-login.vue';
import smsLogin from './components/sms-login.vue';
import smsRegister from './components/sms-register.vue';
import resetPassword from './components/reset-password.vue';
import changeMobile from './components/change-mobile.vue';
import changePasswrod from './components/change-password.vue';
import changeUsername from './components/change-username.vue';
import { closeAuthModal } 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,
});
//勾选协议
function onChange() {
state.protocol = !state.protocol;
}
// 查看协议
function onProtocol(id, title) {
closeAuthModal();
sheep.$router.go('/pages/public/richtext', {
id,
title,
});
}
// 第三方授权登陆
const thirdLogin = async (provider, event = null) => {
if (!state.protocol) {
sheep.$helper.toast('请勾选同意');
return;
}
const loginRes = await sheep.$platform.useProvider(provider).login(event?.detail || null);
if (loginRes) {
closeAuthModal();
const userInfo = await sheep.$store('user').getInfo();
}
};
</script>
<style lang="scss" scoped>
@import './index.scss';
.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>
@@ -0,0 +1,53 @@
<template>
<view>
<s-image-block v-if="type === 'imageBlock'" :data="data" :styles="styles" />
<s-image-banner v-if="type === 'imageBanner'" :data="data" :styles="styles" />
<s-video-block v-if="type === 'videoPlayer'" :data="data" :styles="styles" />
<s-image-cube v-if="type === 'imageCube'" :data="data" :styles="styles" />
<s-notice-block v-if="type === 'noticeBlock'" :data="data" />
<s-search-block v-if="type === 'searchBlock'" :data="data" :navbar="false" />
<s-title-block v-if="type === 'titleBlock'" :data="data" :styles="styles" />
<s-line-block v-if="type === 'lineBlock'" :data="data" />
<s-menu-button v-if="type === 'menuButton'" :data="data" :styles="styles" />
<s-menu-list v-if="type === 'menuList'" :data="data" />
<s-menu-grid v-if="type === 'menuGrid'" :data="data" />
<s-user-card v-if="type === 'userCard'" />
<s-wallet-card v-if="type === 'walletCard'" />
<s-order-card v-if="type === 'orderCard'" :data="data" />
<s-coupon-card v-if="type === 'couponCard'" />
<s-goods-card v-if="type === 'goodsCard'" :data="data" :styles="styles" />
<s-score-block v-if="type === 'scoreGoods'" :data="data" :styles="styles" />
<s-goods-shelves v-if="type === 'goodsShelves'" :data="data" :styles="styles" />
<s-coupon-block v-if="type === 'coupon'" :data="data" :styles="styles"></s-coupon-block>
<s-seckill-block v-if="type === 'seckill'" :data="data" :styles="styles"></s-seckill-block>
<s-groupon-block v-if="type === 'groupon'" :data="data" :styles="styles"></s-groupon-block>
<s-richtext-block v-if="type === 'richtext'" :data="data" :styles="styles"></s-richtext-block>
</view>
</template>
<script setup>
/**
* 装修组件 - 组件集
*/
const props = defineProps({
type: {
type: String,
default: '',
},
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
function onSearch() {}
</script>
<style></style>
+50
View File
@@ -0,0 +1,50 @@
<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.background.type == 'color')
return { background: props.styles.background.bgColor };
if (props.styles.background.type == 'image')
return {
background: `url(${sheep.$url.cdn(
props.styles.background.bgImage,
)}) no-repeat top center / 100% auto`,
};
}
});
const elStyles = computed(() => {
if (props.styles) {
return {
marginTop: `${props.styles.marginTop}px`,
marginBottom: props.styles.marginBottom + 'px',
marginLeft: `${props.styles.marginLeft}px`,
marginRight: props.styles.marginRight + 'px',
'border-top-left-radius': props.styles.borderRadiusTop + 'px',
'border-top-right-radius': props.styles.borderRadiusTop + 'px',
'border-bottom-left-radius': props.styles.borderRadiusBottom + 'px',
'border-bottom-right-radius': props.styles.borderRadiusBottom + 'px',
padding: props.styles.padding + 'px',
overflow: 'hidden',
};
}
});
</script>
@@ -0,0 +1,189 @@
<!-- 优惠券组 -->
<template>
<view>
<!-- 样式1 -->
<view class="lg-coupon-wrap" v-if="mode == 1">
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
<view class="coupon-box ss-flex">
<view
class="coupon-item"
:style="[couponBg, { marginLeft: data.space + 'px' }]"
v-for="(item, index) in couponList"
:key="index"
>
<su-coupon
size="lg"
:textColor="data.fill.color"
background=""
:couponId="item.id"
:title="item.amount_text"
:value="item.amount"
:surplus="item.stock"
:sellBy="`${item.get_start_time.substring(0, 10)} 至 ${item.get_end_time.substring(
0,
10,
)}`"
>
<template v-slot:btn>
<button
class="ss-reset-button card-btn"
:style="[btnStyles]"
@click.stop="onGetCoupon(item.id)"
>
{{ item.get_status_text }}
</button>
</template>
</su-coupon>
</view>
</view>
</scroll-view>
</view>
<!-- 样式2 -->
<view class="md-coupon-wrap" v-if="mode == 2">
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
<view class="coupon-box ss-flex">
<view
class="coupon-item"
:style="[couponBg, { marginLeft: data.space + 'px' }]"
v-for="(item, index) in couponList"
:key="index"
>
<su-coupon
size="md"
:textColor="data.fill.color"
background=""
:title="item.amount_text"
:value="item.amount"
:surplus="item.stock"
:couponId="item.id"
:sellBy="`${item.get_start_time.substring(0, 10)} 至 ${item.get_end_time.substring(
0,
10,
)}`"
>
<template v-slot:btn>
<button
@click.stop="onGetCoupon(item.id)"
class="ss-reset-button card-btn ss-flex ss-row-center ss-col-center"
:style="[btnStyles]"
>
<view class="btn-text">{{ item.get_status_text }}</view>
</button>
</template>
</su-coupon>
</view>
</view>
</scroll-view>
</view>
<!-- 样式3 -->
<view class="xs-coupon-wrap" v-if="mode == 3">
<scroll-view class="scroll-box" scroll-x scroll-anchoring>
<view class="coupon-box ss-flex">
<view
class="coupon-item"
:style="[couponBg, { marginLeft: data.space + 'px' }]"
v-for="(item, index) in couponList"
:key="index"
>
<su-coupon
size="xs"
:textColor="data.fill.color"
background=""
:title="item.amount_text"
:value="item.amount"
:surplus="item.stock"
:couponId="item.id"
:sellBy="`${item.get_start_time.substring(0, 10)} 至 ${item.get_end_time.substring(
0,
10,
)}`"
>
<template v-slot:btn>
<button
class="ss-reset-button card-btn"
:style="[btnStyles]"
@click.stop="onGetCoupon(item.id)"
>
{{ item.get_status_text }}
</button>
</template>
</su-coupon>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { ref, onMounted } from 'vue';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
styles: {
type: Object,
default: () => ({}),
},
});
const { mode, button } = props.data;
const couponBg = {
background: `url(${sheep.$url.cdn(props.data.fill.bgImage)}) no-repeat top center / 100% 100%`,
};
const btnStyles = {
background: button.bgColor,
color: button.color,
};
const couponList = ref([]);
//立即领取优惠券
async function onGetCoupon(id) {
const { error, msg } = await sheep.$api.coupon.get(id);
if (error === 0) {
uni.showToast({
title: msg,
icon: 'none',
});
} else {
let { data } = await sheep.$api.coupon.list({ ids: props.data.couponIds.join(',') });
couponList.value = [...data.data];
}
}
onMounted(async () => {
let { data } = await sheep.$api.coupon.list({ ids: props.data.couponIds.join(',') });
// couponList.value = [...data.data, ...data.data, ...data.data, ...data.data];
couponList.value = [...data.data];
});
</script>
<style lang="scss" scoped>
.card-btn {
width: 140rpx;
height: 50rpx;
border-radius: 25rpx;
font-size: 24rpx;
line-height: 50rpx;
}
.coupon-item {
&:nth-of-type(1) {
margin-left: 0 !important;
}
}
.md-coupon-wrap {
.card-btn {
width: 50rpx;
height: 140rpx;
margin: auto 0;
margin-right: 20rpx;
.btn-text {
font-size: 24rpx;
text-align: center;
writing-mode: vertical-lr;
writing-mode: tb-lr;
}
}
}
</style>
@@ -0,0 +1,80 @@
<template>
<view class="ss-coupon-menu-wrap ss-flex ss-col-center">
<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';
// 接收参数
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',
},
];
},
},
});
</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>
@@ -0,0 +1,108 @@
<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.get_status != 'can_get' && item.get_status != 'can_use' ? 'boder-btn' : ''
"
@click.stop="getBuy(item.id)"
:disabled="item.get_status != 'can_get' && item.get_status != 'can_use'"
>
{{ item.get_status_text }}
</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),
currentValue: -1,
couponId: '',
});
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>
@@ -0,0 +1,184 @@
<template>
<view class="ss-m-20" :style="{ opacity: disabled ? '0.4' : '1' }">
<view class="content">
<view
class="tag ss-flex ss-row-center"
:class="
data.status == 'expired' || data.status == 'used' ? 'disabled-bg-color' : 'info-bg-color'
"
>{{ data.type_text }}</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="
data.status == 'expired' || data.status == 'used' ? 'disabled-color' : 'info-color'
"
>{{ data.name }}</view
>
<view>
<view
class="ss-flex ss-col-bottom"
:class="
data.status != 'expired' && data.status != 'used' ? 'price-text' : 'disabled-color'
"
>
<view class="value-reduce ss-m-b-10" v-if="data.type === 'reduce'"></view>
<view class="value-price">{{ data.amount }}</view>
<view class="value-discount ss-m-b-10 ss-m-l-4" v-if="data.type === 'discount'"
></view
>
</view>
</view>
</view>
<view class="ss-flex ss-row-between ss-m-t-16">
<view
class="sellby-text"
:class="
data.status == 'expired' || data.status == 'used'
? 'disabled-color'
: 'subtitle-color'
"
>
{{
type === 'user'
? '有效期:' + data.use_start_time.substring(0, 11)
: '领取时间:' + data.get_start_time.substring(0, 11)
}}
{{
type === 'user'
? data.use_end_time.substring(0, 11)
: data.get_end_time.substring(0, 11)
}}
</view>
<view
class="value-enough"
:class="
data.status == 'expired' || data.status == 'used'
? 'disabled-color'
: 'subtitle-color'
"
>{{ data.enough }}可用</view
>
</view>
</view>
</view>
<view class="desc ss-flex ss-row-between">
<view class="desc-title">
{{ data.description }}
</view>
<view>
<slot></slot>
</view>
</view>
</view>
</template>
<script setup>
import { reactive } from 'vue';
import sheep from '@/sheep';
const state = reactive({
stateMap: {
0: '立即领取',
1: '去使用',
},
});
// 接受参数
const props = defineProps({
data: {
type: Object,
default: {},
},
disabled: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'coupon',
},
});
</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;
}
.value-reduce {
line-height: normal;
font-size: 32rpx;
}
.value-discount {
line-height: normal;
font-size: 28rpx;
}
.value-enough {
font-size: 24rpx;
font-weight: 400;
}
}
}
.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>
@@ -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, index) in state.couponInfo.can_use" :key="index">
<s-coupon-list :data="item" type="user" :disabled="false">
<template #default>
<label class="radio">
<radio
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
@tap.stop="radioChange(item.id)"
:checked="state.couponId == 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.cannot_use" :key="item.id">
<s-coupon-list :data="item" type="user" :disabled="true" />
</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: 0,
});
function radioChange(couponId) {
if (state.couponId == couponId) {
state.couponId = 0;
} 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;
}
</style>
@@ -0,0 +1,62 @@
<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.src)" 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>
@@ -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>
@@ -0,0 +1,117 @@
<template>
<navbar
:alway="isAlway"
:back="false"
bg=""
:placeholder="isPlaceholder"
:bgStyles="bgStyles"
:opacity="isOpacity"
:sticky="sticky"
>
<template #item>
<view class="nav-box">
<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} mode = [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';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
});
const sticky = computed(() => {
if (props.data.mode == 'inner') {
if (props.data.alway) {
return false;
}
}
if (props.data.mode == 'normal') {
return false;
}
});
const navList = computed(() => {
if (!props.data.list) return [];
// #ifdef MP
return props.data.list.mp;
// #endif
return props.data.list.app;
});
// 单元格大小
const windowWidth = sheep.$platform.device.windowWidth;
const cell = computed(() => {
if (unref(navList).length) {
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 isAlway = computed(() =>
props.data.mode === 'inner' ? Boolean(props.data.alwaysShow) : true,
);
const isOpacity = computed(() =>
props.data.mode === 'normal' ? false : props.data.mode === 'inner',
);
const isPlaceholder = computed(() => props.data.mode === 'normal');
const bgStyles = computed(() => {
if (props.data.type) {
return {
background:
props.data.type == 'color'
? props.data.color
: `url(${sheep.$url.cdn(props.data.src)}) no-repeat top center / 100% 100%`,
};
}
});
</script>
<style lang="scss" scoped>
.nav-box {
width: 750rpx;
position: relative;
height: 100%;
.nav-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
}
</style>
@@ -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
View 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=""><image class="empty-icon" :src="icon" mode="widthFix"></image></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: #999999;
}
.empty-btn {
width: 320rpx;
height: 70rpx;
border: 2rpx solid v-bind('props.buttonColor');
border-radius: 35rpx;
font-weight: 500;
color: v-bind('props.buttonColor');
font-size: 28rpx;
}
</style>
@@ -0,0 +1,81 @@
<template>
<!-- TODO-jj: 点击遮罩关闭 -->
<view>
<view v-if="data?.show === 1">
<uni-fab
ref="fabRef"
:pattern="state.pattern"
:content="state.content"
horizontal="right"
vertical="bottom"
:direction="state.direction"
@trigger="trigger"
@fabClick="fabClick"
:popMenu="true"
></uni-fab>
</view>
<view :class="state.show ? 'float-bg' : ''" @click="onTap"></view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed, reactive, ref, unref, getCurrentInstance } from 'vue';
import { onBackPress } from '@dcloudio/uni-app';
const data = computed(() => sheep.$store('app').template.basic?.floatMenu);
const state = reactive({
pattern: [],
content: [],
direction: '',
show: false,
});
const fabRef = ref(null);
const vm = getCurrentInstance();
if (data.value?.mode === 1) {
state.direction = 'vertical';
} else {
state.direction = 'horizontal';
}
data.value?.list.forEach((i) => {
if (data.value?.isText === 0) {
state.content.push({ iconPath: sheep.$url.cdn(i.src), url: i.url });
} else {
state.content.push({ iconPath: sheep.$url.cdn(i.src), text: i.title.text + '', url: i.url });
}
state.pattern.push({ color: i.title.color });
});
function trigger(e) {
sheep.$router.go(e.item.url);
}
function onTap() {
if (state.show) {
state.show = false;
vm.refs.fabRef.close();
} else {
state.show = true;
vm.refs.fabRef.open();
}
}
function fabClick() {
state.show = !state.show;
}
onBackPress(() => {
if (unref(fabRef).isShow) {
unref(fabRef).close();
return true;
} else {
return false;
}
});
</script>
<style lang="scss" scoped>
.float-bg {
position: fixed;
left: 0;
top: 0;
z-index: 11;
width: 100%;
height: 100%;
background-color: rgba(#000000, 0.4);
}
</style>
@@ -0,0 +1,250 @@
<template>
<!-- 商品卡片 -->
<view>
<!-- 1 100%宽卡片列表-->
<view v-if="mode === 1 && 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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.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]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-goods-column>
</view>
</view>
<!-- 2 双列瀑布流列表-->
<view
v-if="mode === 2 && 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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
:titleWidth="330 - marginLeft - marginRight"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'left')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
:titleWidth="330 - marginLeft - marginRight"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
@getHeight="mountMasonry($event, 'right')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-goods-column>
</view>
</view>
</view>
<!-- 3 30%卡片列表-->
<view v-if="mode === 3 && 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="goodsFields"
:data="item"
:tagStyle="tagStyle"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.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]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-goods-column>
</view>
</view>
</view>
</template>
<script setup>
/**
* 商品模板,装修商品卡片
* @description style 1:带tab 2:瀑布流,横向两个,上图下内容 3:大图,横向一个
*/
import { computed, reactive, onMounted } from 'vue';
import sheep from '@/sheep';
const state = reactive({
goodsList: [],
leftGoodsList: [],
rightGoodsList: [],
});
const props = defineProps({
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
const { mode, tagStyle, buyNowStyle, goodsFields, goodsIds } = props.data ?? {};
const { marginLeft, marginRight } = props.styles ?? {};
async function getGoodsListByIds(ids) {
let { data } = await sheep.$api.goods.ids({ ids });
return data;
}
onMounted(async () => {
state.goodsList = await getGoodsListByIds(goodsIds.join(','));
if (mode === 2) {
mountMasonry();
}
});
// 加载瀑布流
let count = 0;
let leftHeight = 0;
let rightHeight = 0;
function mountMasonry(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++;
}
// 购买按钮样式
const buyStyle = computed(() => {
if (buyNowStyle.mode == 1) {
// button
return {
background: `linear-gradient(to right, ${buyNowStyle.color1}, ${buyNowStyle.color2})`,
};
}
if (buyNowStyle.mode == 2) {
// image
return {
width: '54rpx',
height: '54rpx',
background: `url(${sheep.$url.cdn(buyNowStyle.src)}) no-repeat`,
backgroundSize: '100% 100%',
};
}
});
</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>
@@ -0,0 +1,711 @@
<!-- 页面 -->
<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)"></image>
</view>
<image class="xs-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFit"></image>
<view
v-if="goodsFields.title?.show || goodsFields.price?.show"
class="xs-goods-content ss-flex-col ss-row-around"
>
<view
v-if="goodsFields.title?.show"
class="xs-goods-title ss-line-1"
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
>
{{ data.title }}
</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) ? data.price[0] : 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)"></image>
</view>
<image class="sm-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFill"></image>
<view
v-if="goodsFields.title?.show || goodsFields.price?.show"
class="sm-goods-content"
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
>
<view v-if="goodsFields.title?.show" class="sm-goods-title ss-line-1 ss-m-b-16">
{{ data.title }}
</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) ? data.price[0] : 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)"></image>
</view>
<image
class="md-img-box"
:src="sheep.$url.cdn(data.image)"
mode="widthFix"
@load="calculatePanelHeight"
></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"
class="md-goods-title ss-line-1"
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
>
{{ data.title }}
</view>
<view
v-if="goodsFields.subtitle?.show"
class="md-goods-subtitle ss-m-t-16 ss-line-1"
:style="[{ color: subTitleColor, background: subTitleBackground }]"
>
{{ data.subtitle }}
</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) ? data.price[0] : data.price }}
</view>
<view
v-if="goodsFields.original_price?.show && data.original_price > 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">{{ data.original_price }}</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=""></image>
</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)"></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)" 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"
class="lg-goods-title ss-line-2"
:style="[{ color: titleColor }]"
>
{{ data.title }}
</view>
<view
v-if="goodsFields.subtitle?.show"
class="lg-goods-subtitle ss-m-t-10 ss-line-1"
:style="[{ color: subTitleColor, background: subTitleBackground }]"
>
{{ data.subtitle }}
</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) ? data.price[0] : data.price }}
</view>
<view
v-if="goodsFields.original_price?.show && data.original_price > 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">{{ data.original_price }}</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)"></image>
</view>
<image class="sl-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFill"></image>
<view class="sl-goods-content">
<view>
<view
v-if="goodsFields.title?.show"
class="sl-goods-title ss-line-1"
:style="[{ color: titleColor }]"
>
{{ data.title }}
</view>
<view
v-if="goodsFields.subtitle?.show"
class="sl-goods-subtitle ss-m-t-16"
:style="[{ color: subTitleColor, background: subTitleBackground }]"
>
{{ data.subtitle }}
</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) ? data.price[0] : data.price }}
</view>
<view
v-if="goodsFields.original_price?.show && data.original_price > 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">{{ data.original_price }}</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 } from 'vue';
import sheep from '@/sheep';
import { formatSales } from '@/sheep/hooks/useGoods';
import { formatStock } from '@/sheep/hooks/useGoods';
import goodsCollectVue from '@/pages/user/goods-collect.vue';
import { isArray } from 'lodash';
// 数据
const state = reactive({});
// 接收参数
const props = defineProps({
goodsFields: {
type: [Array, Object],
default() {
return {
title: { show: true },
subtitle: { show: true },
price: { show: true },
original_price: { show: true },
sales: { show: true },
stock: { 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.sales?.show) {
text.push(formatSales(props.data.sales_show_type, props.data.sales));
}
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 calculatePanelHeight(e) {
if (props.size === 'md') {
const view = uni.createSelectorQuery().in(proxy);
view.select(`#${elId}`).fields({ size: true, scrollOffset: true });
view.exec((data) => {
const goodsPriceCard = data[0];
const card = {
width: goodsPriceCard.width,
height: (goodsPriceCard.width / e.detail.width) * e.detail.height + goodsPriceCard.height,
};
emits('getHeight', card.height);
});
}
}
</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>
@@ -0,0 +1,172 @@
<template>
<view class="ss-order-card-warp ss-flex ss-col-stretch ss-row-between bg-white">
<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"
>
{{ price }}
</view>
<view v-if="score && Number(price) > 0">+</view>
<view class="price-text ss-flex ss-col-center" v-if="score">
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
></image>
<view>{{ score }}</view>
</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 class="bottom-box">
<slot name="bottom"></slot>
</view>
</view>
</view>
</template>
<script setup>
import sheep from '@/sheep';
import { computed } from 'vue';
/**
* 订单卡片
*
* @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: '',
},
});
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>
@@ -0,0 +1,32 @@
<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"
></s-goods-column>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
/**
* 商品组 - 横向滚动商品
*/
const props = defineProps({
list: {
type: Array,
default() {
return [];
},
},
});
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,147 @@
<template>
<view>
<!-- 1 两张图片并排 图片坐文案右 -->
<view
v-if="mode === 1"
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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.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 })"
></s-goods-column>
</view>
</view>
<!-- 2 三张商品卡片并排 图片上文案下 -->
<view
v-if="mode === 2"
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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
></s-goods-column>
</view>
</view>
<!-- 3 商品卡片并排 轮播 -->
<view v-if="mode === 3" 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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:titleWidth="(750 - marginRight * 2 - data.space * 4 - marginLeft * 2) / 3"
@click="sheep.$router.go('/pages/goods/index', { id: item.id })"
></s-goods-column>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
/**
* 商品栏s-goods-column
*
* @description style 1:横向两个,左图右内容 2:横向三个,上图下内容 3:左右滚动
*/
import { onMounted, ref } from 'vue';
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
const { mode, tagStyle, buyNowStyle, goodsFields, goodsIds } = props.data;
let { marginLeft, marginRight } = props.styles;
const goodsList = ref([]);
onMounted(async () => {
if (goodsIds.length > 0) {
let { data } = await sheep.$api.goods.ids({ ids: goodsIds.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>
@@ -0,0 +1,156 @@
<!-- 装修组件 - 拼团 -->
<template>
<view>
<view
v-if="mode === 1"
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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@click="
sheep.$router.go('/pages/goods/groupon', {
id: item.id,
activity_id: props.data.activityId,
})
"
></s-goods-column>
</view>
</view>
<!-- 样式2 一行一个 图片左 文案右 -->
<view class="goods-box" v-if="mode == 2">
<view
class="goods-list"
v-for="(item, index) in goodsList"
:key="index"
:style="[{ marginBottom: space + 'px' }]"
>
<s-goods-column
class="goods-card"
size="lg"
:includes="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@click="
sheep.$router.go('/pages/goods/groupon', {
id: item.id,
activity_id: props.data.activityId,
})
"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-goods-column>
</view>
</view>
</view>
</template>
<script setup>
/**
* 拼团
*
* @property {Array} list - 商品列表
*
*
*/
import { computed, onMounted, ref } from 'vue';
import sheep from '@/sheep';
// 接收参数
const props = defineProps({
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
let { mode, tagStyle, buyNowStyle, goodsFields, space } = props.data;
let { marginLeft, marginRight } = props.styles;
// 购买按钮样式
const buyStyle = computed(() => {
let buyNowStyle = props.data.buyNowStyle;
if (buyNowStyle.mode == 1) {
return {
background: `linear-gradient(to right, ${buyNowStyle.color1}, ${buyNowStyle.color2})`,
};
}
if (buyNowStyle.mode == 2) {
return {
width: '54rpx',
height: '54rpx',
background: `url(${sheep.$url.cdn(buyNowStyle.src)}) no-repeat`,
backgroundSize: '100% 100%',
};
}
});
const goodsList = ref([]);
onMounted(async () => {
let { data } = await sheep.$api.goods.activity({ activity_id: props.data.activityId });
goodsList.value = data;
});
</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;
}
}
.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>
@@ -0,0 +1,39 @@
<template>
<su-swiper
:list="imgList"
:dotStyle="dotMap[data.indicator]"
imageMode="widthFix"
dotCur="bg-mask-40"
:seizeHeight="300"
/>
</template>
<script setup>
import { computed } from 'vue';
import sheep from '@/sheep';
const dotMap = {
1: 'long',
2: 'tag',
};
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
styles: {
type: Object,
default: () => ({}),
},
});
const imgList = computed(() =>
props.data.list.map((item) => ({
...item,
src: sheep.$url.cdn(item.src),
poster: sheep.$url.cdn(item.poster),
})),
);
</script>
<style></style>
@@ -0,0 +1,26 @@
<template>
<view @tap="sheep.$router.go(data?.url)">
<su-image :src="sheep.$url.cdn(data.src)" mode="widthFix"></su-image>
</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>
@@ -0,0 +1,109 @@
<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 + 'rpx' }]"
@tap="sheep.$router.go(item.url)"
>
<image class="cube-img" :src="sheep.$url.cdn(item.src)" 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 + props.styles.marginRight + props.styles.padding * 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>
@@ -0,0 +1,96 @@
<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="address-text">{{ props.item.name }}</view>
<view class="person-text">{{ props.item.mobile }}</view>
</view>
<view v-else>
<view class="address-text">请选择收货地址</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')"></image>
</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 _ from 'lodash';
const props = defineProps({
item: {
type: Object,
default() {},
},
hasBorderBottom: {
type: Boolean,
defult: true,
},
});
const onEdit = () => {
sheep.$router.go('/pages/user/invoice/edit', {
id: props.item.id,
});
};
</script>
<style lang="scss" scoped>
.address-item {
padding: 30rpx;
.item-left {
width: 600rpx;
}
.area-text {
font-size: 26rpx;
font-weight: 400;
color: $dark-9;
}
.address-text {
font-size: 32rpx;
font-weight: 600;
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>
+244
View File
@@ -0,0 +1,244 @@
<template>
<view
class="page-app"
:class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
>
<view class="page-main" :style="[bgMain]">
<!-- 默认通用顶部导航栏 -->
<su-navbar
v-if="navbar === 'normal'"
:title="title"
statusBar
:color="color"
:tools="tools"
:opacityBgUi="opacityBgUi"
@search="(e) => emits('search', e)"
:defaultSearch="defaultSearch"
/>
<!-- 装修组件导航栏-普通 -->
<s-custom-navbar
v-else-if="navbar === 'custom' && navbarMode === 'normal'"
:data="navbarStyle"
/>
<view class="page-body" :style="[bgBody]">
<!-- 沉浸式头部 -->
<su-inner-navbar v-if="navbar === 'inner'" :title="title" />
<view
v-if="navbar === 'inner'"
:style="[{ paddingTop: sheep.$platform.navbar + 'px' }]"
></view>
<!-- 装修组件导航栏-沉浸式 -->
<s-custom-navbar v-if="navbar === 'custom' && navbarMode === 'inner'" :data="navbarStyle" />
<!-- 页面内容插槽 -->
<slot />
<!-- 悬浮按钮 -->
<s-float-menu v-if="showFloatButton"></s-float-menu>
<!-- 底部导航 -->
<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';
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: () => ({
mode: '',
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: '',
},
//展示悬浮按钮
showFloatButton: {
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.mode === 'normal') {
return 'normal';
}
return 'inner';
});
// 背景1
const bgMain = computed(() => {
if (navbarMode.value === 'inner') {
return {
background: `${props.bgStyle.color} url(${sheep.$url.cdn(
props.bgStyle.src,
)}) no-repeat top center / 100% auto`,
};
}
return {};
});
// 背景2
const bgBody = computed(() => {
if (navbarMode.value === 'normal') {
return {
background: `${props.bgStyle.color} url(${sheep.$url.cdn(
props.bgStyle.src,
)}) 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>
@@ -0,0 +1,14 @@
<template>
<su-subline :color="data.lineColor" :lineStyle="data.style"></su-subline>
</template>
<script setup>
const props = defineProps({
data: {
type: Object,
default() {},
},
});
</script>
<style></style>
@@ -0,0 +1,368 @@
<template>
<!-- 包裹层 -->
<view
class="ui-swiper"
:class="[props.mode, props.bg, props.ui]"
:style="[{ 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" :col="data.rowNum">
<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: clWidth + 'px', 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.color }]"
>
{{ item.badge.text }}
</view>
<image
v-if="item.src"
class="menu-icon"
:style="[
{
width: props.iconSize + 'rpx',
height: props.iconSize + 'rpx',
},
]"
:src="sheep.$url.cdn(item.src)"
mode="aspectFill"
></image>
<view
v-if="data.layout == 1"
class="menu-title"
:style="[{ color: item.title.color }]"
>
{{ item.title.text }}
</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',
},
bg: {
type: String,
default: 'bg-none',
},
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: 96,
},
color: {
type: String,
default: '#000',
},
});
// 生成数据
const menuList = computed(() => splitData(props.data.list, props.data.row * props.data.col));
const swiperHeight = computed(() => props.data.row * (props.data.layout == 1 ? 200 : 180));
const windowWidth = sheep.$platform.device.windowWidth;
const clWidth = computed(
() =>
(windowWidth -
(props.styles.marginLeft + props.styles.marginRight + props.styles.padding * 2)) /
props.data.col,
);
// 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: 98rpx;
height: 98rpx;
padding-bottom: 10rpx;
}
.menu-title {
font-size: 30rpx;
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>
@@ -0,0 +1,81 @@
<template>
<uni-grid :showBorder="Boolean(data.border)" :column="data.col">
<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.color }]"
>
{{ item.badge.text }}
</view>
<image class="menu-image" :src="sheep.$url.cdn(item.src)"></image>
</view>
<view class="title-box ss-flex ss-flex-col ss-row-center ss-col-center">
<view class="grid-text" :style="[{ color: item.title.color }]">
{{ item.title.text }}
</view>
<view class="grid-tip" :style="[{ color: item.tip.color }]">
{{ item.tip.text }}
</view>
</view>
</view>
</uni-grid-item>
</uni-grid>
</template>
<script setup>
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default() {},
},
});
</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>
@@ -0,0 +1,65 @@
<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.src"
class="list-icon"
:src="sheep.$url.cdn(item.src)"
mode="aspectFit"
></image>
<view
class="title-text ss-flex ss-row-center ss-col-center ss-m-l-20"
:style="[{ color: item.title.color }]"
>
{{ item.title.text }}
</view>
</view>
</template>
<template v-slot:footer>
<view
class="notice-text ss-flex ss-row-center ss-col-center"
:style="[{ color: item.tip.color }]"
>
{{ item.tip.text }}
</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>
@@ -0,0 +1,122 @@
<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"></image>
</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/index/index',
icon: '/static/img/shop/tools/home.png',
title: '首页',
},
{
url: '/pages/index/search',
icon: '/static/img/shop/tools/search.png',
title: '搜索',
},
{
url: '/pages/index/user',
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/user/goods-collect',
icon: '/static/img/shop/tools/collect.png',
title: '我的收藏',
},
{
url: '/pages/public/feedback',
icon: '/static/img/shop/tools/feedback.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>
@@ -0,0 +1,37 @@
<template>
<view class="ss-flex ss-col-center notice-wrap">
<image class="icon-img" :src="sheep.$url.cdn(data.src)" mode="heightFix"></image>
<su-notice-bar
style="flex: 1"
:showIcon="false"
scrollable
single
:text="data.title.text"
:speed="50"
:color="data.title.color"
@tap="sheep.$router.go(data.url)"
></su-notice-bar>
</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: 60rpx;
}
}
</style>
@@ -0,0 +1,102 @@
<template>
<view class="ss-order-menu-wrap ss-flex ss-col-center">
<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.order_num[item.type]"
absolute="rightTop"
size="small"
>
<image class="item-icon" :src="sheep.$url.static(item.icon)" mode="aspectFit"></image>
</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',
},
{
title: '待收货',
value: '3',
icon: '/static/img/shop/order/no_take.png',
path: '/pages/order/list',
type: 'noget',
},
{
title: '待评价',
value: '4',
icon: '/static/img/shop/order/no_comment.png',
path: '/pages/order/list',
type: 'nocomment',
},
{
title: '售后单',
value: '0',
icon: '/static/img/shop/order/change_order.png',
path: '/pages/order/aftersale/list',
type: 'aftersale',
},
{
title: '全部订单',
value: '0',
icon: '/static/img/shop/order/all_order.png',
path: '/pages/order/list',
},
];
const numData = computed(() => sheep.$store('user').numData);
</script>
<style lang="scss" scoped>
.ss-order-menu-wrap {
.menu-item {
height: 160rpx;
position: relative;
.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>
@@ -0,0 +1,73 @@
<template>
<view>
<view v-for="(item, index) in popupList" :key="item.src">
<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.src)"
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 { onShow } from '@dcloudio/uni-app';
import { saveAdvHistory } from '@/sheep/hooks/useModal';
// const modalStore = sheep.$store('modal');
const modalStore = JSON.parse(uni.getStorageSync('modal-store') || '{}');
const advHistory = modalStore.advHistory || [];
const currentIndex = ref(0);
const popupList = computed(() => {
const list = sheep.$store('app').template.basic.popupImage?.list || [];
const newList = [];
if (list.length > 0) {
list.forEach((adv) => {
if (adv.show === 1 && advHistory.includes(adv.src)) {
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>
@@ -0,0 +1,38 @@
<template>
<view
:style="[
{
marginLeft: styles.marginLeft + 'px',
marginRight: styles.marginRight + 'px',
marginBottom: styles.marginBottom + 'px',
marginTop: styles.marginTop + 'px',
padding: styles.padding + 'px',
},
]"
>
<su-parse class="richtext" :content="state.content"></su-parse>
</view>
</template>
<script setup>
import { reactive, onMounted } from 'vue';
import sheep from '@/sheep';
const props = defineProps({
data: {
type: Object,
default: {},
},
styles: {
type: Object,
default() {},
},
});
const state = reactive({
content: '',
});
onMounted(async () => {
const { error, data } = await sheep.$api.data.richtext(props.data.id);
if (error === 0) {
state.content = data.content;
}
});
</script>
@@ -0,0 +1,202 @@
<template>
<view>
<view
v-if="mode === 1 && state.scoreList.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.leftScoreList"
:key="item.id"
>
<s-score-card
class="goods-md-box"
size="md"
:goodsFields="goodsFields"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
:titleWidth="330 - marginLeft - marginRight"
@click="sheep.$router.go('/pages/goods/score', { id: item.id })"
@getHeight="mountMasonry($event, 'left')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-score-card>
</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.rightScoreList"
:key="item.id"
>
<s-score-card
class="goods-md-box"
size="md"
:goodsFields="goodsFields"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
:titleWidth="330 - marginLeft - marginRight"
@click="sheep.$router.go('/pages/goods/score', { id: item.id })"
@getHeight="mountMasonry($event, 'right')"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-score-card>
</view>
</view>
<!-- <view class="goods-hack" v-if="state.scoreList.length % 2 == 1" style="width: 345rpx"></view> -->
</view>
<view v-if="mode === 2 && state.scoreList.length" class="goods-lg-box">
<view
class="goods-box"
:style="[{ marginBottom: data.space + 'px' }]"
v-for="item in state.scoreList"
:key="item.id"
>
<s-score-card
class="goods-card"
size="lg"
:goodsFields="goodsFields"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@tap="sheep.$router.go('/pages/goods/score', { id: item.id })"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-score-card>
</view>
</view>
</view>
</template>
<script setup>
import { computed, reactive, onMounted } from 'vue';
import sheep from '@/sheep';
const state = reactive({
scoreList: [],
leftScoreList: [],
rightScoreList: [],
});
const props = defineProps({
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
const { mode, buyNowStyle, goodsFields, goodsIds } = props.data ?? {};
const { marginLeft, marginRight } = props.styles ?? {};
async function getScoreListByIds(ids) {
let { data } = await sheep.$api.app.scoreShopIds({ ids });
return data;
}
onMounted(async () => {
state.scoreList = await getScoreListByIds(goodsIds.join(','));
if (mode === 1) {
mountMasonry();
}
});
// 加载瀑布流
let count = 0;
let leftHeight = 0;
let rightHeight = 0;
function mountMasonry(height = 0, where = 'left') {
if (!state.scoreList[count]) return;
if (where === 'left') leftHeight += height;
if (where === 'right') rightHeight += height;
if (leftHeight <= rightHeight) {
state.leftScoreList.push(state.scoreList[count]);
} else {
state.rightScoreList.push(state.scoreList[count]);
}
count++;
}
// 购买按钮样式
const buyStyle = computed(() => {
if (buyNowStyle.mode == 1) {
// button
return {
background: `linear-gradient(to right, ${buyNowStyle.color1}, ${buyNowStyle.color2})`,
};
}
if (buyNowStyle.mode == 2) {
// image
return {
width: '54rpx',
height: '54rpx',
background: `url(${sheep.$url.cdn(buyNowStyle.src)}) no-repeat`,
backgroundSize: '100% 100%',
};
}
});
</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;
}
}
}
.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: 10rpx;
right: 10rpx;
z-index: 11;
height: 50rpx;
line-height: 50rpx;
padding: 0 20rpx;
border-radius: 25rpx;
font-size: 24rpx;
color: #fff;
}
}
</style>
@@ -0,0 +1,459 @@
<template>
<view>
<!-- md卡片竖向一行放两个图上内容下 -->
<view v-if="size === 'md'" class="md-goods-card ss-flex-col" :style="[elStyles]" @tap="onClick">
<image
class="md-img-box"
:src="sheep.$url.cdn(data.image)"
mode="widthFix"
@load="calculatePanelHeight"
></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"
class="md-goods-title ss-line-1"
:style="[{ color: titleColor, width: titleWidth ? titleWidth + 'rpx' : '' }]"
>
{{ data.title }}
</view>
<view
v-if="goodsFields.subtitle?.show"
class="md-goods-subtitle ss-m-t-16 ss-line-1"
:style="[{ color: subTitleColor }]"
>
{{ data.subtitle }}
</view>
<view class="ss-col-bottom">
<view
v-if="goodsFields.score_price?.show"
class="md-goods-price ss-m-t-16 font-OPPOSANS ss-m-r-10 ss-flex"
:style="[{ color: goodsFields.score_price.color }]"
>
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
></image>
{{ data.score }}
</view>
<view
v-if="goodsFields.price?.show && data.original_price > 0"
class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
:style="[{ color: goodsFields.price.color }]"
>
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
<view class="ss-m-l-8">{{ data.original_price }}</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=""></image>
</view>
</slot>
</view>
<!-- lg卡片横向型一行放一个图片左内容右边 -->
<view
v-if="size === 'lg'"
class="lg-goods-card ss-flex ss-col-stretch"
:style="[elStyles]"
@tap="onClick"
>
<image class="lg-img-box" :src="sheep.$url.cdn(data.image)" 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 class="ss-m-r-20">
<view
v-if="goodsFields.title?.show"
class="lg-goods-title ss-line-2"
:style="[{ color: titleColor }]"
>
{{ data.title }}
</view>
<view
v-if="goodsFields.subtitle?.show"
class="lg-goods-subtitle ss-m-t-10 ss-line-1"
:style="[{ color: subTitleColor }]"
>
{{ data.subtitle }}
</view>
</view>
<view>
<view class="ss-m-t-10">
<view
v-if="goodsFields.score_price?.show"
class="lg-goods-price ss-m-r-12 ss-flex ss-col-bottom font-OPPOSANS"
:style="[{ color: goodsFields.score_price.color }]"
>
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
></image>
{{ data.score }}
</view>
<view
v-if="goodsFields.price?.show && data.original_price > 0"
class="goods-origin-price ss-flex ss-col-bottom font-OPPOSANS"
:style="[{ color: goodsFields.price.color }]"
>
<text class="price-unit ss-font-20">{{ priceUnit }}</text>
<view class="ss-m-l-8">{{ data.original_price }}</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>
</view>
<slot name="cart"
><view class="buy-box ss-flex ss-col-center ss-row-center">去兑换</view></slot
>
</view>
<!-- sl卡片竖向型一行放一个图片上内容下边 -->
<view v-if="size === 'sl'" class="sl-goods-card ss-flex-col" @tap="onClick">
<image class="sl-img-box" :src="sheep.$url.cdn(data.image)" mode="aspectFill"></image>
<view class="sl-goods-content ss-flex-col ss-row-between ss-p-b-20 ss-p-t-20">
<view class="ss-m-b-20">
<view class="sl-goods-title ss-line-1 ss-p-l-16 ss-p-r-16">
{{ data.title }}
</view>
<view v-if="data.subtitle" class="sl-goods-subtitle ss-p-l-16 ss-p-r-16 ss-m-t-16">
{{ data.subtitle }}
</view>
</view>
<view>
<slot name="activity">
<view
v-if="data.promos?.length"
class="tag-box ss-flex ss-col-center ss-flex-wrap ss-p-l-16 ss-p-r-16"
>
<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 ss-p-l-16 ss-p-r-16 font-OPPOSANS">
<view class="sl-goods-price ss-m-r-12 ss-flex">
<view>{{ Number(data.price[0]) > 0 ? '¥' + data.price[0] + '+' : '' }}</view>
<image
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
></image>
<view>{{ data.score ? data.score : '' }}</view>
</view>
<view
v-if="data.original_price > 0"
class="goods-origin-price ss-m-t-16 font-OPPOSANS ss-flex"
>
<text class="price-unit ss-font-20"></text>
<view class="ss-m-l-8">{{ data.original_price }}</view>
</view>
</view>
<view class="ss-p-l-16 ss-p-r-16 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>
import { computed, getCurrentInstance } from 'vue';
import sheep from '@/sheep';
import { formatSales } from '@/sheep/hooks/useGoods';
import { formatStock } from '@/sheep/hooks/useGoods';
import goodsCollectVue from '@/pages/user/goods-collect.vue';
/**
* 订单卡片
*
* @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 {
title: { show: true },
subtitle: { show: true },
price: { show: true },
original_price: { show: true },
sales: { show: true },
stock: { 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',
},
priceUnit: {
type: String,
default: '¥',
},
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 emits = defineEmits(['click', 'getHeight']);
const onClick = () => {
emits('click');
};
// 格式化销量、库存信息
const salesAndStock = computed(() => {
let text = [];
text.push(formatSales(props.data.sales_show_type, props.data.sales));
text.push(formatStock(props.data.stock_show_type, props.data.stock));
return text.join(' | ');
});
// 获取实时卡片高度
const { proxy } = getCurrentInstance();
const elId = `sheep_${Math.ceil(Math.random() * 10e5).toString(36)}`;
function calculatePanelHeight(e) {
if (props.size === 'md') {
const view = uni.createSelectorQuery().in(proxy);
view.select(`#${elId}`).fields({ size: true, scrollOffset: true });
view.exec((data) => {
const goodsPriceCard = data[0];
const card = {
width: goodsPriceCard.width,
height: (goodsPriceCard.width / e.detail.width) * e.detail.height + goodsPriceCard.height,
};
emits('getHeight', card.height);
});
}
}
</script>
<style lang="scss" scoped>
.price-unit {
margin-right: -4px;
}
.sales-text {
display: table;
font-size: 24rpx;
transform: scale(0.8);
margin-left: -16rpx;
color: #c4c4c4;
}
// 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, var(--ui-BG-Main), var(--ui-BG-Main-gradient));
border-radius: 25rpx;
font-size: 24rpx;
color: #ffffff;
}
.tag-box {
width: 100%;
}
}
.sl-goods-card {
overflow: hidden;
position: relative;
z-index: 1;
width: 100%;
background-color: $white;
.sl-img-box {
width: 100%;
height: 360rpx;
}
.sl-goods-title {
font-size: 26rpx;
color: #333;
width: 100%;
box-sizing: border-box;
}
.sl-goods-subtitle {
font-size: 24rpx;
font-weight: 400;
color: #999999;
line-height: 30rpx;
width: 100%;
box-sizing: border-box;
}
.sl-goods-price {
font-size: 30rpx;
color: $red;
}
.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;
}
}
.goods-origin-price {
font-size: 20rpx;
color: #c4c4c4;
text-decoration: line-through;
}
.score-img {
width: 36rpx;
height: 36rpx;
margin: 0 4rpx;
}
</style>
@@ -0,0 +1,158 @@
<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"
/>
<view class="keyword-link ss-flex">
<view v-for="(item, index) in data.keywords" :key="index">
<view
class="ss-m-r-16"
:style="[{ color: item.color }]"
@tap.stop="sheep.$router.go('/pages/goods/list', { keyword: item.text })"
>{{ item.text }}</view
>
</view>
</view>
<view v-if="data.keywords && data.keywords.length && navbar" class="ss-flex">
<button
class="ss-reset-button keyword-btn"
v-for="(item, index) in data.keywords"
:key="index"
:style="[{ color: item.color, marginRight: '10rpx' }]"
>
{{ item.text }}
</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 });
}
}
</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>
@@ -0,0 +1,158 @@
<!-- 装修组件 - 秒杀 -->
<template>
<view>
<view
v-if="mode === 1"
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="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@click="
sheep.$router.go('/pages/goods/seckill', {
id: item.id,
activity_id: props.data.activityId,
})
"
></s-goods-column>
</view>
</view>
<!-- 样式2 -->
<view class="goods-box" v-if="mode == 2">
<view
class="goods-list"
v-for="(item, index) in goodsList"
:key="index"
:style="[{ marginBottom: space + 'px' }]"
>
<s-goods-column
class="goods-card"
size="lg"
:includes="goodsFields"
:tagStyle="tagStyle"
:data="item"
:titleColor="goodsFields.title?.color"
:subTitleColor="goodsFields.subtitle.color"
:topRadius="data.borderRadiusTop"
:bottomRadius="data.borderRadiusBottom"
@click="
sheep.$router.go('/pages/goods/seckill', {
id: item.id,
activity_id: props.data.activityId,
})
"
>
<template v-slot:cart>
<button class="ss-reset-button cart-btn" :style="[buyStyle]">
{{ buyNowStyle.mode === 1 ? buyNowStyle.text : '' }}
</button>
</template>
</s-goods-column>
</view>
</view>
</view>
</template>
<script setup>
/**
* 秒杀
*
* @property {Array} list - 商品列表
*
*
*/
import { computed, onMounted, ref } from 'vue';
import sheep from '@/sheep';
// 接收参数
const props = defineProps({
data: {
type: Object,
default() {},
},
styles: {
type: Object,
default() {},
},
});
let { mode, tagStyle, buyNowStyle, goodsFields, space } = props.data;
let { marginLeft, marginRight } = props.styles;
// 购买按钮样式
const buyStyle = computed(() => {
let buyNowStyle = props.data.buyNowStyle;
if (buyNowStyle.mode == 1) {
return {
background: `linear-gradient(to right, ${buyNowStyle.color1}, ${buyNowStyle.color2})`,
};
}
if (buyNowStyle.mode == 2) {
return {
width: '54rpx',
height: '54rpx',
background: `url(${sheep.$url.cdn(buyNowStyle.src)}) no-repeat`,
backgroundSize: '100% 100%',
};
}
});
const goodsList = ref([]);
onMounted(async () => {
let { data } = await sheep.$api.goods.activity({ activity_id: props.data.activityId });
goodsList.value = data;
});
</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;
}
}
.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>
@@ -0,0 +1,569 @@
<template>
<!-- 规格弹窗 -->
<su-popup :show="show" round="10" @close="emits('close')">
<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.selectedSkuPrice.image || goodsInfo.image)"
mode="aspectFill"
>
</image>
</view>
<view class="header-right ss-flex-col ss-row-between ss-flex-1">
<view class="goods-title ss-line-2">{{ goodsInfo.title }}</view>
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
<view class="price-text"> {{ goodsPrice }}</view>
<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')">
</image>
</view>
</view>
<view class="tig-title">拼团价</view>
</view>
<view class="stock-text ss-m-l-20">
库存{{ state.selectedSkuPrice.stock || goodsInfo.stock }}
</view>
</view>
</view>
</view>
<view class="modal-content ss-flex-1">
<scroll-view scroll-y="true" class="modal-content-scroll">
<view
v-if="grouponAction === 'create' && activityType === 'groupon_ladder'"
class="sku-item ss-m-b-20"
>
<view class="label-text ss-m-b-20">拼团人数</view>
<view class="ss-flex ss-col-center ss-flex-wrap">
<button
v-for="(ladder, key) in goodsInfo.activity.rules.ladders"
:key="key"
class="ss-reset-button spec-btn"
:class="[
{
'checked-btn': grouponNum == ladder,
},
]"
@tap="onSelectLadder(ladder)"
>
{{ ladder }}人团
</button>
</view>
</view>
<view class="sku-item ss-m-b-20" v-for="sku1 in goodsInfo.skus" :key="sku1.id">
<view class="label-text ss-m-b-20">{{ sku1.name }}</view>
<view class="ss-flex ss-col-center ss-flex-wrap">
<button
class="ss-reset-button spec-btn"
v-for="sku2 in sku1.children"
:class="[
{
'checked-btn': state.currentSkuArray[sku2.parent_id] == sku2.id,
},
{
'disabled-btn': sku2.disabled == true,
},
]"
:key="sku2.id"
:disabled="sku2.disabled == true"
@tap="onSelectSku(sku2.parent_id, sku2.id)"
>
{{ sku2.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.selectedSkuPrice.stock"
:step="1"
v-model="state.selectedSkuPrice.goods_num"
activity="groupon"
></su-number-box>
</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">
{{
grouponAction === 'create' && goodsInfo.activity.rules.is_leader_discount == 1
? leaderPrice
: goodsPrice
}}
</view>
<view
v-if="
grouponAction === 'create' && goodsInfo.activity.rules.is_leader_discount == 0
"
>立即开团</view
>
<view
v-else-if="
grouponAction === 'create' && goodsInfo.activity.rules.is_leader_discount == 1
"
>团长立减价</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 { formatPrice } from '@/sheep/hooks/useGoods';
import { isEmpty } from 'lodash';
const emits = defineEmits(['change', 'addCart', 'buy', 'close', 'update:grouponNum']);
const props = defineProps({
goodsInfo: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
grouponAction: {
type: String,
default: 'create',
},
grouponNum: {
type: [Number, String],
default: 0,
},
});
const state = reactive({
selectedSkuPrice: {},
currentSkuArray: [],
});
// 默认单规格
if (!props.goodsInfo.is_sku) {
state.selectedSkuPrice = props.goodsInfo.sku_prices[0];
}
// 活动类型
const activityType = props.goodsInfo.activity_type;
// 可选规格
const skuPrices = computed(() => {
let skuPrices = props.goodsInfo.sku_prices;
if (props.goodsInfo.is_sku) {
skuPrices.forEach((item) => {
item.goods_sku_id_arr = item.goods_sku_ids.split(',');
});
}
return skuPrices;
});
const skuList = props.goodsInfo.skus;
// 规格价格
const goodsPrice = computed(() => {
if (isEmpty(state.selectedSkuPrice)) {
return formatPrice(props.goodsInfo.price);
}
if (activityType === 'groupon') {
return state.selectedSkuPrice.groupon_price;
}
if (activityType === 'groupon_ladder') {
const ladder = getSkuPriceByLadder();
state.selectedSkuPrice.ladder_price = ladder.ladder_price;
return ladder.ladder_price;
}
});
// 团长优惠
const leaderPrice = computed(() => {
if (isEmpty(state.selectedSkuPrice)) {
return formatPrice(props.goodsInfo.price);
}
if (activityType === 'groupon') {
return state.selectedSkuPrice.leader_price;
}
if (activityType === 'groupon_ladder') {
const ladder = getSkuPriceByLadder();
return ladder.leader_ladder_price;
}
});
// 获取阶梯价
function getSkuPriceByLadder() {
return state.selectedSkuPrice.ladders.find((item) => item.ladder == props.grouponNum);
}
watch(
() => state.selectedSkuPrice,
(newVal) => {
emits('change', newVal);
},
{
immediate: true, // 立即执行
deep: true, // 深度监听
},
);
// 点击购买
function onBuy() {
if (!state.selectedSkuPrice.goods_id) {
sheep.$helper.toast('请选择规格');
return;
}
if (state.selectedSkuPrice.stock <= 0) {
sheep.$helper.toast('库存不足');
return;
}
emits('buy', state.selectedSkuPrice);
}
// 改变禁用状态
function changeDisabled(isChecked = false, pid = 0, skuId = 0) {
let newPrice = []; // 所有可以选择的 skuPrice
if (isChecked) {
// 选中规格
// 当前点击选中规格下的 所有可用 skuPrice
for (let price of skuPrices.value) {
if (price.stock <= 0) {
// this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
if (price.goods_sku_id_arr.indexOf(skuId.toString()) >= 0) {
newPrice.push(price);
}
}
} else {
// 取消选中
// 当前所选规格下,所有可以选择的 skuPrice
newPrice = getCanUseSkuPrice();
}
// 所有存在并且有库存未选择的规格项 的 子项 id
let noChooseSkuIds = [];
for (let price of newPrice) {
noChooseSkuIds = noChooseSkuIds.concat(price.goods_sku_id_arr);
}
// 去重
noChooseSkuIds = Array.from(new Set(noChooseSkuIds));
if (isChecked) {
// 去除当前选中的规格项
let index = noChooseSkuIds.indexOf(skuId.toString());
noChooseSkuIds.splice(index, 1);
} else {
// 循环去除当前已选择的规格项
state.currentSkuArray.forEach((sku) => {
if (sku.toString() != '') {
// sku 为空是反选 填充的
let index = noChooseSkuIds.indexOf(sku.toString());
if (index >= 0) {
// sku 存在于 noChooseSkuIds
noChooseSkuIds.splice(index, 1);
}
}
});
}
// 当前已选择的规格大类
let chooseSkuKey = [];
if (!isChecked) {
// 当前已选择的规格大类
state.currentSkuArray.forEach((sku, key) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuKey.push(key);
}
});
} else {
// 当前点击选择的规格大类
chooseSkuKey = [pid];
}
for (let i in skuList) {
// 当前点击的规格,或者取消选择时候 已选中的规格 不进行处理
if (chooseSkuKey.indexOf(skuList[i]['id']) >= 0) {
continue;
}
for (let j in skuList[i]['children']) {
// 如果当前规格项 id 不存在于有库存的规格项中,则禁用
if (noChooseSkuIds.indexOf(skuList[i]['children'][j]['id'].toString()) >= 0) {
skuList[i]['children'][j]['disabled'] = false;
} else {
skuList[i]['children'][j]['disabled'] = true;
}
}
}
}
// 当前所选规格下,获取所有有库存的 skuPrice
function getCanUseSkuPrice() {
let newPrice = [];
for (let price of skuPrices.value) {
if (price.stock <= 0) {
// || price.stock < this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
var isOk = true;
state.currentSkuArray.forEach((sku) => {
// sku 不为空,并且,这个 条 skuPrice 没有被选中,则排除
if (sku.toString() != '' && price.goods_sku_id_arr.indexOf(sku.toString()) < 0) {
isOk = false;
}
});
if (isOk) {
newPrice.push(price);
}
}
return newPrice;
}
// 选择阶梯拼团人数
function onSelectLadder(ladder) {
emits('update:grouponNum', ladder);
}
// 选择规格
function onSelectSku(pid, skuId) {
// 清空已选择
if (activityType === 'groupon_ladder' && props.grouponNum == 0) {
sheep.$helper.toast('请选择拼团人数');
return;
}
let isChecked = true; // 选中 or 取消选中
if (state.currentSkuArray[pid] != undefined && state.currentSkuArray[pid] == skuId) {
// 点击已被选中的,删除并填充 ''
isChecked = false;
state.currentSkuArray.splice(pid, 1, '');
} else {
// 选中
state.currentSkuArray[pid] = skuId;
}
let chooseSkuId = []; // 选中的规格大类
state.currentSkuArray.forEach((sku) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuId.push(sku);
}
});
// 当前所选规格下,所有可以选择的 skuPric
let newPrice = getCanUseSkuPrice();
// 判断所有规格大类是否选择完成
if (chooseSkuId.length == skuList.length && newPrice.length) {
newPrice[0].goods_num = state.selectedSkuPrice.goods_num || 1;
state.selectedSkuPrice = newPrice[0];
} else {
state.selectedSkuPrice = {};
}
// 改变规格项禁用状态
changeDisabled(isChecked, pid, skuId);
}
changeDisabled(false);
</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("sheep.$url.css('/static/img/shop/goods/groupon-btn-long.png')");
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;
}
.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;
}
}
}
}
.tig {
border: 2rpx solid #ff6000;
border-radius: 4rpx;
width: 126rpx;
height: 38rpx;
.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;
}
}
image {
width: 100%;
height: 100%;
}
</style>
@@ -0,0 +1,433 @@
<template>
<!-- 规格弹窗 -->
<su-popup :show="show" round="10" @close="emits('close')">
<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.selectedSkuPrice.image || state.goodsInfo.image)"
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.title }}</view>
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
<view class="price-text">
{{ state.selectedSkuPrice.groupon_price || formatPrice(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.selectedSkuPrice.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="sku1 in state.goodsInfo.skus" :key="sku1.id">
<view class="label-text ss-m-b-20">{{ sku1.name }}</view>
<view class="ss-flex ss-col-center ss-flex-wrap">
<button
class="ss-reset-button spec-btn"
v-for="sku2 in sku1.children"
:class="[
{
'checked-btn': state.currentSkuArray[sku2.parent_id] == sku2.id,
},
{
'disabled-btn': sku2.disabled == true,
},
]"
:key="sku2.id"
:disabled="sku2.disabled == true"
@tap="onSelectSku(sku2.parent_id, sku2.id)"
>
{{ sku2.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.selectedSkuPrice.stock"
:step="1"
v-model="state.selectedSkuPrice.goods_num"
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>
/**
* 1. 根据spus获得skus 后台已算好
* 2. 用路径字典,生成路径map
* 3. 计算用户选择后,其他路径是否可用
*/
// 按钮状态: active,nostock
import { computed, reactive, watch } from 'vue';
import sheep from '@/sheep';
const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
const props = defineProps({
modelValue: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
});
const state = reactive({
goodsInfo: computed(() => props.modelValue),
skuPrices: computed(() => {
let skuPrices = props.modelValue.sku_prices;
if (props.modelValue.is_sku) {
skuPrices.forEach((item) => {
item.goods_sku_id_arr = item.goods_sku_ids.split(',');
});
}
return skuPrices;
}),
skuList: props.modelValue.skus,
selectedSkuPrice: {},
currentSkuArray: [],
});
if (!state.goodsInfo.is_sku) {
state.selectedSkuPrice = state.goodsInfo.sku_prices[0];
}
watch(
() => state.selectedSkuPrice,
(newVal) => {
emits('change', newVal);
},
{
immediate: true, // 立即执行
deep: true, // 深度监听
},
);
// 格式化价格
const formatPrice = (e) => {
return e.length === 1 ? e[0] : e.join('~');
};
const onAddCart = () => {
if (state.selectedSkuPrice.goods_id) {
if (state.selectedSkuPrice.stock <= 0) {
sheep.$helper.toast('库存不足');
} else {
emits('addCart', state.selectedSkuPrice);
}
} else {
sheep.$helper.toast('请选择规格');
}
};
const onBuy = () => {
if (state.selectedSkuPrice.goods_id) {
if (state.selectedSkuPrice.stock <= 0) {
sheep.$helper.toast('库存不足');
} else {
emits('buy', state.selectedSkuPrice);
}
} else {
sheep.$helper.toast('请选择规格');
}
};
// 改变禁用状态
const changeDisabled = (isChecked = false, pid = 0, skuId = 0) => {
let newPrice = []; // 所有可以选择的 skuPrice
if (isChecked) {
// 选中规格
// 当前点击选中规格下的 所有可用 skuPrice
for (let price of state.skuPrices) {
if (price.stock <= 0) {
// this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
if (price.goods_sku_id_arr.indexOf(skuId.toString()) >= 0) {
newPrice.push(price);
}
}
} else {
// 取消选中
// 当前所选规格下,所有可以选择的 skuPrice
newPrice = getCanUseSkuPrice();
}
// 所有存在并且有库存未选择的规格项 的 子项 id
let noChooseSkuIds = [];
for (let price of newPrice) {
noChooseSkuIds = noChooseSkuIds.concat(price.goods_sku_id_arr);
}
// 去重
noChooseSkuIds = Array.from(new Set(noChooseSkuIds));
if (isChecked) {
// 去除当前选中的规格项
let index = noChooseSkuIds.indexOf(skuId.toString());
noChooseSkuIds.splice(index, 1);
} else {
// 循环去除当前已选择的规格项
state.currentSkuArray.forEach((sku) => {
if (sku.toString() != '') {
// sku 为空是反选 填充的
let index = noChooseSkuIds.indexOf(sku.toString());
if (index >= 0) {
// sku 存在于 noChooseSkuIds
noChooseSkuIds.splice(index, 1);
}
}
});
}
// 当前已选择的规格大类
let chooseSkuKey = [];
if (!isChecked) {
// 当前已选择的规格大类
state.currentSkuArray.forEach((sku, key) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuKey.push(key);
}
});
} else {
// 当前点击选择的规格大类
chooseSkuKey = [pid];
}
for (let i in state.skuList) {
// 当前点击的规格,或者取消选择时候 已选中的规格 不进行处理
if (chooseSkuKey.indexOf(state.skuList[i]['id']) >= 0) {
continue;
}
for (let j in state.skuList[i]['children']) {
// 如果当前规格项 id 不存在于有库存的规格项中,则禁用
if (noChooseSkuIds.indexOf(state.skuList[i]['children'][j]['id'].toString()) >= 0) {
state.skuList[i]['children'][j]['disabled'] = false;
} else {
state.skuList[i]['children'][j]['disabled'] = true;
}
}
}
};
// 当前所选规格下,获取所有有库存的 skuPrice
const getCanUseSkuPrice = () => {
let newPrice = [];
for (let price of state.skuPrices) {
if (price.stock <= 0) {
// || price.stock < this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
var isOk = true;
state.currentSkuArray.forEach((sku) => {
// sku 不为空,并且,这个 条 skuPrice 没有被选中,则排除
if (sku.toString() != '' && price.goods_sku_id_arr.indexOf(sku.toString()) < 0) {
isOk = false;
}
});
if (isOk) {
newPrice.push(price);
}
}
return newPrice;
};
// 选择规格
const onSelectSku = (pid, skuId) => {
// 清空已选择
let isChecked = true; // 选中 or 取消选中
if (state.currentSkuArray[pid] != undefined && state.currentSkuArray[pid] == skuId) {
// 点击已被选中的,删除并填充 ''
isChecked = false;
state.currentSkuArray.splice(pid, 1, '');
} else {
// 选中
state.currentSkuArray[pid] = skuId;
}
let chooseSkuId = []; // 选中的规格大类
state.currentSkuArray.forEach((sku) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuId.push(sku);
}
});
// 当前所选规格下,所有可以选择的 skuPric
let newPrice = getCanUseSkuPrice();
// 判断所有规格大类是否选择完成
if (chooseSkuId.length == state.skuList.length && newPrice.length) {
newPrice[0].goods_num = state.selectedSkuPrice.goods_num || 1;
state.selectedSkuPrice = newPrice[0];
} else {
state.selectedSkuPrice = {};
}
// 改变规格项禁用状态
changeDisabled(isChecked, pid, skuId);
};
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>
@@ -0,0 +1,460 @@
<template>
<!-- 规格弹窗 -->
<su-popup :show="show" round="10" @close="emits('close')">
<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.selectedSkuPrice.image || goodsInfo.image)"
mode="aspectFill"
></image>
</view>
<view class="header-right ss-flex-col ss-row-between ss-flex-1">
<view class="goods-title ss-line-2">{{ goodsInfo.title }}</view>
<view class="header-right-bottom ss-flex ss-col-center ss-row-between">
<view class="ss-flex">
<view v-if="goodsPrice.price > 0" class="price-text">
{{ goodsPrice.price }}
</view>
<view class="ss-flex">
<view
v-if="goodsPrice.price > 0 && goodsPrice.score > 0"
class="score-text ss-m-l-4"
>+</view
>
<image
v-if="goodsPrice.score > 0"
:src="sheep.$url.static('/static/img/shop/goods/score1.svg')"
class="score-img"
></image>
<view v-if="goodsPrice.score > 0" class="score-text">
{{ goodsPrice.score }}
</view>
</view>
</view>
<view class="stock-text ss-m-l-20">
{{
state.selectedSkuPrice.stock
? formatStock(goodsInfo.stock_show_type, state.selectedSkuPrice.stock)
: formatStock(goodsInfo.stock_show_type, 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="sku1 in goodsInfo.skus" :key="sku1.id">
<view class="label-text ss-m-b-20">{{ sku1.name }}</view>
<view class="ss-flex ss-col-center ss-flex-wrap">
<button
class="ss-reset-button spec-btn"
v-for="sku2 in sku1.children"
:class="[
{
'ui-BG-Main-Gradient': state.currentSkuArray[sku2.parent_id] == sku2.id,
},
{
'disabled-btn': sku2.disabled == true,
},
]"
:key="sku2.id"
:disabled="sku2.disabled == true"
@tap="onSelectSku(sku2.parent_id, sku2.id)"
>
{{ sku2.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.selectedSkuPrice.stock"
:step="1"
v-model="state.selectedSkuPrice.goods_num"
></su-number-box>
</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"
v-if="isScore"
>
<button class="ss-reset-button score-btn ui-Shadow-Main" @tap="onBuy">立即兑换</button>
</view>
<view class="buy-box ss-flex ss-col-center ss-flex ss-col-center ss-row-center" v-else>
<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, formatPrice } from '@/sheep/hooks/useGoods';
import { isEmpty } from 'lodash';
const emits = defineEmits(['change', 'addCart', 'buy', 'close']);
const props = defineProps({
goodsInfo: {
type: Object,
default() {},
},
show: {
type: Boolean,
default: false,
},
isScore: {
type: Boolean,
default: false,
},
});
const state = reactive({
selectedSkuPrice: {},
currentSkuArray: [],
});
// 默认单规格
if (!props.goodsInfo.is_sku) {
state.selectedSkuPrice = props.goodsInfo.sku_prices[0];
}
const skuList = props.goodsInfo.skus;
// 可选规格
const skuPrices = computed(() => {
let skuPrices = props.goodsInfo.sku_prices;
if (props.goodsInfo.is_sku) {
skuPrices.forEach((item) => {
item.goods_sku_id_arr = item.goods_sku_ids.split(',');
});
}
return skuPrices;
});
watch(
() => state.selectedSkuPrice,
(newVal) => {
emits('change', newVal);
},
{
immediate: true, // 立即执行
deep: true, // 深度监听
},
);
const goodsPrice = computed(() => {
let price, score;
if (isEmpty(state.selectedSkuPrice)) {
price = props.goodsInfo.price[0];
score = props.goodsInfo.score || 0;
} else {
price = state.selectedSkuPrice.price;
score = state.selectedSkuPrice.score || 0;
}
return { price, score };
});
function onAddCart() {
if (state.selectedSkuPrice.goods_id) {
if (state.selectedSkuPrice.stock <= 0) {
sheep.$helper.toast('库存不足');
} else {
emits('addCart', state.selectedSkuPrice);
}
} else {
sheep.$helper.toast('请选择规格');
}
}
function onBuy() {
if (state.selectedSkuPrice.goods_id) {
if (state.selectedSkuPrice.stock <= 0) {
sheep.$helper.toast('库存不足');
} else {
emits('buy', state.selectedSkuPrice);
}
} else {
sheep.$helper.toast('请选择规格');
}
}
// 改变禁用状态
function changeDisabled(isChecked = false, pid = 0, skuId = 0) {
let newPrice = []; // 所有可以选择的 skuPrice
if (isChecked) {
// 选中规格
// 当前点击选中规格下的 所有可用 skuPrice
for (let price of skuPrices.value) {
if (price.stock <= 0) {
// this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
if (price.goods_sku_id_arr.indexOf(skuId.toString()) >= 0) {
newPrice.push(price);
}
}
} else {
// 取消选中
// 当前所选规格下,所有可以选择的 skuPrice
newPrice = getCanUseSkuPrice();
}
// 所有存在并且有库存未选择的规格项 的 子项 id
let noChooseSkuIds = [];
for (let price of newPrice) {
noChooseSkuIds = noChooseSkuIds.concat(price.goods_sku_id_arr);
}
// 去重
noChooseSkuIds = Array.from(new Set(noChooseSkuIds));
if (isChecked) {
// 去除当前选中的规格项
let index = noChooseSkuIds.indexOf(skuId.toString());
noChooseSkuIds.splice(index, 1);
} else {
// 循环去除当前已选择的规格项
state.currentSkuArray.forEach((sku) => {
if (sku.toString() != '') {
// sku 为空是反选 填充的
let index = noChooseSkuIds.indexOf(sku.toString());
if (index >= 0) {
// sku 存在于 noChooseSkuIds
noChooseSkuIds.splice(index, 1);
}
}
});
}
// 当前已选择的规格大类
let chooseSkuKey = [];
if (!isChecked) {
// 当前已选择的规格大类
state.currentSkuArray.forEach((sku, key) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuKey.push(key);
}
});
} else {
// 当前点击选择的规格大类
chooseSkuKey = [pid];
}
for (let i in skuList) {
// 当前点击的规格,或者取消选择时候 已选中的规格 不进行处理
if (chooseSkuKey.indexOf(skuList[i]['id']) >= 0) {
continue;
}
for (let j in skuList[i]['children']) {
// 如果当前规格项 id 不存在于有库存的规格项中,则禁用
if (noChooseSkuIds.indexOf(skuList[i]['children'][j]['id'].toString()) >= 0) {
skuList[i]['children'][j]['disabled'] = false;
} else {
skuList[i]['children'][j]['disabled'] = true;
}
}
}
}
// 当前所选规格下,获取所有有库存的 skuPrice
function getCanUseSkuPrice() {
let newPrice = [];
for (let price of skuPrices.value) {
if (price.stock <= 0) {
// || price.stock < this.goodsNum 不判断是否大于当前选择数量,在 uni-number-box 判断,并且 取 stock 和 goods_num 的小值
continue;
}
var isOk = true;
state.currentSkuArray.forEach((sku) => {
// sku 不为空,并且,这个 条 skuPrice 没有被选中,则排除
if (sku.toString() != '' && price.goods_sku_id_arr.indexOf(sku.toString()) < 0) {
isOk = false;
}
});
if (isOk) {
newPrice.push(price);
}
}
return newPrice;
}
// 选择规格
function onSelectSku(pid, skuId) {
// 清空已选择
let isChecked = true; // 选中 or 取消选中
if (state.currentSkuArray[pid] != undefined && state.currentSkuArray[pid] == skuId) {
// 点击已被选中的,删除并填充 ''
isChecked = false;
state.currentSkuArray.splice(pid, 1, '');
} else {
// 选中
state.currentSkuArray[pid] = skuId;
}
let chooseSkuId = []; // 选中的规格大类
state.currentSkuArray.forEach((sku) => {
if (sku != '') {
// sku 为空是反选 填充的
chooseSkuId.push(sku);
}
});
// 当前所选规格下,所有可以选择的 skuPric
let newPrice = getCanUseSkuPrice();
// 判断所有规格大类是否选择完成
if (chooseSkuId.length == skuList.length && newPrice.length) {
newPrice[0].goods_num = state.selectedSkuPrice.goods_num || 1;
state.selectedSkuPrice = newPrice[0];
} else {
state.selectedSkuPrice = {};
}
// 改变规格项禁用状态
changeDisabled(isChecked, pid, skuId);
}
changeDisabled(false);
</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>
@@ -0,0 +1,156 @@
<!-- 页面 -->
<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">
<view
v-if="poster.src === ''"
class="poster-title ss-flex ss-row-center"
:style="{
height: poster.height + 'px',
width: poster.width + 'px',
}"
>
海报加载中...
</view>
<image
v-else
class="poster-img"
:src="poster.src"
:style="{
height: poster.height + 'px',
width: poster.width + 'px',
}"
></image>
<canvas
class="hideCanvas"
:canvas-id="poster.canvasId"
:id="poster.canvasId"
:style="{
height: poster.height + 'px',
width: poster.width + 'px',
}"
/>
<view
class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
v-if="poster.src != ''"
>
<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>
</view>
</su-popup>
</template>
<script setup>
import { computed, reactive, getCurrentInstance } from 'vue';
import sheep from '@/sheep';
import useCanvas from './useCanvas';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
shareInfo: {
type: Object,
default() {},
},
});
const poster = reactive({
canvasId: 'canvasId',
width: sheep.$platform.device.windowWidth * 0.9,
height: 600,
src: '',
});
const emits = defineEmits(['success', 'close']);
const vm = getCurrentInstance();
const onClosePoster = () => {
emits('close');
};
const onSavePoster = () => {
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
sheep.$helper.toast('请长按图片保存');
return;
}
uni.saveImageToPhotosAlbum({
filePath: poster.src,
success: (res) => {
onClosePoster();
sheep.$helper.toast('保存成功');
},
fail: (err) => {
sheep.$helper.toast('保存失败');
console.log('图片保存失败:', err);
},
});
};
async function getPoster(params) {
poster.src = '';
poster.shareInfo = props.shareInfo;
// #ifdef APP-PLUS
poster.canvasId = 'canvasId-' + new Date().getTime();
// #endif
const canvas = await useCanvas(poster, vm);
return canvas;
}
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;
}
.hideCanvas {
position: fixed;
top: -99999rpx;
left: -99999rpx;
z-index: -99999;
}
</style>
@@ -0,0 +1,119 @@
import sheep from '@/sheep';
const goods = (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
return {
background: sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg),
list: [
{
name: 'nickname',
type: 'text',
val: userInfo.nickname,
x: width * 0.22,
y: width * 0.06,
paintbrushProps: {
fillStyle: '#333',
font: {
fontSize: 16,
fontFamily: 'sans-serif',
},
},
},
{
name: 'avatar',
type: 'image',
val: sheep.$url.cdn(userInfo.avatar),
x: width * 0.04,
y: width * 0.04,
width: width * 0.14,
height: width * 0.14,
d: width * 0.14,
},
{
name: 'goodsImage',
type: 'image',
val: poster.shareInfo.poster.image,
x: width * 0.03,
y: width * 0.21,
width: width * 0.94,
height: width * 0.94,
r: 10,
},
{
name: 'goodsTitle',
type: 'text',
val: poster.shareInfo.poster.title,
x: width * 0.04,
y: width * 1.18,
maxWidth: width * 0.92,
lineHeight: 5,
paintbrushProps: {
fillStyle: '#333',
font: {
fontSize: 14,
},
},
},
{
name: 'goodsPrice',
type: 'text',
val: '¥' + poster.shareInfo.poster.price,
x: width * 0.04,
y: width * 1.3,
paintbrushProps: {
fillStyle: '#ff0000',
font: {
fontSize: 20,
fontFamily: 'OPPOSANS',
},
},
},
{
name: 'goodsOriginalPrice',
type: 'text',
val:
poster.shareInfo.poster.original_price > 0
? '¥' + poster.shareInfo.poster.original_price
: '',
x: width * 0.3,
y: width * 1.32,
paintbrushProps: {
fillStyle: '#999',
font: {
fontSize: 10,
fontFamily: 'OPPOSANS',
},
},
textDecoration: {
line: 'line-through',
style: 'solide',
},
},
// #ifndef MP-WEIXIN
{
name: 'qrcode',
type: 'qrcode',
val: poster.shareInfo.link,
x: width * 0.75,
y: width * 1.3,
size: width * 0.2,
},
// #endif
// #ifdef MP-WEIXIN
{
name: 'wxacode',
type: 'image',
val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
x: width * 0.75,
y: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
// #endif
],
};
};
export default goods;
@@ -0,0 +1,112 @@
import sheep from '@/sheep';
const groupon = (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
return {
background: sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.groupon_bg),
list: [
{
name: 'nickname',
type: 'text',
val: userInfo.nickname,
x: width * 0.22,
y: width * 0.06,
paintbrushProps: {
fillStyle: '#333',
font: {
fontSize: 16,
fontFamily: 'sans-serif',
},
},
},
{
name: 'avatar',
type: 'image',
val: sheep.$url.cdn(userInfo.avatar),
x: width * 0.04,
y: width * 0.04,
width: width * 0.14,
height: width * 0.14,
d: width * 0.14,
},
{
name: 'goodsImage',
type: 'image',
val: poster.shareInfo.poster.image,
x: width * 0.03,
y: width * 0.21,
width: width * 0.94,
height: width * 0.94,
r: 10,
},
{
name: 'goodsTitle',
type: 'text',
val: poster.shareInfo.poster.title,
x: width * 0.04,
y: width * 1.18,
maxWidth: width * 0.92,
lineHeight: 5,
paintbrushProps: {
fillStyle: '#333',
font: {
fontSize: 14,
},
},
},
{
name: 'goodsPrice',
type: 'text',
val: '¥' + poster.shareInfo.poster.price,
x: width * 0.04,
y: width * 1.3,
paintbrushProps: {
fillStyle: '#ff0000',
font: {
fontSize: 20,
fontFamily: 'OPPOSANS',
},
},
},
{
name: 'grouponNum',
type: 'text',
val: '2人团',
x: width * 0.3,
y: width * 1.32,
paintbrushProps: {
fillStyle: '#ff0000',
font: {
fontSize: 10,
fontFamily: 'OPPOSANS',
},
},
},
// #ifndef MP-WEIXIN
{
name: 'qrcode',
type: 'qrcode',
val: poster.shareInfo.link,
x: width * 0.75,
y: width * 1.3,
size: width * 0.2,
},
// #endif
// #ifdef MP-WEIXIN
{
name: 'wxacode',
type: 'image',
val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
x: width * 0.75,
y: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
// #endif
],
};
};
export default groupon;
@@ -0,0 +1,14 @@
import user from './user';
import goods from './goods';
import groupon from './groupon';
export function getPosterData(options) {
switch (options.shareInfo.poster.type) {
case 'user':
return user(options);
case 'goods':
return goods(options);
case 'groupon':
return groupon(options);
}
}
@@ -0,0 +1,60 @@
import sheep from '@/sheep';
const user = (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
return {
background: sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.user_bg),
list: [
{
name: 'nickname',
type: 'text',
val: userInfo.nickname,
x: width / 2,
y: width * 0.4,
paintbrushProps: {
textAlign: 'center',
fillStyle: '#333',
font: {
fontSize: 14,
fontFamily: 'sans-serif',
},
},
},
{
name: 'avatar',
type: 'image',
val: sheep.$url.cdn(userInfo.avatar),
x: width * 0.4,
y: width * 0.16,
width: width * 0.2,
height: width * 0.2,
d: width * 0.2,
},
// #ifndef MP-WEIXIN
{
name: 'qrcode',
type: 'qrcode',
val: poster.shareInfo.link,
x: width * 0.35,
y: width * 0.84,
size: width * 0.3,
},
// #endif
// #ifdef MP-WEIXIN
{
name: 'wxacode',
type: 'image',
val: sheep.$api.third.wechat.getWxacode(poster.shareInfo.path),
x: width * 0.35,
y: width * 0.84,
width: width * 0.3,
height: width * 0.3,
},
// #endif
],
};
};
export default user;
@@ -0,0 +1,89 @@
/**
* Shopro + qs-canvas 绘制海报
* @version 1.0.0
* @author lidongtony
* @param {Object} options - 海报参数
* @param {Object} vm - 自定义组件实例
*/
import sheep from '@/sheep';
import QSCanvas from 'qs-canvas';
import { getPosterData } from './poster';
export default async function useCanvas(options, vm) {
const width = options.width;
const qsc = new QSCanvas(
{
canvasId: options.canvasId,
width: options.width,
height: options.height,
setCanvasWH: (canvas) => {
options.height = canvas.height;
},
},
vm,
);
let drawer = getPosterData(options);
// 绘制背景图
const background = await qsc.drawImg({
type: 'image',
val: drawer.background,
x: 0,
y: 0,
width,
mode: 'widthFix',
zIndex: 0,
});
await qsc.updateCanvasWH({
width: background.width,
height: background.bottom,
});
let list = drawer.list;
for (let i = 0; i < list.length; i++) {
let item = list[i];
// 绘制文字
if (item.type === 'text') {
await qsc.drawText(item);
}
// 绘制图片
if (item.type === 'image') {
if (item.d) {
qsc.setCircle({
x: item.x,
y: item.y,
d: item.d,
clip: true,
});
}
if (item.r) {
qsc.setRect({
x: item.x,
y: item.y,
height: item.height,
width: item.width,
r: item.r,
clip: true,
});
}
await qsc.drawImg(item);
qsc.restore();
}
// 绘制二维码
if (item.type === 'qrcode') {
await qsc.drawQrCode(item);
}
}
await qsc.draw();
// 延迟执行, 防止不稳定
setTimeout(async () => {
options.src = await qsc.toImage();
}, 100);
return options;
}
@@ -0,0 +1,201 @@
<template>
<!-- 分享弹框 -->
<view>
<su-popup :show="state.showShareGuide" :showClose="false" @close="onCloseGuide"></su-popup>
<view v-if="state.showShareGuide" class="guide-wrap">
<image
class="guide-image"
:src="sheep.$url.static('/static/img/shop/share/share_guide.png')"
></image>
</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=""
></image>
<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=""
></image>
<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=""
></image>
<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 { showShareModal, 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;
}
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>
@@ -0,0 +1,10 @@
<template>
<view class="status_bar"></view>
</template>
<style>
.status_bar {
height: var(--status-bar-height);
width: 100%;
}
</style>
+88
View File
@@ -0,0 +1,88 @@
<template>
<view class="u-page__item" v-if="tabbar?.list.length > 0">
<su-tabbar
:value="path"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
:inactiveColor="tabbar.inactiveColor"
:activeColor="tabbar.activeColor"
:midTabBar="tabbar.mode === 2"
:customStyle="tabbarStyle"
>
<su-tabbar-item
v-for="(item, index) in tabbar.list"
:key="item.text"
:text="item.text"
:name="item.url"
:isCenter="getTabbarCenter(index)"
:centerImage="sheep.$url.cdn(item.inactiveIcon)"
@tap="sheep.$router.go(item.url)"
>
<template v-slot:active-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.activeIcon)"></image>
</template>
<template v-slot:inactive-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.inactiveIcon)"></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.background;
if (backgroundStyle.type == 'color') return { background: backgroundStyle.bgColor };
if (backgroundStyle.type == 'image')
return {
background: `url(${sheep.$url.cdn(
backgroundStyle.bgImage,
)}) no-repeat top center / 100% auto`,
};
});
const getTabbarCenter = (index) => {
if (unref(tabbar).mode !== 2) return false;
return unref(tabbar).list % 2 > 0
? Math.ceil(unref(tabbar).list.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>
@@ -0,0 +1,114 @@
<!-- 页面 -->
<template>
<view
class="ss-title-wrap ss-flex ss-col-center"
:class="[state.typeMap[data.location]]"
:style="[elStyles]"
>
<view class="title-content">
<view v-if="data.title.text" class="title-text" :style="[titleStyles]">{{
data.title.text
}}</view>
<view v-if="data.subtitle.text" :style="[subtitleStyles]" class="sub-title-text">
{{ data.subtitle.text }}
</view>
</view>
<view v-if="data.more.show" class="more-box ss-flex ss-col-center">
<view class="more-text">查看更多</view>
<text class="_icon-forward"></text>
</view>
</view>
</template>
<script setup>
/**
*
* 标题栏
*
* @property {String} title - 标题
* @property {String} subTitle - 副标题
* @property {Number} height - 高度
* @property {String} Type = [left | right | center | between] - 样式
*
*/
import { computed, reactive } 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() {},
},
height: {
type: Number,
default: 100,
},
});
// 组件样式
const elStyles = {
background: `url(${sheep.$url.cdn(props.data.src)}) no-repeat top center / 100% 100%`,
height: props.styles.height + 'px',
fontStyle: props.data.title.other.includes('italic') ? 'italic' : 'normal',
fontWeight: props.data.title.other.includes('bold') ? 'bold' : '',
};
// 标题样式
const titleStyles = {
color: props.data.title.color,
fontSize: props.data.title.textFontSize + 'px',
textAlign: props.data.location,
marginLeft: props.data.skew + 'px',
};
// 副标题
const subtitleStyles = {
color: props.data.subtitle.color,
fontSize: props.data.subtitle.textFontSize + 'px',
textAlign: props.data.location,
fontStyle: props.data.subtitle.other.includes('italic') ? 'italic' : 'normal',
fontWeight: props.data.subtitle.other.includes('bold') ? 'bold' : '',
};
</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>
@@ -0,0 +1,213 @@
'use strict';
import sheep from '@/sheep';
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) {
let { path } = await sheep.$api.app.upload(file.path, 'ugc');
file.url = path;
}
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 };
+674
View File
@@ -0,0 +1,674 @@
<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
View 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>
@@ -0,0 +1,302 @@
<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') {
return [this.filesList];
}
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
View 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;
};
@@ -0,0 +1,164 @@
<!-- 页面 -->
<template>
<view class="ss-user-info-wrap ss-p-t-50">
<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>
<view
class="bind-mobile-box ss-flex ss-row-between ss-col-center"
v-if="isLogin && !userInfo.verification?.mobile"
>
<view class="ss-flex">
<text class="cicon-mobile-o"></text>
<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, reactive } from 'vue';
import sheep from '@/sheep';
import { showShareModal, showAuthModal } from '@/sheep/hooks/useModal';
// 用户信息
const userInfo = computed(() => sheep.$store('user').userInfo);
// 是否登录
const isLogin = computed(() => sheep.$store('user').isLogin);
// 接收参数
const props = defineProps({
background: {
type: String,
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',
},
});
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>
@@ -0,0 +1,31 @@
<!-- 订单详情 -->
<template>
<su-video
class="sss"
:uid="guid()"
:src="sheep.$url.cdn(data.videoUrl)"
:poster="sheep.$url.cdn(data.src)"
:height="styles.height"
></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>
@@ -0,0 +1,110 @@
<template>
<view class="ss-wallet-menu-wrap ss-flex ss-col-center">
<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">{{ userInfo.money }}</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/commission')">
<view class="value-box ss-flex ss-col-bottom">
<view class="value-text">{{ userInfo?.commission || '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.score }}</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.coupons_num }}</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"
>
</image>
<view class="menu-title ss-m-t-30">我的钱包</view>
</view>
</view>
</template>
<script setup>
/**
* 装修组件 - 订单菜单组
*/
import { computed, ref } from 'vue';
import sheep from '@/sheep';
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: 16rpx;
color: #000000;
line-height: 16rpx;
}
}
}
}
</style>
+18
View File
@@ -0,0 +1,18 @@
// 开发环境配置
export let baseUrl;
if (process.env.NODE_ENV === 'development') {
baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL;
} else {
baseUrl = import.meta.env.SHOPRO_BASE_URL;
}
console.log(`[Shopro v1.0.8] https://www.sheepjs.com/`);
export const apiPath = import.meta.env.SHOPRO_API_PATH;
export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
export default {
baseUrl,
apiPath,
staticUrl,
};
+20
View File
@@ -0,0 +1,20 @@
// uniapp在H5中各API的z-index值如下:
/**
* actionsheet: 999
* modal: 999
* navigate: 998
* tabbar: 998
* toast: 999
*/
export default {
toast: 10090,
noNetwork: 10080,
popup: 10075, // popup包含popupactionsheetkeyboardpicker的值
mask: 10070,
navbar: 980,
topTips: 975,
sticky: 970,
indexListSticky: 965,
popover: 960,
};
+168
View File
@@ -0,0 +1,168 @@
let _boundaryCheckingState = true; // 是否进行越界检查的全局开关
/**
* 把错误的数据转正
* @private
* @example strip(0.09999999999999998)=0.1
*/
function strip(num, precision = 15) {
return +parseFloat(Number(num).toPrecision(precision));
}
/**
* Return digits length of a number
* @private
* @param {*number} num Input number
*/
function digitLength(num) {
// Get digit length of e
const eSplit = num.toString().split(/[eE]/);
const len = (eSplit[0].split('.')[1] || '').length - +(eSplit[1] || 0);
return len > 0 ? len : 0;
}
/**
* 把小数转成整数,如果是小数则放大成整数
* @private
* @param {*number} num 输入数
*/
function float2Fixed(num) {
if (num.toString().indexOf('e') === -1) {
return Number(num.toString().replace('.', ''));
}
const dLen = digitLength(num);
return dLen > 0 ? strip(Number(num) * Math.pow(10, dLen)) : Number(num);
}
/**
* 检测数字是否越界,如果越界给出提示
* @private
* @param {*number} num 输入数
*/
function checkBoundary(num) {
if (_boundaryCheckingState) {
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
console.warn(`${num} 超出了精度限制,结果可能不正确`);
}
}
}
/**
* 把递归操作扁平迭代化
* @param {number[]} arr 要操作的数字数组
* @param {function} operation 迭代操作
* @private
*/
function iteratorOperation(arr, operation) {
const [num1, num2, ...others] = arr;
let res = operation(num1, num2);
others.forEach((num) => {
res = operation(res, num);
});
return res;
}
/**
* 高精度乘法
* @export
*/
export function times(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, times);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
const baseNum = digitLength(num1) + digitLength(num2);
const leftValue = num1Changed * num2Changed;
checkBoundary(leftValue);
return leftValue / Math.pow(10, baseNum);
}
/**
* 高精度加法
* @export
*/
export function plus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, plus);
}
const [num1, num2] = nums;
// 取最大的小数位
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
// 把小数都转为整数然后再计算
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
}
/**
* 高精度减法
* @export
*/
export function minus(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, minus);
}
const [num1, num2] = nums;
const baseNum = Math.pow(10, Math.max(digitLength(num1), digitLength(num2)));
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
}
/**
* 高精度除法
* @export
*/
export function divide(...nums) {
if (nums.length > 2) {
return iteratorOperation(nums, divide);
}
const [num1, num2] = nums;
const num1Changed = float2Fixed(num1);
const num2Changed = float2Fixed(num2);
checkBoundary(num1Changed);
checkBoundary(num2Changed);
// 重要,这里必须用strip进行修正
return times(
num1Changed / num2Changed,
strip(Math.pow(10, digitLength(num2) - digitLength(num1))),
);
}
/**
* 四舍五入
* @export
*/
export function round(num, ratio) {
const base = Math.pow(10, ratio);
let result = divide(Math.round(Math.abs(times(num, base))), base);
if (num < 0 && result !== 0) {
result = times(result, -1);
}
// 位数不足则补0
return result;
}
/**
* 是否进行边界检查,默认开启
* @param flag 标记开关,true 为开启,false 为关闭,默认为 true
* @export
*/
export function enableBoundaryChecking(flag = true) {
_boundaryCheckingState = flag;
}
export default {
times,
plus,
minus,
divide,
round,
enableBoundaryChecking,
};
+708
View File
@@ -0,0 +1,708 @@
import test from './test.js';
import { round } from './digit.js';
/**
* @description 如果value小于min,取min;如果value大于max,取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)));
}
/**
* @description 用于获取用户传递值的px值 如果用户传递了"xxpx"或者"xxrpx",取出其数值部分,如果是"xxxrpx"还需要用过uni.upx2px进行转换
* @param {number|string} value 用户传递值的px值
* @param {boolean} unit
* @returns {number|string}
*/
export function getPx(value, unit = false) {
if (test.number(value)) {
return unit ? `${value}px` : Number(value);
}
// 如果带有rpx,先取出其数值部分,再转为px值
if (/(rpx|upx)$/.test(value)) {
return unit ? `${uni.upx2px(parseInt(value))}px` : Number(uni.upx2px(parseInt(value)));
}
return unit ? `${parseInt(value)}px` : parseInt(value);
}
/**
* @description 进行延时,以达到可以简写代码的目的
* @param {number} value 堵塞时间 单位ms 毫秒
* @returns {Promise} 返回promise
*/
export function sleep(value = 30) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, value);
});
}
/**
* @description 运行期判断平台
* @returns {string} 返回所在平台(小写)
* @link 运行期判断平台 https://uniapp.dcloud.io/frame?id=判断平台
*/
export function os() {
return uni.getSystemInfoSync().platform.toLowerCase();
}
/**
* @description 获取系统信息同步接口
* @link 获取系统信息同步接口 https://uniapp.dcloud.io/api/system/info?id=getsysteminfosync
*/
export function sys() {
return uni.getSystemInfoSync();
}
/**
* @description 取一个区间数
* @param {Number} min 最小值
* @param {Number} max 最大值
*/
function random(min, max) {
if (min >= 0 && max > 0 && max >= min) {
const gab = max - min + 1;
return Math.floor(Math.random() * gab + min);
}
return 0;
}
/**
* @param {Number} len uuid的长度
* @param {Boolean} firstU 将返回的首字母置为"u"
* @param {Nubmer} radix 生成uuid的基数(意味着返回的字符串都是这个基数),2-二进制,8-八进制,10-十进制,16-十六进制
*/
export function guid(len = 32, firstU = true, radix = null) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
if (firstU) {
uuid.shift();
return `u${uuid.join('')}`;
}
return uuid.join('');
}
/**
* @description 获取父组件的参数,因为支付宝小程序不支持provide/inject的写法
this.$parent在非H5中,可以准确获取到父组件,但是在H5中,需要多次this.$parent.$parent.xxx
这里默认值等于undefined有它的含义,因为最顶层元素(组件)的$parent就是undefined,意味着不传name
值(默认为undefined),就是查找最顶层的$parent
* @param {string|undefined} name 父组件的参数名
*/
export function $parent(name = undefined) {
let parent = this.$parent;
// 通过while历遍,这里主要是为了H5需要多层解析的问题
while (parent) {
// 父组件
if (parent.$options && parent.$options.name !== name) {
// 如果组件的name不相等,继续上一级寻找
parent = parent.$parent;
} else {
return parent;
}
}
return false;
}
/**
* @description 样式转换
* 对象转字符串,或者字符串转对象
* @param {object | string} customStyle 需要转换的目标
* @param {String} target 转换的目的,object-转为对象,string-转为字符串
* @returns {object|string}
*/
export function addStyle(customStyle, target = 'object') {
// 字符串转字符串,对象转对象情形,直接返回
if (
test.empty(customStyle) ||
(typeof customStyle === 'object' && target === 'object') ||
(target === 'string' && typeof customStyle === 'string')
) {
return customStyle;
}
// 字符串转对象
if (target === 'object') {
// 去除字符串样式中的两端空格(中间的空格不能去掉,比如padding: 20px 0如果去掉了就错了),空格是无用的
customStyle = trim(customStyle);
// 根据";"将字符串转为数组形式
const styleArray = customStyle.split(';');
const style = {};
// 历遍数组,拼接成对象
for (let i = 0; i < styleArray.length; i++) {
// 'font-size:20px;color:red;',如此最后字符串有";"的话,会导致styleArray最后一个元素为空字符串,这里需要过滤
if (styleArray[i]) {
const item = styleArray[i].split(':');
style[trim(item[0])] = trim(item[1]);
}
}
return style;
}
// 这里为对象转字符串形式
let string = '';
for (const i in customStyle) {
// 驼峰转为中划线的形式,否则css内联样式,无法识别驼峰样式属性名
const key = i.replace(/([A-Z])/g, '-$1').toLowerCase();
string += `${key}:${customStyle[i]};`;
}
// 去除两端空格
return trim(string);
}
/**
* @description 添加单位,如果有rpx,upx,%,px等单位结尾或者值为auto,直接返回,否则加上px单位结尾
* @param {string|number} value 需要添加单位的值
* @param {string} unit 添加的单位名 比如px
*/
export function addUnit(value = 'auto', unit = 'px') {
value = String(value);
return test.number(value) ? `${value}${unit}` : value;
}
/**
* @description 深度克隆
* @param {object} obj 需要深度克隆的对象
* @returns {*} 克隆后的对象或者原值(不是对象)
*/
function deepClone(obj) {
// 对常见的“非”值,直接返回原来值
if ([null, undefined, NaN, false].includes(obj)) return obj;
if (typeof obj !== 'object' && typeof obj !== 'function') {
// 原始类型直接返回
return obj;
}
const o = test.array(obj) ? [] : {};
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i];
}
}
return o;
}
/**
* @description JS对象深度合并
* @param {object} target 需要拷贝的对象
* @param {object} source 拷贝的来源对象
* @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象)
*/
export function deepMerge(target = {}, source = {}) {
target = deepClone(target);
if (typeof target !== 'object' || typeof source !== 'object') return false;
for (const prop in source) {
if (!source.hasOwnProperty(prop)) continue;
if (prop in target) {
if (typeof target[prop] !== 'object') {
target[prop] = source[prop];
} else if (typeof source[prop] !== 'object') {
target[prop] = source[prop];
} else if (target[prop].concat && source[prop].concat) {
target[prop] = target[prop].concat(source[prop]);
} else {
target[prop] = deepMerge(target[prop], source[prop]);
}
} else {
target[prop] = source[prop];
}
}
return target;
}
/**
* @description error提示
* @param {*} err 错误内容
*/
function error(err) {
// 开发环境才提示,生产环境不会提示
if (process.env.NODE_ENV === 'development') {
console.error(`SheepJS:${err}`);
}
}
/**
* @description 打乱数组
* @param {array} array 需要打乱的数组
* @returns {array} 打乱后的数组
*/
function randomArray(array = []) {
// 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0
return array.sort(() => Math.random() - 0.5);
}
// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序
// 所以这里做一个兼容polyfill的兼容处理
if (!String.prototype.padStart) {
// 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解
String.prototype.padStart = function (maxLength, fillString = ' ') {
if (Object.prototype.toString.call(fillString) !== '[object String]') {
throw new TypeError('fillString must be String');
}
const str = this;
// 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉
if (str.length >= maxLength) return String(str);
const fillLength = maxLength - str.length;
let times = Math.ceil(fillLength / fillString.length);
while ((times >>= 1)) {
fillString += fillString;
if (times === 1) {
fillString += fillString;
}
}
return fillString.slice(0, fillLength) + str;
};
}
/**
* @description 格式化时间
* @param {String|Number} dateTime 需要格式化的时间戳
* @param {String} fmt 格式化规则 yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 默认yyyy-mm-dd
* @returns {string} 返回格式化后的字符串
*/
function timeFormat(dateTime = null, formatStr = 'yyyy-mm-dd') {
let date;
// 若传入时间为假值,则取当前时间
if (!dateTime) {
date = new Date();
}
// 若为unix秒时间戳,则转为毫秒时间戳(逻辑有点奇怪,但不敢改,以保证历史兼容)
else if (/^\d{10}$/.test(dateTime?.toString().trim())) {
date = new Date(dateTime * 1000);
}
// 若用户传入字符串格式时间戳,new Date无法解析,需做兼容
else if (typeof dateTime === 'string' && /^\d+$/.test(dateTime.trim())) {
date = new Date(Number(dateTime));
}
// 其他都认为符合 RFC 2822 规范
else {
// 处理平台性差异,在Safari/Webkit中,new Date仅支持/作为分割符的字符串时间
date = new Date(typeof dateTime === 'string' ? dateTime.replace(/-/g, '/') : dateTime);
}
const timeSource = {
y: date.getFullYear().toString(), // 年
m: (date.getMonth() + 1).toString().padStart(2, '0'), // 月
d: date.getDate().toString().padStart(2, '0'), // 日
h: date.getHours().toString().padStart(2, '0'), // 时
M: date.getMinutes().toString().padStart(2, '0'), // 分
s: date.getSeconds().toString().padStart(2, '0'), // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
};
for (const key in timeSource) {
const [ret] = new RegExp(`${key}+`).exec(formatStr) || [];
if (ret) {
// 年可能只需展示两位
const beginIndex = key === 'y' && ret.length === 2 ? 2 : 0;
formatStr = formatStr.replace(ret, timeSource[key].slice(beginIndex));
}
}
return formatStr;
}
/**
* @description 时间戳转为多久之前
* @param {String|Number} timestamp 时间戳
* @param {String|Boolean} format
* 格式化规则如果为时间格式字符串,超出一定时间范围,返回固定的时间格式;
* 如果为布尔值false,无论什么时间,都返回多久以前的格式
* @returns {string} 转化后的内容
*/
function timeFrom(timestamp = null, format = 'yyyy-mm-dd') {
if (timestamp == null) timestamp = Number(new Date());
timestamp = parseInt(timestamp);
// 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位)
if (timestamp.toString().length == 10) timestamp *= 1000;
let timer = new Date().getTime() - timestamp;
timer = parseInt(timer / 1000);
// 如果小于5分钟,则返回"刚刚",其他以此类推
let tips = '';
switch (true) {
case timer < 300:
tips = '刚刚';
break;
case timer >= 300 && timer < 3600:
tips = `${parseInt(timer / 60)}分钟前`;
break;
case timer >= 3600 && timer < 86400:
tips = `${parseInt(timer / 3600)}小时前`;
break;
case timer >= 86400 && timer < 2592000:
tips = `${parseInt(timer / 86400)}天前`;
break;
default:
// 如果format为false,则无论什么时间戳,都显示xx之前
if (format === false) {
if (timer >= 2592000 && timer < 365 * 86400) {
tips = `${parseInt(timer / (86400 * 30))}个月前`;
} else {
tips = `${parseInt(timer / (86400 * 365))}年前`;
}
} else {
tips = timeFormat(timestamp, format);
}
}
return tips;
}
/**
* @description 去除空格
* @param String str 需要去除空格的字符串
* @param String pos both(左右)|left|right|all 默认both
*/
function trim(str, pos = 'both') {
str = String(str);
if (pos == 'both') {
return str.replace(/^\s+|\s+$/g, '');
}
if (pos == 'left') {
return str.replace(/^\s*/, '');
}
if (pos == 'right') {
return str.replace(/(\s*$)/g, '');
}
if (pos == 'all') {
return str.replace(/\s+/g, '');
}
return str;
}
/**
* @description 对象转url参数
* @param {object} data,对象
* @param {Boolean} isPrefix,是否自动加上"?"
* @param {string} arrayFormat 规则 indices|brackets|repeat|comma
*/
function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') {
const prefix = isPrefix ? '?' : '';
const _result = [];
if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1)
arrayFormat = 'brackets';
for (const key in data) {
const value = data[key];
// 去掉为空的参数
if (['', undefined, null].indexOf(value) >= 0) {
continue;
}
// 如果值为数组,另行处理
if (value.constructor === Array) {
// e.g. {ids: [1, 2, 3]}
switch (arrayFormat) {
case 'indices':
// 结果: ids[0]=1&ids[1]=2&ids[2]=3
for (let i = 0; i < value.length; i++) {
_result.push(`${key}[${i}]=${value[i]}`);
}
break;
case 'brackets':
// 结果: ids[]=1&ids[]=2&ids[]=3
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
break;
case 'repeat':
// 结果: ids=1&ids=2&ids=3
value.forEach((_value) => {
_result.push(`${key}=${_value}`);
});
break;
case 'comma':
// 结果: ids=1,2,3
let commaStr = '';
value.forEach((_value) => {
commaStr += (commaStr ? ',' : '') + _value;
});
_result.push(`${key}=${commaStr}`);
break;
default:
value.forEach((_value) => {
_result.push(`${key}[]=${_value}`);
});
}
} else {
_result.push(`${key}=${value}`);
}
}
return _result.length ? prefix + _result.join('&') : '';
}
/**
* 显示消息提示框
* @param {String} title 提示的内容,长度与 icon 取值有关。
* @param {Number} duration 提示的延迟时间,单位毫秒,默认:2000
*/
function toast(title, duration = 2000) {
uni.showToast({
title: String(title),
icon: 'none',
duration,
});
}
/**
* @description 根据主题type值,获取对应的图标
* @param {String} type 主题名称,primary|info|error|warning|success
* @param {boolean} fill 是否使用fill填充实体的图标
*/
function type2icon(type = 'success', fill = false) {
// 如果非预置值,默认为success
if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success';
let iconName = '';
// 目前(2019-12-12),info和primary使用同一个图标
switch (type) {
case 'primary':
iconName = 'info-circle';
break;
case 'info':
iconName = 'info-circle';
break;
case 'error':
iconName = 'close-circle';
break;
case 'warning':
iconName = 'error-circle';
break;
case 'success':
iconName = 'checkmark-circle';
break;
default:
iconName = 'checkmark-circle';
}
// 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的
if (fill) iconName += '-fill';
return iconName;
}
/**
* @description 数字格式化
* @param {number|string} number 要格式化的数字
* @param {number} decimals 保留几位小数
* @param {string} decimalPoint 小数点符号
* @param {string} thousandsSeparator 千分位符号
* @returns {string} 格式化后的数字
*/
function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') {
number = `${number}`.replace(/[^0-9+-Ee.]/g, '');
const n = !isFinite(+number) ? 0 : +number;
const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals);
const sep = typeof thousandsSeparator === 'undefined' ? ',' : thousandsSeparator;
const dec = typeof decimalPoint === 'undefined' ? '.' : decimalPoint;
let s = '';
s = (prec ? round(n, prec) + '' : `${Math.round(n)}`).split('.');
const re = /(-?\d+)(\d{3})/;
while (re.test(s[0])) {
s[0] = s[0].replace(re, `$1${sep}$2`);
}
if ((s[1] || '').length < prec) {
s[1] = s[1] || '';
s[1] += new Array(prec - s[1].length + 1).join('0');
}
return s.join(dec);
}
/**
* @description 获取duration值
* 如果带有ms或者s直接返回,如果大于一定值,认为是ms单位,小于一定值,认为是s单位
* 比如以30位阈值,那么300大于30,可以理解为用户想要的是300ms,而不是想花300s去执行一个动画
* @param {String|number} value 比如: "1s"|"100ms"|1|100
* @param {boolean} unit 提示: 如果是false 默认返回number
* @return {string|number}
*/
function getDuration(value, unit = true) {
const valueNum = parseInt(value);
if (unit) {
if (/s$/.test(value)) return value;
return value > 30 ? `${value}ms` : `${value}s`;
}
if (/ms$/.test(value)) return valueNum;
if (/s$/.test(value)) return valueNum > 30 ? valueNum : valueNum * 1000;
return valueNum;
}
/**
* @description 日期的月或日补零操作
* @param {String} value 需要补零的值
*/
function padZero(value) {
return `00${value}`.slice(-2);
}
/**
* @description 获取某个对象下的属性,用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式
* @param {object} obj 对象
* @param {string} key 需要获取的属性字段
* @returns {*}
*/
function getProperty(obj, key) {
if (!obj) {
return;
}
if (typeof key !== 'string' || key === '') {
return '';
}
if (key.indexOf('.') !== -1) {
const keys = key.split('.');
let firstObj = obj[keys[0]] || {};
for (let i = 1; i < keys.length; i++) {
if (firstObj) {
firstObj = firstObj[keys[i]];
}
}
return firstObj;
}
return obj[key];
}
/**
* @description 设置对象的属性值,如果'a.b.c'的形式进行设置
* @param {object} obj 对象
* @param {string} key 需要设置的属性
* @param {string} value 设置的值
*/
function setProperty(obj, key, value) {
if (!obj) {
return;
}
// 递归赋值
const inFn = function (_obj, keys, v) {
// 最后一个属性key
if (keys.length === 1) {
_obj[keys[0]] = v;
return;
}
// 0~length-1个key
while (keys.length > 1) {
const k = keys[0];
if (!_obj[k] || typeof _obj[k] !== 'object') {
_obj[k] = {};
}
const key = keys.shift();
// 自调用判断是否存在属性,不存在则自动创建对象
inFn(_obj[k], keys, v);
}
};
if (typeof key !== 'string' || key === '') {
} else if (key.indexOf('.') !== -1) {
// 支持多层级赋值操作
const keys = key.split('.');
inFn(obj, keys, value);
} else {
obj[key] = value;
}
}
/**
* @description 获取当前页面路径
*/
function page() {
const pages = getCurrentPages();
// 某些特殊情况下(比如页面进行redirectTo时的一些时机)pages可能为空数组
return `/${pages[pages.length - 1]?.route ?? ''}`;
}
/**
* @description 获取当前路由栈实例数组
*/
function pages() {
const pages = getCurrentPages();
return pages;
}
/**
* 获取H5-真实根地址 兼容hash+history模式
*/
export function getRootUrl() {
let url = '';
// #ifdef H5
url = location.origin + location.pathname;
if (location.hash !== '') {
url += '#/';
}
// #endif
return url;
}
/**
* copyText 多端复制文本
*/
export function copyText(text) {
// #ifndef H5
uni.setClipboardData({
data: text,
success: function () {
toast('复制成功!');
},
fail: function () {
toast('复制失败!');
},
});
// #endif
// #ifdef H5
var createInput = document.createElement('textarea');
createInput.value = text;
document.body.appendChild(createInput);
createInput.select();
document.execCommand('Copy');
createInput.className = 'createInput';
createInput.style.display = 'none';
toast('复制成功');
// #endif
}
export default {
range,
getPx,
sleep,
os,
sys,
random,
guid,
$parent,
addStyle,
addUnit,
deepClone,
deepMerge,
error,
randomArray,
timeFormat,
timeFrom,
trim,
queryParams,
toast,
type2icon,
priceFormat,
getDuration,
padZero,
getProperty,
setProperty,
page,
pages,
test,
getRootUrl,
copyText,
};
+285
View File
@@ -0,0 +1,285 @@
/**
* 验证电子邮箱格式
*/
function email(value) {
return /^\w+((-\w+)|(\.\w+))*\@[A-Za-z0-9]+((\.|-)[A-Za-z0-9]+)*\.[A-Za-z0-9]+$/.test(value);
}
/**
* 验证手机格式
*/
function mobile(value) {
return /^1[23456789]\d{9}$/.test(value);
}
/**
* 验证URL格式
*/
function url(value) {
return /^((https|http|ftp|rtsp|mms):\/\/)(([0-9a-zA-Z_!~*'().&=+$%-]+: )?[0-9a-zA-Z_!~*'().&=+$%-]+@)?(([0-9]{1,3}.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z].[a-zA-Z]{2,6})(:[0-9]{1,4})?((\/?)|(\/[0-9a-zA-Z_!~*'().;?:@&=+$,%#-]+)+\/?)$/.test(
value,
);
}
/**
* 验证日期格式
*/
function date(value) {
if (!value) return false;
// 判断是否数值或者字符串数值(意味着为时间戳),转为数值,否则new Date无法识别字符串时间戳
if (number(value)) value = +value;
return !/Invalid|NaN/.test(new Date(value).toString());
}
/**
* 验证ISO类型的日期格式
*/
function dateISO(value) {
return /^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(value);
}
/**
* 验证十进制数字
*/
function number(value) {
return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value);
}
/**
* 验证字符串
*/
function string(value) {
return typeof value === 'string';
}
/**
* 验证整数
*/
function digits(value) {
return /^\d+$/.test(value);
}
/**
* 验证身份证号码
*/
function idCard(value) {
return /^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test(value);
}
/**
* 是否车牌号
*/
function carNo(value) {
// 新能源车牌
const xreg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
// 旧车牌
const creg =
/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
if (value.length === 7) {
return creg.test(value);
}
if (value.length === 8) {
return xreg.test(value);
}
return false;
}
/**
* 金额,只允许2位小数
*/
function amount(value) {
// 金额,只允许保留两位小数
return /^[1-9]\d*(,\d{3})*(\.\d{1,2})?$|^0\.\d{1,2}$/.test(value);
}
/**
* 中文
*/
function chinese(value) {
const reg = /^[\u4e00-\u9fa5]+$/gi;
return reg.test(value);
}
/**
* 只能输入字母
*/
function letter(value) {
return /^[a-zA-Z]*$/.test(value);
}
/**
* 只能是字母或者数字
*/
function enOrNum(value) {
// 英文或者数字
const reg = /^[0-9a-zA-Z]*$/g;
return reg.test(value);
}
/**
* 验证是否包含某个值
*/
function contains(value, param) {
return value.indexOf(param) >= 0;
}
/**
* 验证一个值范围[min, max]
*/
function range(value, param) {
return value >= param[0] && value <= param[1];
}
/**
* 验证一个长度范围[min, max]
*/
function rangeLength(value, param) {
return value.length >= param[0] && value.length <= param[1];
}
/**
* 是否固定电话
*/
function landline(value) {
const reg = /^\d{3,4}-\d{7,8}(-\d{3,4})?$/;
return reg.test(value);
}
/**
* 判断是否为空
*/
function empty(value) {
switch (typeof value) {
case 'undefined':
return true;
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!value) return true;
break;
case 'number':
if (value === 0 || isNaN(value)) return true;
break;
case 'object':
if (value === null || value.length === 0) return true;
for (const i in value) {
return false;
}
return true;
}
return false;
}
/**
* 是否json字符串
*/
function jsonString(value) {
if (typeof value === 'string') {
try {
const obj = JSON.parse(value);
if (typeof obj === 'object' && obj) {
return true;
}
return false;
} catch (e) {
return false;
}
}
return false;
}
/**
* 是否数组
*/
function array(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value);
}
return Object.prototype.toString.call(value) === '[object Array]';
}
/**
* 是否对象
*/
function object(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
/**
* 是否短信验证码
*/
function code(value, len = 6) {
return new RegExp(`^\\d{${len}}$`).test(value);
}
/**
* 是否函数方法
* @param {Object} value
*/
function func(value) {
return typeof value === 'function';
}
/**
* 是否promise对象
* @param {Object} value
*/
function promise(value) {
return object(value) && func(value.then) && func(value.catch);
}
/** 是否图片格式
* @param {Object} value
*/
function image(value) {
const newValue = value.split('?')[0];
const IMAGE_REGEXP = /\.(jpeg|jpg|gif|png|svg|webp|jfif|bmp|dpg)/i;
return IMAGE_REGEXP.test(newValue);
}
/**
* 是否视频格式
* @param {Object} value
*/
function video(value) {
const VIDEO_REGEXP = /\.(mp4|mpg|mpeg|dat|asf|avi|rm|rmvb|mov|wmv|flv|mkv|m3u8)/i;
return VIDEO_REGEXP.test(value);
}
/**
* 是否为正则对象
* @param {Object}
* @return {Boolean}
*/
function regExp(o) {
return o && Object.prototype.toString.call(o) === '[object RegExp]';
}
export default {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
isEmpty: empty,
isNumber: number,
jsonString,
landline,
object,
array,
code,
};
+31
View File
@@ -0,0 +1,31 @@
let timer;
let flag;
/**
* 节流原理:在一定时间内,只能触发一次
*
* @param {Function} func 要执行的回调函数
* @param {Number} wait 延时的时间
* @param {Boolean} immediate 是否立即执行
* @return null
*/
function throttle(func, wait = 500, immediate = true) {
if (immediate) {
if (!flag) {
flag = true;
// 如果是立即执行,则在wait毫秒内开始时执行
typeof func === 'function' && func();
timer = setTimeout(() => {
flag = false;
}, wait);
} else {
}
} else if (!flag) {
flag = true;
// 如果是非立即执行,则在wait毫秒内的结束处执行
timer = setTimeout(() => {
flag = false;
typeof func === 'function' && func();
}, wait);
}
}
export default throttle;
+67
View File
@@ -0,0 +1,67 @@
import router from '@/sheep/router';
export default {
/**
* 打电话
* @param {String<Number>} phoneNumber - 数字字符串
*/
callPhone(phoneNumber = '') {
let num = phoneNumber.toString();
uni.makePhoneCall({
phoneNumber: num,
fail(err) {
console.log('makePhoneCall出错', err);
},
});
},
/**
* 微信头像
* @param {String} url -图片地址
*/
checkMPUrl(url) {
// #ifdef MP
if (
url.substring(0, 4) === 'http' &&
url.substring(0, 5) !== 'https' &&
url.substring(0, 12) !== 'http://store' &&
url.substring(0, 10) !== 'http://tmp' &&
url.substring(0, 10) !== 'http://usr'
) {
url = 'https' + url.substring(4, url.length);
}
// #endif
return url;
},
/**
* getUuid 生成唯一id
*/
getUuid(len = 32, firstU = true, radix = null) {
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
const uuid = [];
radix = radix || chars.length;
if (len) {
// 如果指定uuid长度,只是取随机的字符,0|x为位运算,能去掉x的小数位,返回整数位
for (let i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)];
} else {
let r;
// rfc4122标准要求返回的uuid中,某些位为固定的字符
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
for (let i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | (Math.random() * 16);
uuid[i] = chars[i == 19 ? (r & 0x3) | 0x8 : r];
}
}
}
// 移除第一个字符,并用u替代,因为第一个字符为数值时,该guuid不能用作id或者class
if (firstU) {
uuid.shift();
return `u${uuid.join('')}`;
}
return uuid.join('');
},
};
+168
View File
@@ -0,0 +1,168 @@
export function isArray(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value);
} else {
return Object.prototype.toString.call(value) === '[object Array]';
}
}
export function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]';
}
export function isNumber(value) {
return !isNaN(Number(value));
}
export function isFunction(value) {
return typeof value == 'function';
}
export function isString(value) {
return typeof value == 'string';
}
export function isEmpty(value) {
if (isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.keys(value).length === 0;
}
return value === '' || value === undefined || value === null;
}
export function isBoolean(value) {
return typeof value === 'boolean';
}
export function last(data) {
if (isArray(data) || isString(data)) {
return data[data.length - 1];
}
}
export function cloneDeep(obj) {
const d = isArray(obj) ? obj : {};
if (isObject(obj)) {
for (const key in obj) {
if (obj[key]) {
if (obj[key] && typeof obj[key] === 'object') {
d[key] = cloneDeep(obj[key]);
} else {
d[key] = obj[key];
}
}
}
}
return d;
}
export function clone(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));
}
export function deepMerge(a, b) {
let k;
for (k in b) {
a[k] = a[k] && a[k].toString() === '[object Object]' ? deepMerge(a[k], b[k]) : (a[k] = b[k]);
}
return a;
}
export function contains(parent, node) {
while (node && (node = node.parentNode)) if (node === parent) return true;
return false;
}
export function orderBy(list, key) {
return list.sort((a, b) => a[key] - b[key]);
}
export function deepTree(list) {
const newList = [];
const map = {};
list.forEach((e) => (map[e.id] = e));
list.forEach((e) => {
const parent = map[e.parentId];
if (parent) {
(parent.children || (parent.children = [])).push(e);
} else {
newList.push(e);
}
});
const fn = (list) => {
list.map((e) => {
if (e.children instanceof Array) {
e.children = orderBy(e.children, 'orderNum');
fn(e.children);
}
});
};
fn(newList);
return orderBy(newList, 'orderNum');
}
export function revDeepTree(list = []) {
const d = [];
let id = 0;
const deep = (list, parentId) => {
list.forEach((e) => {
if (!e.id) {
e.id = id++;
}
e.parentId = parentId;
d.push(e);
if (e.children && isArray(e.children)) {
deep(e.children, e.id);
}
});
};
deep(list || [], null);
return d;
}
export function basename(path) {
let index = path.lastIndexOf('/');
index = index > -1 ? index : path.lastIndexOf('\\');
if (index < 0) {
return path;
}
return path.substring(index + 1);
}
export function isWxBrowser() {
const ua = navigator.userAgent.toLowerCase();
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
return true;
} else {
return false;
}
}
/**
* @description 如果value小于min,取min;如果value大于max,取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
export function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)));
}
View File
+137
View File
@@ -0,0 +1,137 @@
import { ref } from 'vue';
import dayjs from 'dayjs';
import $url from '@/sheep/url';
// 格式化销量
export function formatSales(type, num) {
num = num + '';
if (type === 'exact') {
return '已售' + num;
} else {
if (num < 10) {
return '销量≤10';
} else {
let a = Math.pow(10, num.length - 1);
return '已售' + parseInt(num / a) * a + '+';
}
}
}
// 格式化兑换量
export function formatExchange(type, num) {
num = num + '';
if (type === 'exact') {
return '已兑换' + num;
} else {
if (num < 10) {
return '已兑换≤10';
} else {
let a = Math.pow(10, num.length - 1);
return '已兑换' + parseInt(num / a) * a + '+';
}
}
}
// 格式化库存
export function formatStock(type, num) {
num = num + '';
if (type === 'exact') {
return '库存' + num;
} else {
if (num < 10) {
return '库存≤10';
} else {
let a = Math.pow(10, num.length - 1);
return '库存 ' + parseInt(num / a) * a + '+';
}
}
}
// 格式化价格
export function formatPrice(e) {
return e.length === 1 ? e[0] : e.join('~');
}
// 格式化商品轮播
export function formatGoodsSwiper(list) {
let swiper = [];
list.forEach((item, key) => {
if (item.indexOf('.avi') !== -1 || item.indexOf('.mp4') !== -1) {
swiper.push({
src: $url.cdn(item),
type: 'video',
});
} else {
swiper.push({
src: $url.cdn(item),
type: 'image',
});
}
});
return swiper;
}
export function formatOrderColor(type) {
if (
type === 'apply_refund' ||
type === 'groupon_ing' ||
type === 'nocomment' ||
type === 'noget' ||
type === 'nosend'
) {
return 'warning-color';
} else if (
type === 'closed' ||
type === 'groupon_invalid' ||
type === 'cancel' ||
type === 'refund_agree'
) {
return 'danger-color';
} else if (type === 'completed') {
return 'success-color';
} else if (type === 'unpaid') {
return 'info-color';
}
}
// 计算相隔时间
export function useDurationTime(toTime, fromTime = '') {
toTime = getDayjsTime(toTime);
if (fromTime === '') {
fromTime = dayjs();
}
let duration = ref(toTime - fromTime);
if (duration.value > 0) {
setTimeout(() => {
if (duration.value > 0) {
duration.value -= 1000;
}
}, 1000);
}
let durationTime = dayjs.duration(duration.value);
return {
h: (durationTime.months() * 30 * 24 + durationTime.days() * 24 + durationTime.hours())
.toString()
.padStart(2, '0'),
m: durationTime.minutes().toString().padStart(2, '0'),
s: durationTime.seconds().toString().padStart(2, '0'),
ms: durationTime.$ms,
};
}
function getDayjsTime(time) {
time = time.toString();
if (time.indexOf('-') > 0) {
// 'date'
return dayjs(time);
}
if (time.length > 10) {
// 'timestamp'
return dayjs(parseInt(time));
}
if (time.length === 10) {
// 'unixtime'
return dayjs.unix(parseInt(time));
}
}
+128
View File
@@ -0,0 +1,128 @@
import $store from '@/sheep/store';
import $helper from '@/sheep/helper';
import dayjs from 'dayjs';
import { ref } from 'vue';
import test from '@/sheep/helper/test.js';
import $api from '@/sheep/api';
// 打开授权弹框
export function showAuthModal(type = 'accountLogin') {
const modal = $store('modal');
if (modal.auth !== '') {
closeAuthModal();
setTimeout(() => {
modal.$patch((state) => {
state.auth = type;
});
}, 100);
} else {
modal.$patch((state) => {
state.auth = type;
});
}
}
// 关闭授权弹框
export function closeAuthModal() {
$store('modal').$patch((state) => {
state.auth = '';
});
}
// 打开分享弹框
export function showShareModal() {
$store('modal').$patch((state) => {
state.share = true;
});
}
// 关闭分享弹框
export function closeShareModal() {
$store('modal').$patch((state) => {
state.share = false;
});
}
// 打开快捷菜单
export function showMenuTools() {
$store('modal').$patch((state) => {
state.menu = true;
});
}
// 关闭快捷菜单
export function closeMenuTools() {
$store('modal').$patch((state) => {
state.menu = false;
});
}
// 发送短信验证码 60秒
export function getSmsCode(event, mobile = '') {
const modalStore = $store('modal');
const lastSendTimer = modalStore.lastTimer[event];
if (typeof lastSendTimer === 'undefined') {
$helper.toast('短信发送事件错误');
return;
}
const duration = dayjs().unix() - lastSendTimer;
const canSend = duration >= 60;
if (!canSend) {
$helper.toast('请稍后再试');
return;
}
if (!test.mobile(mobile)) {
$helper.toast('手机号码格式不正确');
return;
}
// 发送验证码 + 更新上次发送验证码时间
$api.app.sendSms({ mobile, event }).then((res) => {
if (res.error === 0) {
modalStore.$patch((state) => {
state.lastTimer[event] = dayjs().unix();
});
}
});
}
// 获取短信验证码倒计时 -- 60秒
export function getSmsTimer(event, mobile = '') {
const modalStore = $store('modal');
const lastSendTimer = modalStore.lastTimer[event];
if (typeof lastSendTimer === 'undefined') {
$helper.toast('短信发送事件错误');
return;
}
const duration = ref(dayjs().unix() - lastSendTimer - 60);
const canSend = duration.value >= 0;
if (canSend) {
return '获取验证码';
}
if (!canSend) {
setTimeout(() => {
duration.value++;
}, 1000);
return -duration.value.toString() + ' 秒';
}
}
// 记录广告弹框历史
export function saveAdvHistory(adv) {
const modal = $store('modal');
modal.$patch((state) => {
if (!state.advHistory.includes(adv.src)) {
state.advHistory.push(adv.src);
}
});
}
+53
View File
@@ -0,0 +1,53 @@
import $api from '@/sheep/api';
import $url from '@/sheep/url';
import $router from '@/sheep/router';
import $platform from '@/sheep/platform';
import $helper from '@/sheep/helper';
import zIndex from '@/sheep/config/zIndex.js';
import $store from '@/sheep/store';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import duration from 'dayjs/plugin/duration';
import 'dayjs/locale/zh-cn';
dayjs.locale('zh-cn');
dayjs.extend(relativeTime);
dayjs.extend(duration);
const sheep = {
$api,
$store,
$url,
$router,
$platform,
$helper,
$zIndex: zIndex,
};
// 加载Shopro底层依赖
export async function ShoproInit() {
// 应用初始化
await $store('app').init();
// 平台初始化加载(各平台provider提供不同的加载流程)
$platform.load();
if (process.env.NODE_ENV === 'development') {
ShoproDebug();
}
}
// 开发模式
function ShoproDebug() {
// 开发环境引入vconsole调试
// #ifdef H5
// import("vconsole").then(vconsole => {
// new vconsole.default();
// });
// #endif
// 同步前端页面到后端
$api.app.pageSync(ROUTES);
}
export default sheep;
+101
View File
@@ -0,0 +1,101 @@
import buildURL from '../helpers/buildURL';
import buildFullPath from '../core/buildFullPath';
import settle from '../core/settle';
import { isUndefined } from '../utils';
/**
* 返回可选值存在的配置
* @param {Array} keys - 可选值数组
* @param {Object} config2 - 配置
* @return {{}} - 存在的配置项
*/
const mergeKeys = (keys, config2) => {
const config = {};
keys.forEach((prop) => {
if (!isUndefined(config2[prop])) {
config[prop] = config2[prop];
}
});
return config;
};
export default (config) =>
new Promise((resolve, reject) => {
const fullPath = buildURL(buildFullPath(config.baseURL, config.url), config.params);
const _config = {
url: fullPath,
header: config.header,
complete: (response) => {
config.fullPath = fullPath;
response.config = config;
try {
// 对可能字符串不是json 的情况容错
if (typeof response.data === 'string') {
response.data = JSON.parse(response.data);
}
// eslint-disable-next-line no-empty
} catch (e) {}
settle(resolve, reject, response);
},
};
let requestTask;
if (config.method === 'UPLOAD') {
delete _config.header['content-type'];
delete _config.header['Content-Type'];
const otherConfig = {
// #ifdef MP-ALIPAY
fileType: config.fileType,
// #endif
filePath: config.filePath,
name: config.name,
};
const optionalKeys = [
// #ifdef APP-PLUS || H5
'files',
// #endif
// #ifdef H5
'file',
// #endif
// #ifdef H5 || APP-PLUS
'timeout',
// #endif
'formData',
];
requestTask = uni.uploadFile({
..._config,
...otherConfig,
...mergeKeys(optionalKeys, config),
});
} else if (config.method === 'DOWNLOAD') {
// #ifdef H5 || APP-PLUS
if (!isUndefined(config.timeout)) {
_config.timeout = config.timeout;
}
// #endif
requestTask = uni.downloadFile(_config);
} else {
const optionalKeys = [
'data',
'method',
// #ifdef H5 || APP-PLUS || MP-ALIPAY || MP-WEIXIN
'timeout',
// #endif
'dataType',
// #ifndef MP-ALIPAY
'responseType',
// #endif
// #ifdef APP-PLUS
'sslVerify',
// #endif
// #ifdef H5
'withCredentials',
// #endif
// #ifdef APP-PLUS
'firstIpv4',
// #endif
];
requestTask = uni.request({ ..._config, ...mergeKeys(optionalKeys, config) });
}
if (config.getTask) {
config.getTask(requestTask, config);
}
});
@@ -0,0 +1,50 @@
'use strict';
function InterceptorManager() {
this.handlers = [];
}
/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled,
rejected,
});
return this.handlers.length - 1;
};
/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
this.handlers.forEach((h) => {
if (h !== null) {
fn(h);
}
});
};
export default InterceptorManager;
+198
View File
@@ -0,0 +1,198 @@
/**
* @Class Request
* @description luch-request http请求插件
* @version 3.0.7
* @Author lu-ch
* @Date 2021-09-04
* @Email webwork.s@qq.com
* 文档: https://www.quanzhan.co/luch-request/
* github: https://github.com/lei-mu/luch-request
* DCloud: http://ext.dcloud.net.cn/plugin?id=392
* HBuilderX: beat-3.0.4 alpha-3.0.4
*/
import dispatchRequest from './dispatchRequest';
import InterceptorManager from './InterceptorManager';
import mergeConfig from './mergeConfig';
import defaults from './defaults';
import { isPlainObject } from '../utils';
import clone from '../utils/clone';
export default class Request {
/**
* @param {Object} arg - 全局配置
* @param {String} arg.baseURL - 全局根路径
* @param {Object} arg.header - 全局header
* @param {String} arg.method = [GET|POST|PUT|DELETE|CONNECT|HEAD|OPTIONS|TRACE] - 全局默认请求方式
* @param {String} arg.dataType = [json] - 全局默认的dataType
* @param {String} arg.responseType = [text|arraybuffer] - 全局默认的responseType。支付宝小程序不支持
* @param {Object} arg.custom - 全局默认的自定义参数
* @param {Number} arg.timeout - 全局默认的超时时间,单位 ms。默认60000。H5(HBuilderX 2.9.9+)、APP(HBuilderX 2.9.9+)、微信小程序(2.10.0)、支付宝小程序
* @param {Boolean} arg.sslVerify - 全局默认的是否验证 ssl 证书。默认true.仅App安卓端支持(HBuilderX 2.3.3+
* @param {Boolean} arg.withCredentials - 全局默认的跨域请求时是否携带凭证(cookies)。默认false。仅H5支持(HBuilderX 2.6.15+
* @param {Boolean} arg.firstIpv4 - 全DNS解析时优先使用ipv4。默认false。仅 App-Android 支持 (HBuilderX 2.8.0+)
* @param {Function(statusCode):Boolean} arg.validateStatus - 全局默认的自定义验证器。默认statusCode >= 200 && statusCode < 300
*/
constructor(arg = {}) {
if (!isPlainObject(arg)) {
arg = {};
console.warn('设置全局参数必须接收一个Object');
}
this.config = clone({ ...defaults, ...arg });
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
}
/**
* @Function
* @param {Request~setConfigCallback} f - 设置全局默认配置
*/
setConfig(f) {
this.config = f(this.config);
}
middleware(config) {
config = mergeConfig(this.config, config);
const chain = [dispatchRequest, undefined];
let promise = Promise.resolve(config);
this.interceptors.request.forEach((interceptor) => {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach((interceptor) => {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
/**
* @Function
* @param {Object} config - 请求配置项
* @prop {String} options.url - 请求路径
* @prop {Object} options.data - 请求参数
* @prop {Object} [options.responseType = config.responseType] [text|arraybuffer] - 响应的数据类型
* @prop {Object} [options.dataType = config.dataType] - 如果设为 json,会尝试对返回的数据做一次 JSON.parse
* @prop {Object} [options.header = config.header] - 请求header
* @prop {Object} [options.method = config.method] - 请求方法
* @returns {Promise<unknown>}
*/
request(config = {}) {
return this.middleware(config);
}
get(url, options = {}) {
return this.middleware({
url,
method: 'GET',
...options,
});
}
post(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'POST',
...options,
});
}
// #ifndef MP-ALIPAY
put(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'PUT',
...options,
});
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
delete(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'DELETE',
...options,
});
}
// #endif
// #ifdef H5 || MP-WEIXIN
connect(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'CONNECT',
...options,
});
}
// #endif
// #ifdef H5 || MP-WEIXIN || MP-BAIDU
head(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'HEAD',
...options,
});
}
// #endif
// #ifdef APP-PLUS || H5 || MP-WEIXIN || MP-BAIDU
options(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'OPTIONS',
...options,
});
}
// #endif
// #ifdef H5 || MP-WEIXIN
trace(url, data, options = {}) {
return this.middleware({
url,
data,
method: 'TRACE',
...options,
});
}
// #endif
upload(url, config = {}) {
config.url = url;
config.method = 'UPLOAD';
return this.middleware(config);
}
download(url, config = {}) {
config.url = url;
config.method = 'DOWNLOAD';
return this.middleware(config);
}
}
/**
* setConfig回调
* @return {Object} - 返回操作后的config
* @callback Request~setConfigCallback
* @param {Object} config - 全局默认config
*/
@@ -0,0 +1,20 @@
'use strict';
import isAbsoluteURL from '../helpers/isAbsoluteURL';
import combineURLs from '../helpers/combineURLs';
/**
* Creates a new URL by combining the baseURL with the requestedURL,
* only when the requestedURL is not already an absolute URL.
* If the requestURL is absolute, this function returns the requestedURL untouched.
*
* @param {string} baseURL The base URL
* @param {string} requestedURL Absolute or relative URL to combine
* @returns {string} The combined full path
*/
export default function buildFullPath(baseURL, requestedURL) {
if (baseURL && !isAbsoluteURL(requestedURL)) {
return combineURLs(baseURL, requestedURL);
}
return requestedURL;
}

Some files were not shown because too many files have changed in this diff Show More