骑手端app代码仓库创建

This commit is contained in:
admin
2026-01-06 21:22:12 +08:00
commit 34c63780a8
467 changed files with 65334 additions and 0 deletions

66
sheep/api/infra/file.js Normal file
View File

@@ -0,0 +1,66 @@
import { baseUrl, apiPath, tenantId } from '@/sheep/config';
import request from '@/sheep/request';
const FileApi = {
// 上传文件
uploadFile: (file) => {
// TODO 芋艿:访问令牌的接入;
const token = uni.getStorageSync('token');
uni.showLoading({
title: '上传中',
});
return new Promise((resolve, reject) => {
uni.uploadFile({
url: baseUrl + apiPath + '/infra/file/upload',
filePath: file,
name: 'file',
header: {
// Accept: 'text/json',
Accept: '*/*',
'tenant-id': tenantId,
// Authorization: 'Bearer test247',
},
success: (uploadFileRes) => {
let result = JSON.parse(uploadFileRes.data);
if (result.error === 1) {
uni.showToast({
icon: 'none',
title: result.msg,
});
} else {
return resolve(result);
}
},
fail: (error) => {
console.log('上传失败:', error);
return resolve(false);
},
complete: () => {
uni.hideLoading();
},
});
});
},
// 获取文件预签名地址
getFilePresignedUrl: (path) => {
return request({
url: '/infra/file/presigned-url',
method: 'GET',
params: {
path,
},
});
},
// 创建文件
createFile: (data) => {
return request({
url: '/infra/file/create', // 请求的 URL
method: 'POST', // 请求方法
data: data, // 要发送的数据
});
},
};
export default FileApi;

View File

@@ -0,0 +1,61 @@
import request from '@/sheep/request';
const AddressApi = {
// 获得用户收件地址列表
getAddressList: () => {
return request({
url: '/member/address/list',
method: 'GET'
});
},
// 创建用户收件地址
createAddress: (data) => {
return request({
url: '/member/address/create',
method: 'POST',
data,
custom: {
showSuccess: true,
successMsg: '保存成功'
},
});
},
// 更新用户收件地址
updateAddress: (data) => {
return request({
url: '/member/address/update',
method: 'PUT',
data,
custom: {
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 获得用户收件地址
getAddress: (id) => {
return request({
url: '/member/address/get',
method: 'GET',
params: { id }
});
},
// 删除用户收件地址
deleteAddress: (id) => {
return request({
url: '/member/address/delete',
method: 'DELETE',
params: { id }
});
},
// 获得商户关联的冷库列表
getStorehouseList: () => {
return request({
url: '/delivery/storehouse/list',
method: 'GET',
});
},
};
export default AddressApi;

132
sheep/api/member/auth.js Normal file
View File

@@ -0,0 +1,132 @@
import request from '@/sheep/request';
const AuthUtil = {
// 使用手机 + 密码登录
login: (data) => {
return request({
url: '/member/auth/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 使用手机 + 验证码登录
smsLogin: (data) => {
return request({
url: '/member/auth/sms-login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登录中',
successMsg: '登录成功',
},
});
},
// 发送手机验证码
sendSmsCode: (mobile, scene) => {
return request({
url: '/member/auth/send-sms-code',
method: 'POST',
data: {
mobile,
scene,
},
custom: {
loadingMsg: '发送中',
showSuccess: true,
successMsg: '发送成功',
},
});
},
// 登出系统
logout: () => {
return request({
url: '/member/auth/logout',
method: 'POST',
});
},
// 刷新令牌
refreshToken: (refreshToken) => {
return request({
url: '/member/auth/refresh-token',
method: 'POST',
params: {
refreshToken
},
custom: {
loading: false, // 不用加载中
showError: false, // 不展示错误提示
},
});
},
// 社交授权的跳转
socialAuthRedirect: (type, redirectUri) => {
return request({
url: '/member/auth/social-auth-redirect',
method: 'GET',
params: {
type,
redirectUri,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 社交快捷登录
socialLogin: (type, code, state) => {
return request({
url: '/member/auth/social-login',
method: 'POST',
data: {
type,
code,
state,
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
});
},
// 微信小程序的一键登录
weixinMiniAppLogin: (phoneCode, loginCode, state) => {
return request({
url: '/member/auth/weixin-mini-app-login',
method: 'POST',
data: {
phoneCode,
loginCode,
state
},
custom: {
showSuccess: true,
loadingMsg: '登陆中',
successMsg: '登录成功',
},
});
},
// 创建微信 JS SDK 初始化所需的签名
createWeixinMpJsapiSignature: (url) => {
return request({
url: '/member/auth/create-weixin-jsapi-signature',
method: 'POST',
params: {
url
},
custom: {
showError: false,
showLoading: false,
},
})
},
//
};
export default AuthUtil;

View File

@@ -0,0 +1,76 @@
import request from '@/sheep/request';
const SocialApi = {
// 获得社交用户
getSocialUser: (type) => {
return request({
url: '/member/social-user/get',
method: 'GET',
params: {
type
},
custom: {
showLoading: false,
},
});
},
// 社交绑定
socialBind: (type, code, state) => {
return request({
url: '/member/social-user/bind',
method: 'POST',
data: {
type,
code,
state
},
custom: {
custom: {
showSuccess: true,
loadingMsg: '绑定中',
successMsg: '绑定成功',
},
},
});
},
// 社交绑定
socialUnbind: (type, openid) => {
return request({
url: '/member/social-user/unbind',
method: 'DELETE',
data: {
type,
openid
},
custom: {
showLoading: false,
loadingMsg: '解除绑定',
successMsg: '解绑成功',
},
});
},
// 获取订阅消息模板列表
getSubscribeTemplateList: () =>
request({
url: '/member/social-user/get-subscribe-template-list',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
}),
// 获取微信小程序码
getWxaQrcode: async (path, query) => {
return await request({
url: '/member/social-user/wxa-qrcode',
method: 'POST',
data: {
scene: query,
path,
checkPath: false, // TODO 开发环境暂不检查 path 是否存在
},
});
},
};
export default SocialApi;

98
sheep/api/member/user.js Normal file
View File

@@ -0,0 +1,98 @@
import request from '@/sheep/request';
const UserApi = {
// 获得基本信息
getUserInfo: () => {
return request({
url: '/member/user/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 修改基本信息
updateUser: (data) => {
return request({
url: '/member/user/update',
method: 'PUT',
data,
custom: {
auth: true,
showSuccess: true,
successMsg: '更新成功'
},
});
},
// 修改用户手机
updateUserMobile: (data) => {
return request({
url: '/member/user/update-mobile',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 基于微信小程序的授权码,修改用户手机
updateUserMobileByWeixin: (code) => {
return request({
url: '/member/user/update-mobile-by-weixin',
method: 'PUT',
data: {
code
},
custom: {
showSuccess: true,
loadingMsg: '获取中',
successMsg: '修改成功'
},
});
},
// 修改密码
updateUserPassword: (data) => {
return request({
url: '/member/user/update-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 修改密码
updateUserPasswordReset: (data) => {
return request({
url: '/member/user/update-password-by-reset',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
},
});
},
// 重置密码
resetUserPassword: (data) => {
return request({
url: '/member/user/reset-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功'
}
});
},
};
export default UserApi;

View File

@@ -0,0 +1,21 @@
import request from '@/sheep/request';
// TODO 芋艿:小程序直播还不支持
export default {
//小程序直播
mplive: {
getRoomList: (ids) =>
request({
url: 'app/mplive/getRoomList',
method: 'GET',
params: {
ids: ids.join(','),
}
}),
getMpLink: () =>
request({
url: 'app/mplive/getMpLink',
method: 'GET'
}),
},
};

View File

@@ -0,0 +1,10 @@
const files = import.meta.glob('./*.js', { eager: true });
let api = {};
Object.keys(files).forEach((key) => {
api = {
...api,
[key.replace(/(.*\/)*([^.]+).*/gi, '$2')]: files[key].default,
};
});
export default api;

View File

@@ -0,0 +1,18 @@
import request from '@/sheep/request';
export default {
// 苹果相关
apple: {
// 第三方登录
login: (data) =>
request({
url: 'third/apple/login',
method: 'POST',
data,
custom: {
showSuccess: true,
loadingMsg: '登陆中',
},
}),
},
};

14
sheep/api/pay/channel.js Normal file
View File

@@ -0,0 +1,14 @@
import request from '@/sheep/request';
const PayChannelApi = {
// 获得指定应用的开启的支付渠道编码列表
getEnableChannelCodeList: (appId) => {
return request({
url: '/pay/channel/get-enable-code-list',
method: 'GET',
params: { appId }
});
},
};
export default PayChannelApi;

22
sheep/api/pay/order.js Normal file
View File

@@ -0,0 +1,22 @@
import request from '@/sheep/request';
const PayOrderApi = {
// 获得支付订单
getOrder: (id, sync) => {
return request({
url: '/pay/order/get',
method: 'GET',
params: { id, sync },
});
},
// 提交支付订单
submitOrder: (data) => {
return request({
url: '/pay/order/submit',
method: 'POST',
data,
});
},
};
export default PayOrderApi;

68
sheep/api/pay/wallet.js Normal file
View File

@@ -0,0 +1,68 @@
import request from '@/sheep/request';
const PayWalletApi = {
// 获取钱包
getPayWallet() {
return request({
url: '/pay/wallet/get',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 获得钱包流水分页
getWalletTransactionPage: (params) => {
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/pay/wallet-transaction/page?${queryString}`,
method: 'GET',
});
},
// 获得钱包流水统计
getWalletTransactionSummary: (params) => {
const queryString = `createTime=${params.createTime[0]}&createTime=${params.createTime[1]}`;
return request({
url: `/pay/wallet-transaction/get-summary?${queryString}`,
// url: `/pay/wallet-transaction/get-summary`,
method: 'GET',
// params: params
});
},
// 获得钱包充值套餐列表
getWalletRechargePackageList: () => {
return request({
url: '/pay/wallet-recharge-package/list',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
});
},
// 创建钱包充值记录(发起充值)
createWalletRecharge: (data) => {
return request({
url: '/pay/wallet-recharge/create',
method: 'POST',
data,
});
},
// 获得钱包充值记录分页
getWalletRechargePage: (params) => {
return request({
url: '/pay/wallet-recharge/page',
method: 'GET',
params,
custom: {
showError: false,
showLoading: false,
},
});
},
};
export default PayWalletApi;

View File

@@ -0,0 +1,21 @@
import request from '@/sheep/request';
const CategoryApi = {
// 查询分类列表
getCategoryList: () => {
return request({
url: '/product/category/list',
method: 'GET',
});
},
// 查询分类列表,指定编号
getCategoryListByIds: (ids) => {
return request({
url: '/product/category/list-by-ids',
method: 'GET',
params: { ids },
});
},
};
export default CategoryApi;

View File

@@ -0,0 +1,22 @@
import request from '@/sheep/request';
const CommentApi = {
// 获得商品评价分页
getCommentPage: (spuId, pageNo, pageSize, type) => {
return request({
url: '/product/comment/page',
method: 'GET',
params: {
spuId,
pageNo,
pageSize,
type,
},
custom: {
showLoading: false,
showError: false,
},
});
},
};
export default CommentApi;

View File

@@ -0,0 +1,54 @@
import request from '@/sheep/request';
const FavoriteApi = {
// 获得商品收藏分页
getFavoritePage: (data) => {
return request({
url: '/product/favorite/page',
method: 'GET',
params: data,
});
},
// 检查是否收藏过商品
isFavoriteExists: (spuId) => {
return request({
url: '/product/favorite/exits',
method: 'GET',
params: {
spuId,
},
});
},
// 添加商品收藏
createFavorite: (spuId) => {
return request({
url: '/product/favorite/create',
method: 'POST',
data: {
spuId,
},
custom: {
auth: true,
showSuccess: true,
successMsg: '收藏成功',
},
});
},
// 取消商品收藏
deleteFavorite: (spuId) => {
return request({
url: '/product/favorite/delete',
method: 'DELETE',
data: {
spuId,
},
custom: {
auth: true,
showSuccess: true,
successMsg: '取消成功',
},
});
},
};
export default FavoriteApi;

View File

@@ -0,0 +1,39 @@
import request from '@/sheep/request';
const SpuHistoryApi = {
// 删除商品浏览记录
deleteBrowseHistory: (spuIds) => {
return request({
url: '/product/browse-history/delete',
method: 'DELETE',
data: { spuIds },
custom: {
showSuccess: true,
successMsg: '删除成功',
},
});
},
// 清空商品浏览记录
cleanBrowseHistory: () => {
return request({
url: '/product/browse-history/clean',
method: 'DELETE',
custom: {
showSuccess: true,
successMsg: '清空成功',
},
});
},
// 获得商品浏览记录分页
getBrowseHistoryPage: (data) => {
return request({
url: '/product/browse-history/page',
method: 'GET',
data,
custom: {
showLoading: false
},
});
},
};
export default SpuHistoryApi;

53
sheep/api/product/spu.js Normal file
View File

@@ -0,0 +1,53 @@
import request from '@/sheep/request';
const SpuApi = {
// 获得商品 SPU 列表
getSpuListByIds: (ids) => {
return request({
url: '/product/spu/list-by-ids',
method: 'GET',
params: { ids },
custom: {
showLoading: false,
showError: false,
},
});
},
// 获得商品结算信息
getSettlementProduct: (spuIds) => {
return request({
url: '/trade/order/settlement-product',
method: 'GET',
params: { spuIds },
custom: {
showLoading: false,
showError: false,
},
});
},
// 获得商品 SPU 分页
getSpuPage: (params) => {
return request({
url: '/product/spu/page',
method: 'GET',
params,
custom: {
showLoading: false,
showError: false,
},
});
},
// 查询商品
getSpuDetail: (id) => {
return request({
url: '/product/spu/get-detail',
method: 'GET',
params: { id },
custom: {
showLoading: false,
showError: false,
},
});
},
};
export default SpuApi;

View File

@@ -0,0 +1,16 @@
import request from '@/sheep/request';
const ActivityApi = {
// 获得单个商品,进行中的拼团、秒杀、砍价活动信息
getActivityListBySpuId: (spuId) => {
return request({
url: '/promotion/activity/list-by-spu-id',
method: 'GET',
params: {
spuId,
},
});
},
};
export default ActivityApi;

View File

@@ -0,0 +1,12 @@
import request from '@/sheep/request';
export default {
// 获得文章详情
getArticle: (id, title) => {
return request({
url: '/promotion/article/get',
method: 'GET',
params: { id, title }
});
}
}

View File

@@ -0,0 +1,78 @@
import request from '@/sheep/request';
// 拼团 API
const CombinationApi = {
// 获得拼团活动分页
getCombinationActivityPage: (params) => {
return request({
url: '/promotion/combination-activity/page',
method: 'GET',
params,
});
},
// 获得拼团活动明细
getCombinationActivity: (id) => {
return request({
url: '/promotion/combination-activity/get-detail',
method: 'GET',
params: {
id,
},
});
},
// 获得拼团活动列表,基于活动编号数组
getCombinationActivityListByIds: (ids) => {
return request({
url: '/promotion/combination-activity/list-by-ids',
method: 'GET',
params: {
ids,
},
});
},
// 获得最近 n 条拼团记录(团长发起的)
getHeadCombinationRecordList: (activityId, status, count) => {
return request({
url: '/promotion/combination-record/get-head-list',
method: 'GET',
params: {
activityId,
status,
count,
},
});
},
// 获得我的拼团记录分页
getCombinationRecordPage: (params) => {
return request({
url: '/promotion/combination-record/page',
method: 'GET',
params,
});
},
// 获得拼团记录明细
getCombinationRecordDetail: (id) => {
return request({
url: '/promotion/combination-record/get-detail',
method: 'GET',
params: {
id,
},
});
},
// 获得拼团记录的概要信息
getCombinationRecordSummary: () => {
return request({
url: '/promotion/combination-record/get-summary',
method: 'GET',
});
},
};
export default CombinationApi;

View File

@@ -0,0 +1,84 @@
import request from '@/sheep/request';
const CouponApi = {
// 获得优惠劵模板列表
getCouponTemplateListByIds: (ids) => {
return request({
url: '/promotion/coupon-template/list-by-ids',
method: 'GET',
params: { ids },
custom: {
showLoading: false, // 不展示 Loading避免领取优惠劵时不成功提示
showError: false,
},
});
},
// 获得优惠劵模版列表
getCouponTemplateList: (spuId, productScope, count) => {
return request({
url: '/promotion/coupon-template/list',
method: 'GET',
params: { spuId, productScope, count },
});
},
// 获得优惠劵模版分页
getCouponTemplatePage: (params) => {
return request({
url: '/promotion/coupon-template/page',
method: 'GET',
params,
});
},
// 获得优惠劵模版
getCouponTemplate: (id) => {
return request({
url: '/promotion/coupon-template/get',
method: 'GET',
params: { id },
});
},
// 我的优惠劵列表
getCouponPage: (params) => {
return request({
url: '/promotion/coupon/page',
method: 'GET',
params,
});
},
// 领取优惠券
takeCoupon: (templateId) => {
return request({
url: '/promotion/coupon/take',
method: 'POST',
data: { templateId },
custom: {
auth: true,
showLoading: true,
loadingMsg: '领取中',
showSuccess: true,
successMsg: '领取成功',
},
});
},
// 获得优惠劵
getCoupon: (id) => {
return request({
url: '/promotion/coupon/get',
method: 'GET',
params: { id },
});
},
// 获得未使用的优惠劵数量
getUnusedCouponCount: () => {
return request({
url: '/promotion/coupon/get-unused-count',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
};
export default CouponApi;

View File

@@ -0,0 +1,38 @@
import request from '@/sheep/request';
const DiyApi = {
getUsedDiyTemplate: () => {
return request({
url: '/promotion/diy-template/used',
method: 'GET',
custom: {
showError: false,
showLoading: false,
},
});
},
getDiyTemplate: (id) => {
return request({
url: '/promotion/diy-template/get',
method: 'GET',
params: {
id
},
custom: {
showError: false,
showLoading: false,
},
});
},
getDiyPage: (id) => {
return request({
url: '/promotion/diy-page/get',
method: 'GET',
params: {
id
}
});
},
};
export default DiyApi;

View File

@@ -0,0 +1,31 @@
import request from '@/sheep/request';
const KeFuApi = {
sendKefuMessage: (data) => {
return request({
url: '/promotion/kefu-message/send',
method: 'POST',
data,
custom: {
auth: true,
showLoading: true,
loadingMsg: '发送中',
showSuccess: true,
successMsg: '发送成功',
},
});
},
getKefuMessageList: (params) => {
return request({
url: '/promotion/kefu-message/list',
method: 'GET',
params,
custom: {
auth: true,
showLoading: false,
},
});
},
};
export default KeFuApi;

View File

@@ -0,0 +1,30 @@
import request from '@/sheep/request';
const PointApi = {
// 获得积分商城活动分页
getPointActivityPage: (params) => {
return request({ url: 'promotion/point-activity/page', method: 'GET', params });
},
// 获得积分商城活动列表,基于活动编号数组
getPointActivityListByIds: (ids) => {
return request({
url: '/promotion/point-activity/list-by-ids',
method: 'GET',
params: {
ids,
},
});
},
// 获得积分商城活动明细
getPointActivity: (id) => {
return request({
url: 'promotion/point-activity/get-detail',
method: 'GET',
params: { id },
});
},
};
export default PointApi;

View File

@@ -0,0 +1,14 @@
import request from '@/sheep/request';
const RewardActivityApi = {
// 获得满减送活动
getRewardActivity: (id) => {
return request({
url: '/promotion/reward-activity/get',
method: 'GET',
params: { id },
});
}
};
export default RewardActivityApi;

View File

@@ -0,0 +1,44 @@
import request from '@/sheep/request';
const SeckillApi = {
// 获得秒杀时间段列表
getSeckillConfigList: () => {
return request({ url: 'promotion/seckill-config/list', method: 'GET' });
},
// 获得当前秒杀活动
getNowSeckillActivity: () => {
return request({ url: 'promotion/seckill-activity/get-now', method: 'GET' });
},
// 获得秒杀活动分页
getSeckillActivityPage: (params) => {
return request({ url: 'promotion/seckill-activity/page', method: 'GET', params });
},
// 获得秒杀活动列表,基于活动编号数组
getSeckillActivityListByIds: (ids) => {
return request({
url: '/promotion/seckill-activity/list-by-ids',
method: 'GET',
params: {
ids,
},
});
},
/**
* 获得秒杀活动明细
* @param {number} id 秒杀活动编号
* @return {*}
*/
getSeckillActivity: (id) => {
return request({
url: 'promotion/seckill-activity/get-detail',
method: 'GET',
params: { id },
});
},
};
export default SeckillApi;

13
sheep/api/system/area.js Normal file
View File

@@ -0,0 +1,13 @@
import request from '@/sheep/request';
const AreaApi = {
// 获得地区树
getAreaTree: () => {
return request({
url: '/system/area/tree',
method: 'GET'
});
},
};
export default AreaApi;

16
sheep/api/system/dict.js Normal file
View File

@@ -0,0 +1,16 @@
import request from '@/sheep/request';
const DictApi = {
// 根据字典类型查询字典数据信息
getDictDataListByType: (type) => {
return request({
url: `/system/dict-data/type`,
method: 'GET',
params: {
type,
},
});
},
};
export default DictApi;

View File

@@ -0,0 +1,115 @@
import request from '@/sheep/request';
const AfterSaleApi = {
// 获得售后分页
getAfterSalePage: (params) => {
return request({
url: `/trade/after-sale/page`,
method: 'GET',
params,
custom: {
showLoading: false,
},
});
},
// 创建售后
createAfterSale: (data) => {
return request({
url: `/trade/after-sale/create`,
method: 'POST',
data,
});
},
// 获得售后
getAfterSale: (id) => {
return request({
url: `/trade/after-sale/get`,
method: 'GET',
params: {
id,
},
});
},
// 取消售后
cancelAfterSale: (id) => {
return request({
url: `/trade/after-sale/cancel`,
method: 'DELETE',
params: {
id,
},
});
},
// 获得售后日志列表
getAfterSaleLogList: (afterSaleId) => {
return request({
url: `/trade/after-sale-log/list`,
method: 'GET',
params: {
afterSaleId,
},
});
},
// 退回货物
deliveryAfterSale: (data) => {
return request({
url: `/trade/after-sale/delivery`,
method: 'PUT',
data,
});
},
// 创建售后单
afterOrderCreate: (data) => {
return request({
url: `/merchants/after-order/create`,
method: 'POST',
data,
});
},
// 获得售后单分页
afterOrderPage: (data) => {
return request({
url: `/merchants/after-order/page`,
method: 'GET',
data,
});
},
// 获得售后单详情
afterOrderGet: (id) => {
return request({
url: `/merchants/after-order/get`,
method: 'GET',
params: {
id,
},
});
},
// 取消售后
afterOrderCancel: (id) => {
return request({
url: `/merchants/after-order/cancel`,
method: 'GET',
params: {
id,
},
});
},
// 修改售后单
afterOrderUpdate: (data) => {
return request({
url: `/merchants/after-order/update`,
method: 'POST',
data,
});
},
// 售后单发货状态标记已完成
afterOrderFinish: (data) => {
return request({
url: `/merchants/after-order/finish`,
method: 'POST',
data,
});
},
};
export default AfterSaleApi;

View File

@@ -0,0 +1,93 @@
import request from '@/sheep/request';
const BrokerageApi = {
// 绑定分销用户
bindBrokerageUser: (data)=>{
return request({
url: '/trade/brokerage-user/bind',
method: 'PUT',
data
});
},
// 获得个人分销信息
getBrokerageUser: () => {
return request({
url: '/trade/brokerage-user/get',
method: 'GET'
});
},
// 获得个人分销统计
getBrokerageUserSummary: () => {
return request({
url: '/trade/brokerage-user/get-summary',
method: 'GET',
});
},
// 获得分销记录分页
getBrokerageRecordPage: params => {
if (params.status === undefined) {
delete params.status
}
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-record/page?${queryString}`,
method: 'GET',
});
},
// 创建分销提现
createBrokerageWithdraw: data => {
return request({
url: '/trade/brokerage-withdraw/create',
method: 'POST',
data,
});
},
// 获得商品的分销金额
getProductBrokeragePrice: spuId => {
return request({
url: '/trade/brokerage-record/get-product-brokerage-price',
method: 'GET',
params: { spuId }
});
},
// 获得分销用户排行(基于佣金)
getRankByPrice: params => {
const queryString = `times=${params.times[0]}&times=${params.times[1]}`;
return request({
url: `/trade/brokerage-user/get-rank-by-price?${queryString}`,
method: 'GET',
});
},
// 获得分销用户排行分页(基于佣金)
getBrokerageUserChildSummaryPageByPrice: params => {
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-user/rank-page-by-price?${queryString}`,
method: 'GET',
});
},
// 获得分销用户排行分页(基于用户量)
getBrokerageUserRankPageByUserCount: params => {
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + '=' + params[key])
.join('&');
return request({
url: `/trade/brokerage-user/rank-page-by-user-count?${queryString}`,
method: 'GET',
});
},
// 获得下级分销统计分页
getBrokerageUserChildSummaryPage: params => {
return request({
url: '/trade/brokerage-user/child-summary-page',
method: 'GET',
params,
})
}
}
export default BrokerageApi

50
sheep/api/trade/cart.js Normal file
View File

@@ -0,0 +1,50 @@
import request from '@/sheep/request';
const CartApi = {
addCart: (data) => {
return request({
url: '/trade/cart/add',
method: 'POST',
data: data,
custom: {
showSuccess: true,
successMsg: '已添加到购物车~',
}
});
},
updateCartCount: (data) => {
return request({
url: '/trade/cart/update-count',
method: 'PUT',
data: data
});
},
updateCartSelected: (data) => {
return request({
url: '/trade/cart/update-selected',
method: 'PUT',
data: data
});
},
deleteCart: (ids) => {
return request({
url: '/trade/cart/delete',
method: 'DELETE',
params: {
ids
}
});
},
getCartList: () => {
return request({
url: '/trade/cart/list',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
};
export default CartApi;

16
sheep/api/trade/config.js Normal file
View File

@@ -0,0 +1,16 @@
import request from '@/sheep/request';
const TradeConfigApi = {
// 获得交易配置
getTradeConfig: () => {
return request({
url: `/trade/config/get`,
method: 'GET',
custom: {
showLoading: false,
},
});
},
};
export default TradeConfigApi;

View File

@@ -0,0 +1,31 @@
import request from '@/sheep/request';
const DeliveryApi = {
// 获得快递公司列表
getDeliveryExpressList: () => {
return request({
url: `/trade/delivery/express/list`,
method: 'get',
});
},
// 获得自提门店列表
getDeliveryPickUpStoreList: (params) => {
return request({
url: `/trade/delivery/pick-up-store/list`,
method: 'GET',
params,
});
},
// 获得自提门店
getDeliveryPickUpStore: (id) => {
return request({
url: `/trade/delivery/pick-up-store/get`,
method: 'GET',
params: {
id,
},
});
},
};
export default DeliveryApi;

196
sheep/api/trade/order.js Normal file
View File

@@ -0,0 +1,196 @@
import request from '@/sheep/request';
import { isEmpty } from '@/sheep/helper/utils';
const OrderApi = {
// 计算订单信息
settlementOrder: (data) => {
const data2 = {
...data,
};
// 移除多余字段
if (!(data.couponId > 0)) {
delete data2.couponId;
}
if (!(data.addressId > 0)) {
delete data2.addressId;
}
if (!(data.pickUpStoreId > 0)) {
delete data2.pickUpStoreId;
}
if (isEmpty(data.receiverName)) {
delete data2.receiverName;
}
if (isEmpty(data.receiverMobile)) {
delete data2.receiverMobile;
}
if (!(data.combinationActivityId > 0)) {
delete data2.combinationActivityId;
}
if (!(data.combinationHeadId > 0)) {
delete data2.combinationHeadId;
}
if (!(data.seckillActivityId > 0)) {
delete data2.seckillActivityId;
}
if (!(data.pointActivityId > 0)) {
delete data2.pointActivityId;
}
if (!(data.deliveryType > 0)) {
delete data2.deliveryType;
}
// 解决 SpringMVC 接受 List<Item> 参数的问题
delete data2.items;
for (let i = 0; i < data.items.length; i++) {
data2[encodeURIComponent('items[' + i + '' + '].skuId')] = data.items[i].skuId + '';
data2[encodeURIComponent('items[' + i + '' + '].count')] = data.items[i].count + '';
if (data.items[i].cartId) {
data2[encodeURIComponent('items[' + i + '' + '].cartId')] = data.items[i].cartId + '';
}
}
const queryString = Object.keys(data2)
.map((key) => key + '=' + data2[key])
.join('&');
return request({
url: `/trade/order/settlement?${queryString}`,
method: 'GET',
custom: {
showError: true,
showLoading: true,
},
});
},
// 获得商品结算信息
getSettlementProduct: (spuIds) => {
return request({
url: '/trade/order/settlement-product',
method: 'GET',
params: { spuIds },
custom: {
showLoading: false,
showError: false,
},
});
},
// 创建订单
createOrder: (data) => {
return request({
url: `/trade/order/create`,
method: 'POST',
data,
});
},
// 获得订单详细sync 是可选参数
getOrderDetail: (id, sync) => {
return request({
url: `/trade/order/get-detail`,
method: 'GET',
params: {
id,
sync,
},
custom: {
showLoading: false,
},
});
},
// 订单列表
getOrderPage: (params) => {
return request({
url: '/trade/order/page',
method: 'GET',
params,
custom: {
showLoading: false,
},
});
},
// 确认收货
receiveOrder: (id) => {
return request({
url: `/trade/order/receive`,
method: 'PUT',
params: {
id,
},
});
},
// 取消订单
cancelOrder: (id, cancelReason) => {
return request({
url: `/trade/order/cancel`,
method: 'DELETE',
params: {
id,
cancelReason
}
});
},
// 删除订单
deleteOrder: (id) => {
return request({
url: `/trade/order/delete`,
method: 'DELETE',
params: {
id,
},
});
},
// 获得交易订单的物流轨迹
getOrderExpressTrackList: (id) => {
return request({
url: `/trade/order/get-express-track-list`,
method: 'GET',
params: {
id,
},
});
},
// 获得交易订单数量
getOrderCount: () => {
return request({
url: '/trade/order/get-count',
method: 'GET',
custom: {
showLoading: false,
auth: true,
},
});
},
// 创建单个评论
createOrderItemComment: (data) => {
return request({
url: `/trade/order/item/create-comment`,
method: 'POST',
data,
});
},
// 通过商户订单ID获取相关发货单列表
getListByTradeOrderId: (data) => {
return request({
url: '/delivery/order/getListByTradeOrderId',
method: 'GET',
data
});
},
// 确认签收
confirmSign: (data) => {
return request({
url: '/delivery/sign-order/confirmSign',
method: 'POST',
data
});
},
// 撤回取消交易订单
withdrawOrder: (id) => {
return request({
url: `/trade/order/withdraw`,
method: 'PUT',
params: {
id,
},
});
},
};
export default OrderApi;

View File

@@ -0,0 +1,196 @@
<!-- TODO 是不是怎么复用 s-count-down 组件 -->
<template>
<view class="time" :style="justifyLeft">
<text class="" v-if="tipText">{{ tipText }}</text>
<text
class="styleAll p6"
v-if="isDay === true"
:style="{ background: bgColor.bgColor, color: bgColor.Color }"
>{{ day }}{{ bgColor.isDay ? '天' : '' }}</text
>
<text
class="timeTxt"
v-if="dayText"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ dayText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ hour }}</text
>
<text
class="timeTxt"
v-if="hourText"
:class="isCol ? 'whit' : ''"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ hourText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ minute }}</text
>
<text
class="timeTxt"
v-if="minuteText"
:class="isCol ? 'whit' : ''"
:style="{ width: bgColor.timeTxtwidth, color: bgColor.bgColor }"
>{{ minuteText }}</text
>
<text
class="styleAll"
:class="isCol ? 'timeCol' : ''"
:style="{ background: bgColor.bgColor, color: bgColor.Color, width: bgColor.width }"
>{{ second }}</text
>
<text class="timeTxt" v-if="secondText">{{ secondText }}</text>
</view>
</template>
<script>
export default {
name: 'countDown',
props: {
justifyLeft: {
type: String,
default: '',
},
//距离开始提示文字
tipText: {
type: String,
default: '倒计时',
},
dayText: {
type: String,
default: '天',
},
hourText: {
type: String,
default: '时',
},
minuteText: {
type: String,
default: '分',
},
secondText: {
type: String,
default: '秒',
},
datatime: {
type: Number,
default: 0,
},
isDay: {
type: Boolean,
default: true,
},
isCol: {
type: Boolean,
default: false,
},
bgColor: {
type: Object,
default: null,
},
},
data: function () {
return {
day: '00',
hour: '00',
minute: '00',
second: '00',
};
},
created: function () {
this.show_time();
},
mounted: function () {},
methods: {
show_time: function () {
let that = this;
function runTime() {
//时间函数
let intDiff = that.datatime - Date.parse(new Date()) / 1000; //获取数据中的时间戳的时间差;
let day = 0,
hour = 0,
minute = 0,
second = 0;
if (intDiff > 0) {
//转换时间
if (that.isDay === true) {
day = Math.floor(intDiff / (60 * 60 * 24));
} else {
day = 0;
}
hour = Math.floor(intDiff / (60 * 60)) - day * 24;
minute = Math.floor(intDiff / 60) - day * 24 * 60 - hour * 60;
second = Math.floor(intDiff) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
if (hour <= 9) hour = '0' + hour;
if (minute <= 9) minute = '0' + minute;
if (second <= 9) second = '0' + second;
that.day = day;
that.hour = hour;
that.minute = minute;
that.second = second;
} else {
that.day = '00';
that.hour = '00';
that.minute = '00';
that.second = '00';
}
}
runTime();
setInterval(runTime, 1000);
},
},
};
</script>
<style scoped>
.p6 {
padding: 0 8rpx;
}
.styleAll {
/* color: #fff; */
font-size: 24rpx;
height: 36rpx;
line-height: 36rpx;
border-radius: 6rpx;
text-align: center;
/* padding: 0 6rpx; */
}
.timeTxt {
text-align: center;
/* width: 16rpx; */
height: 36rpx;
line-height: 36rpx;
display: inline-block;
}
.whit {
color: #fff !important;
}
.time {
display: flex;
justify-content: center;
}
.red {
color: #fc4141;
margin: 0 4rpx;
}
.timeCol {
/* width: 40rpx;
height: 40rpx;
line-height: 40rpx;
text-align:center;
border-radius: 6px;
background: #fff;
font-size: 24rpx; */
color: #e93323;
}
</style>

View File

@@ -0,0 +1,108 @@
<!-- 账号密码登录 accountLogin -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="ss-m-b-20">
<!-- <view class="head-title-active head-title-line" @tap="showAuthModal('smsLogin')">
短信登录
</view> -->
<view class="head-title head-title-animation">登录账号</view>
</view>
<view class="head-subtitle">如无账号请联系平台开通</view>
<!-- <view class="head-subtitle">如果未设置过密码请点击忘记密码</view> -->
</view>
<!-- 表单项 -->
<uni-forms
ref="accountLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="账号">
<uni-easyinput placeholder="请输入手机号" v-model="state.model.mobile" :inputBorder="false">
<!-- <template v-slot:right>
<button class="ss-reset-button forgot-btn" @tap="showAuthModal('resetPassword')">
忘记密码
</button>
</template> -->
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput
type="password"
placeholder="请输入密码"
v-model="state.model.password"
:inputBorder="false"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="accountLoginSubmit">登录</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { mobile, password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
const accountLoginRef = ref(null);
const emits = defineEmits(['onConfirm']);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
model: {
mobile: '', // 账号
password: '', // 密码
},
rules: {
mobile,
password,
},
});
// 账号登录
async function accountLoginSubmit() {
// 表单验证
const validate = await unref(accountLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) return;
// 同意协议
if (!props.agreeStatus) {
emits('onConfirm', true)
sheep.$helper.toast('请勾选同意');
return;
}
// 提交数据
const { code, data } = await AuthUtil.login(state.model);
if (code === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,127 @@
<!-- 绑定/更换手机号 changeMobile -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20">
{{ userInfo.mobile ? '更换手机号' : '绑定手机号' }}
</view>
<view class="head-subtitle">为了您的账号安全请使用本人手机号码</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="changeMobileRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
type="number"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('changeMobile', state.model.mobile)"
>
{{ getSmsTimer('changeMobile') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
:inputBorder="false"
type="number"
maxlength="4"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="changeMobileSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<!-- 微信独有读取手机号 -->
<button
v-if="'WechatMiniProgram' === sheep.$platform.name"
class="ss-reset-button type-btn"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
使用微信手机号
</button>
</view>
</template>
<script setup>
import { computed, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile } from '@/sheep/validate/form';
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const changeMobileRef = ref(null);
const userInfo = computed(() => sheep.$store('user').userInfo);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 绑定手机号
async function changeMobileSubmit() {
const validate = await unref(changeMobileRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 提交更新请求
const { code } = await UserApi.updateUserMobile(state.model);
if (code !== 0) {
return;
}
sheep.$store('user').getInfo();
closeAuthModal();
}
// 使用微信手机号
async function getPhoneNumber(e) {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
return;
}
const result = await sheep.$platform.useProvider().bindUserPhoneNumber(e.detail);
if (result) {
sheep.$store('user').getInfo();
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,113 @@
<!-- 修改密码登录时 -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20 head-title-animation text-bold">修改密码</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="reNewPassword" label="手机号">
<uni-easyinput
type="number"
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
maxlength="11"
>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="reNewPassword" label="新密码">
<uni-easyinput
type="password"
placeholder="请输入新密码"
v-model="state.model.newPassword"
:inputBorder="false"
maxlength="16"
>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="reNewPassword" label="确认密码">
<uni-easyinput
type="password"
placeholder="请重新输入新密码进行确认"
v-model="state.model.ackPassword"
:inputBorder="false"
maxlength="16"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="changePasswordSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button class="ss-reset-button type-btn" @tap="closeAuthModal">
取消修改
</button>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import { ackPassword, newPassword, mobile } from '@/sheep/validate/form';
import { closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const changePasswordRef = ref(null);
// 数据
const state = reactive({
model: {
mobile: '', // 手机号
newPassword: '', // 密码
ackPassword: '', // 密码
},
rules: {
mobile,
newPassword,
ackPassword
},
});
// 更改密码
async function changePasswordSubmit() {
// 参数校验
const validate = await unref(changePasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 发起请求
const { code } = await UserApi.updateUserPasswordReset(state.model);
if (code !== 0) {
return;
}
// 成功后,只需要关闭弹窗
closeAuthModal();
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
.text-bold {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,152 @@
<!-- 微信授权信息 mpAuthorization -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60 ss-flex-col">
<view class="ss-flex ss-m-b-20">
<view class="head-title ss-m-r-40 head-title-animation">授权信息</view>
</view>
<view class="head-subtitle">完善您的头像昵称手机号</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="accountLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<!-- 获取头像昵称https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html -->
<uni-forms-item name="avatar" label="头像">
<button
class="ss-reset-button avatar-btn"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<image
class="avatar-img"
:src="sheep.$url.cdn(state.model.avatar)"
mode="aspectFill"
@tap="sheep.$router.go('/pages/user/info')"
/>
<text class="cicon-forward" />
</button>
</uni-forms-item>
<uni-forms-item name="nickname" label="昵称">
<uni-easyinput
type="nickname"
placeholder="请输入昵称"
v-model="state.model.nickname"
:inputBorder="false"
/>
</uni-forms-item>
<view class="foot-box">
<button class="ss-reset-button authorization-btn" @tap="onConfirm"> 确认授权 </button>
</view>
</uni-forms>
</view>
</template>
<script setup>
import { computed, ref, reactive } from 'vue';
import sheep from '@/sheep';
import { closeAuthModal } from '@/sheep/hooks/useModal';
import FileApi from '@/sheep/api/infra/file';
import UserApi from '@/sheep/api/member/user';
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
const userInfo = computed(() => sheep.$store('user').userInfo);
const accountLoginRef = ref(null);
// 数据
const state = reactive({
model: {
nickname: userInfo.value.nickname,
avatar: userInfo.value.avatar,
},
rules: {},
disabledStyle: {
color: '#999',
disableColor: '#fff',
},
});
// 选择头像(来自微信)
function onChooseAvatar(e) {
const tempUrl = e.detail.avatarUrl || '';
uploadAvatar(tempUrl);
}
// 选择头像(来自文件系统)
async function uploadAvatar(tempUrl) {
if (!tempUrl) {
return;
}
let { data } = await FileApi.uploadFile(tempUrl);
state.model.avatar = data;
}
// 确认授权
async function onConfirm() {
const { model } = state;
const { nickname, avatar } = model;
if (!nickname) {
sheep.$helper.toast('请输入昵称');
return;
}
if (!avatar) {
sheep.$helper.toast('请选择头像');
return;
}
// 发起更新
const { code } = await UserApi.updateUser({
avatar: state.model.avatar,
nickname: state.model.nickname,
});
// 更新成功
if (code === 0) {
sheep.$helper.toast('授权成功');
await sheep.$store('user').getInfo();
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
.foot-box {
width: 100%;
display: flex;
justify-content: center;
}
.authorization-btn {
width: 686rpx;
height: 80rpx;
background-color: var(--ui-BG-Main);
border-radius: 40rpx;
color: #fff;
}
.avatar-img {
width: 72rpx;
height: 72rpx;
border-radius: 36rpx;
}
.cicon-forward {
font-size: 30rpx;
color: #595959;
}
.avatar-btn {
width: 100%;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,119 @@
<!-- 重置密码未登录时 -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="head-title ss-m-b-20">重置密码</view>
<view class="head-subtitle">为了您的账号安全设置密码前请先进行安全验证</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="resetPasswordRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
type="number"
:inputBorder="false"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('resetPassword', state.model.mobile)"
>
{{ getSmsTimer('resetPassword') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
type="number"
maxlength="4"
:inputBorder="false"
/>
</uni-forms-item>
<uni-forms-item name="password" label="密码">
<uni-easyinput
type="password"
placeholder="请输入密码"
v-model="state.model.password"
:inputBorder="false"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="resetPasswordSubmit">
确认
</button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
<button v-if="!isLogin" class="ss-reset-button type-btn" @tap="showAuthModal('accountLogin')">
返回登录
</button>
</view>
</template>
<script setup>
import { computed, ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile, password } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import UserApi from '@/sheep/api/member/user';
const resetPasswordRef = ref(null);
const isLogin = computed(() => sheep.$store('user').isLogin);
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
model: {
mobile: '', // 手机号
code: '', // 验证码
password: '', // 密码
},
rules: {
code,
mobile,
password,
},
});
// 重置密码
const resetPasswordSubmit = async () => {
// 参数校验
const validate = await unref(resetPasswordRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
// 发起请求
const { code } = await UserApi.resetUserPassword(state.model);
if (code !== 0) {
return;
}
// 成功后,用户重新登录
showAuthModal('accountLogin')
};
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,119 @@
<!-- 短信登录 - smsLogin -->
<template>
<view>
<!-- 标题栏 -->
<view class="head-box ss-m-b-60">
<view class="ss-flex ss-m-b-20">
<view class="head-title head-title-line head-title-animation">短信登录</view>
<view class="head-title-active ss-m-r-40" @tap="showAuthModal('accountLogin')">
账号登录
</view>
</view>
<view class="head-subtitle">未注册的手机号验证后自动注册账号</view>
</view>
<!-- 表单项 -->
<uni-forms
ref="smsLoginRef"
v-model="state.model"
:rules="state.rules"
validateTrigger="bind"
labelWidth="140"
labelAlign="center"
>
<uni-forms-item name="mobile" label="手机号">
<uni-easyinput
placeholder="请输入手机号"
v-model="state.model.mobile"
:inputBorder="false"
type="number"
>
<template v-slot:right>
<button
class="ss-reset-button code-btn code-btn-start"
:disabled="state.isMobileEnd"
:class="{ 'code-btn-end': state.isMobileEnd }"
@tap="getSmsCode('smsLogin', state.model.mobile)"
>
{{ getSmsTimer('smsLogin') }}
</button>
</template>
</uni-easyinput>
</uni-forms-item>
<uni-forms-item name="code" label="验证码">
<uni-easyinput
placeholder="请输入验证码"
v-model="state.model.code"
:inputBorder="false"
type="number"
maxlength="4"
>
<template v-slot:right>
<button class="ss-reset-button login-btn-start" @tap="smsLoginSubmit"> 登录 </button>
</template>
</uni-easyinput>
</uni-forms-item>
</uni-forms>
</view>
</template>
<script setup>
import { ref, reactive, unref } from 'vue';
import sheep from '@/sheep';
import { code, mobile } from '@/sheep/validate/form';
import { showAuthModal, closeAuthModal, getSmsCode, getSmsTimer } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
const smsLoginRef = ref(null);
const emits = defineEmits(['onConfirm']);
const props = defineProps({
agreeStatus: {
type: Boolean,
default: false,
},
});
// 数据
const state = reactive({
isMobileEnd: false, // 手机号输入完毕
codeText: '获取验证码',
model: {
mobile: '', // 手机号
code: '', // 验证码
},
rules: {
code,
mobile,
},
});
// 短信登录
async function smsLoginSubmit() {
// 参数校验
const validate = await unref(smsLoginRef)
.validate()
.catch((error) => {
console.log('error: ', error);
});
if (!validate) {
return;
}
if (!props.agreeStatus) {
emits('onConfirm', true)
sheep.$helper.toast('请勾选同意');
return;
}
// 提交数据
const { code } = await AuthUtil.smsLogin(state.model);
if (code === 0) {
closeAuthModal();
}
}
</script>
<style lang="scss" scoped>
@import '../index.scss';
</style>

View File

@@ -0,0 +1,156 @@
@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;
font-weight: 500;
line-height: 36rpx;
}
.head-title-active {
width: 160rpx;
font-size: 32rpx;
font-weight: 600;
color: #999;
line-height: 36rpx;
}
.head-title-animation {
text-align: center;
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;
font-size: 24rpx;
color: #999999;
// text-align: left;
text-align: center;
// 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;
}
}

View File

@@ -0,0 +1,251 @@
<template>
<!-- 规格弹窗 -->
<su-popup :show="authType !== ''" round="10" :showClose="true" @close="closeAuthModal">
<view class="login-wrap main-blue">
<!-- 1. 账号密码登录 accountLogin -->
<account-login
v-if="authType === 'accountLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/>
<!-- 2. 短信登录 smsLogin -->
<!-- <sms-login
v-if="authType === 'smsLogin'"
:agreeStatus="state.protocol"
@onConfirm="onConfirm"
/> -->
<!-- 3. 忘记密码 resetPassword-->
<reset-password v-if="authType === 'resetPassword'" />
<!-- 4. 绑定手机号 changeMobile -->
<change-mobile v-if="authType === 'changeMobile'" />
<!-- 5. 修改密码 changePassword-->
<changePassword v-if="authType === 'changePassword'" />
<!-- 6. 微信小程序授权 -->
<mp-authorization v-if="authType === 'mpAuthorization'" />
<!-- 7. 第三方登录 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="auto-login-box ss-flex ss-flex-col ss-row-center ss-col-center"
>
<!-- 7.1 微信小程序的快捷登录 -->
<view v-if="sheep.$platform.name === 'WechatMiniProgram'" class="ss-flex register-box">
<!-- <view class="register-title">还没有账号?</view> -->
<view class="register-title">已经拥有账号可以,</view>
<button
class="ss-reset-button login-btn"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
快捷登录
</button>
<view class="circle"></view>
</view>
<!-- 7.2 微信的公众号App小程序的登录基于 openid + code -->
<!-- <button
v-if="
['WechatOfficialAccount', 'WechatMiniProgram', 'App'].includes(sheep.$platform.name) &&
sheep.$platform.isWechatInstalled
"
@tap="thirdLogin('wechat')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/wechat.png')"
/>
</button> -->
<!-- 7.3 iOS 登录 TODO 芋艿:等后面搞 App 再弄 -->
<!-- <button
v-if="sheep.$platform.os === 'ios' && sheep.$platform.name === 'App'"
@tap="thirdLogin('apple')"
class="ss-reset-button auto-login-btn"
>
<image
class="auto-login-img"
:src="sheep.$url.static('/static/img/shop/platform/apple.png')"
/>
</button> -->
</view>
<!-- 用户协议的勾选 -->
<view
v-if="['accountLogin', 'smsLogin'].includes(authType)"
class="agreement-box ss-flex ss-row-center"
:class="{ shake: currentProtocol }"
>
<label class="radio ss-flex ss-col-center" @tap="onChange">
<radio
:checked="state.protocol"
color="var(--ui-BG-Main)"
style="transform: scale(0.8)"
@tap.stop="onChange"
/>
<view class="agreement-text ss-flex ss-col-center ss-m-l-8">
我已阅读并同意
<view class="tcp-text" @tap.stop="onProtocol('用户协议')"> 《用户协议》 </view>
<view class="agreement-text">与</view>
<view class="tcp-text" @tap.stop="onProtocol('隐私协议')"> 《隐私协议》 </view>
</view>
</label>
</view>
<view class="safe-box" />
</view>
</su-popup>
</template>
<script setup>
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import accountLogin from './components/account-login.vue';
import smsLogin from './components/sms-login.vue';
import resetPassword from './components/reset-password.vue';
import changeMobile from './components/change-mobile.vue';
import changePassword from './components/change-password.vue';
import mpAuthorization from './components/mp-authorization.vue';
import { closeAuthModal, showAuthModal } from '@/sheep/hooks/useModal';
const modalStore = sheep.$store('modal');
// 授权弹窗类型
const authType = computed(() => modalStore.auth);
// const authType = "accountLogin";
const state = reactive({
protocol: false,
});
const currentProtocol = ref(false);
// 勾选协议
function onChange() {
state.protocol = !state.protocol;
}
// 查看协议
function onProtocol(title) {
closeAuthModal();
sheep.$router.go('/pages/public/richtext', {
title,
});
}
// 点击登录 / 注册事件
function onConfirm(e) {
currentProtocol.value = e;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
}
// 第三方授权登陆微信小程序、Apple
const thirdLogin = async (provider) => {
if (!state.protocol) {
currentProtocol.value = true;
setTimeout(() => {
currentProtocol.value = false;
}, 1000);
sheep.$helper.toast('请勾选同意');
return;
}
const loginRes = await sheep.$platform.useProvider(provider).login();
if (loginRes) {
const userInfo = await sheep.$store('user').getInfo();
closeAuthModal();
// 如果用户已经有头像和昵称,不需要再次授权
if (userInfo.avatar && userInfo.nickname) {
return;
}
// 触发小程序授权信息弹框
// #ifdef MP-WEIXIN
showAuthModal('mpAuthorization');
// #endif
}
};
// 微信小程序的“手机号快速验证”https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
const getPhoneNumber = async (e) => {
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
sheep.$helper.toast('快捷登录失败');
return;
}
console.log("e", e);
let result = await sheep.$platform.useProvider().mobileLogin(e.detail);
console.log("result", result);
if (result) {
closeAuthModal();
}
};
</script>
<style lang="scss" scoped>
@import './index.scss';
.shake {
animation: shake 0.05s linear 4 alternate;
}
@keyframes shake {
from {
transform: translateX(-10rpx);
}
to {
transform: translateX(10rpx);
}
}
.register-box {
position: relative;
justify-content: center;
.register-btn {
color: #999999;
font-size: 30rpx;
font-weight: 500;
}
.register-title {
color: #999999;
font-size: 30rpx;
font-weight: 400;
margin-right: 24rpx;
}
.or-title {
margin: 0 16rpx;
color: #999999;
font-size: 30rpx;
font-weight: 400;
}
.login-btn {
color: var(--ui-BG-Main);
font-size: 30rpx;
font-weight: 500;
}
.circle {
position: absolute;
right: 0rpx;
top: 18rpx;
width: 8rpx;
height: 8rpx;
border-radius: 8rpx;
background: var(--ui-BG-Main);
}
}
.safe-box {
height: calc(constant(safe-area-inset-bottom) / 5 * 3);
height: calc(env(safe-area-inset-bottom) / 5 * 3);
}
.tcp-text {
color: var(--ui-BG-Main);
}
.agreement-text {
color: $dark-9;
}
</style>

View File

@@ -0,0 +1,69 @@
<!-- 顶部导航栏 - 单元格 -->
<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 radius-img" v-if="data.imgUrl == imgSrc" :src="userInfo.avatar?userInfo.avatar:defautAvatar" mode="aspectFit"></image>
<image class="nav-image" v-else :src="sheep.$url.cdn(data.imgUrl)" mode="aspectFit"></image>
</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 imgSrc = 'http://api.jnmall.zq-hightech.com/admin-api/infra/file/29/get/e2f8b02bae129322f99ed06226543a55a8c13226fa688017f1454508a974d7bb.png'
const defautAvatar = 'http://api.jnmall.zq-hightech.com/admin-api/infra/file/29/get/e2f8b02bae129322f99ed06226543a55a8c13226fa688017f1454508a974d7bb.png'
const userInfo = computed(() => sheep.$store('user').userInfo);
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;
height: 65rpx;
width: 65rpx;
border-radius: 50% !important;
}
}
.radius-img {
}
</style>

View File

@@ -0,0 +1,314 @@
<template>
<su-fixed
:noFixed="props.noFixed"
:alway="props.alway"
:bgStyles="props.bgStyles"
:val="0"
:index="props.zIndex"
noNav
:bg="props.bg"
:ui="props.ui"
:opacity="props.opacity"
:placeholder="props.placeholder"
:sticky="props.sticky"
>
<su-status-bar />
<!--
:class="[{ 'border-bottom': !props.opacity && props.bg != 'bg-none' }]"
-->
<view class="ui-navbar-box">
<view
class="ui-bar"
:class="
props.status == '' ? `text-a` : props.status == 'light' ? 'text-white' : 'text-black'
"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<slot name="item"></slot>
<view class="right">
<!-- #ifdef MP -->
<view :style="[state.capsuleStyle]"></view>
<!-- #endif -->
</view>
</view>
</view>
</su-fixed>
</template>
<script setup>
/**
* 标题栏 - 基础组件navbar
*
* @param {Number} zIndex = 100 - 层级
* @param {Boolean} back = true - 是否返回上一页
* @param {String} backtext = '' - 返回文本
* @param {String} bg = 'bg-white' - 公共Class
* @param {String} status = '' - 状态栏颜色
* @param {Boolean} alway = true - 是否常驻
* @param {Boolean} opacity = false - 是否开启透明渐变
* @param {Boolean} opacityBg = false - 开启滑动渐变后,返回按钮是否添加背景
* @param {Boolean} noFixed = false - 是否浮动
* @param {String} ui = '' - 公共Class
* @param {Boolean} capsule = false - 是否开启胶囊返回
* @param {Boolean} stopBack = false - 是否禁用返回
* @param {Boolean} placeholder = true - 是否开启占位
* @param {Object} bgStyles = {} - 背景样式
*
*/
import { computed, reactive, onBeforeMount } from 'vue';
import sheep from '@/sheep';
// 本地数据
const state = reactive({
statusCur: '',
capsuleStyle: {},
capsuleBack: {},
});
const sys_statusBar = sheep.$platform.device.statusBarHeight;
const sys_navBar = sheep.$platform.navbar;
const props = defineProps({
sticky: Boolean,
zIndex: {
type: Number,
default: 100,
},
back: {
//是否返回上一页
type: Boolean,
default: true,
},
backtext: {
//返回文本
type: String,
default: '',
},
bg: {
type: String,
default: 'bg-white',
},
status: {
//状态栏颜色 可以选择light dark/其他字符串视为黑色
type: String,
default: '',
},
// 常驻
alway: {
type: Boolean,
default: true,
},
opacity: {
//是否开启滑动渐变
type: Boolean,
default: false,
},
opacityBg: {
//开启滑动渐变后 返回按钮是否添加背景
type: Boolean,
default: false,
},
noFixed: {
//是否浮动
type: Boolean,
default: false,
},
ui: {
type: String,
default: '',
},
capsule: {
//是否开启胶囊返回
type: Boolean,
default: false,
},
stopBack: {
type: Boolean,
default: false,
},
placeholder: {
type: [Boolean],
default: true,
},
bgStyles: {
type: Object,
default() {},
},
});
const emits = defineEmits(['navback']);
onBeforeMount(() => {
init();
});
// 返回
const onNavback = () => {
sheep.$router.back();
};
// 初始化
const init = () => {
state.capsuleStyle = {
width: sheep.$platform.capsule.width + 'px',
height: sheep.$platform.capsule.height + 'px',
margin: '0 ' + (sheep.$platform.device.windowWidth - sheep.$platform.capsule.right) + 'px',
};
state.capsuleBack = state.capsuleStyle;
};
</script>
<style lang="scss" scoped>
.ui-navbar-box {
background-color: transparent;
width: 100%;
.ui-bar {
position: relative;
z-index: 2;
white-space: nowrap;
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
.left {
@include flex-bar;
.back {
@include flex-bar;
.back-icon {
@include flex-center;
width: 56rpx;
height: 56rpx;
margin: 0 10rpx;
font-size: 46rpx !important;
&.opacityIcon {
position: relative;
border-radius: 50%;
background-color: rgba(127, 127, 127, 0.5);
&::after {
content: '';
display: block;
position: absolute;
height: 200%;
width: 200%;
left: 0;
top: 0;
border-radius: inherit;
transform: scale(0.5);
transform-origin: 0 0;
opacity: 0.1;
border: 1px solid currentColor;
pointer-events: none;
}
&::before {
transform: scale(0.9);
}
}
}
/* #ifdef MP-ALIPAY */
._icon-back {
opacity: 0;
}
/* #endif */
}
.capsule {
@include flex-bar;
border-radius: 100px;
position: relative;
&.dark {
background-color: rgba(255, 255, 255, 0.5);
}
&.light {
background-color: rgba(0, 0, 0, 0.15);
}
&::after {
content: '';
display: block;
position: absolute;
height: 60%;
width: 1px;
left: 50%;
top: 20%;
background-color: currentColor;
opacity: 0.1;
pointer-events: none;
}
&::before {
content: '';
display: block;
position: absolute;
height: 200%;
width: 200%;
left: 0;
top: 0;
border-radius: inherit;
transform: scale(0.5);
transform-origin: 0 0;
opacity: 0.1;
border: 1px solid currentColor;
pointer-events: none;
}
.capsule-back,
.capsule-home {
@include flex-center;
flex: 1;
}
&.isFristPage {
.capsule-back,
&::after {
display: none;
}
}
}
}
.right {
@include flex-bar;
.right-content {
@include flex;
flex-direction: row-reverse;
}
}
.center {
@include flex-center;
text-overflow: ellipsis;
text-align: center;
flex: 1;
.image {
display: block;
height: 36px;
max-width: calc(100vw - 200px);
}
}
}
.ui-bar-bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 1;
pointer-events: none;
}
}
</style>

View File

@@ -0,0 +1,207 @@
<!-- 顶部导航栏 -->
<template>
<navbar
:alway="isAlways"
:back="false"
bg=""
:placeholder="isPlaceholder"
:bgStyles="bgStyles"
:opacity="isOpacity"
:sticky="sticky"
>
<template #item>
<view class="nav-box">
<view class="nav-icon" v-if="showLeftButton">
<view class="icon-box ss-flex" :class="{ 'inner-icon-box': data.styleType === 'inner' }">
<view class="icon-button icon-button-left ss-flex ss-row-center" @tap="onClickLeft">
<text class="sicon-back" v-if="hasHistory" />
<text class="sicon-home" v-else />
</view>
<view class="line"></view>
<view class="icon-button icon-button-right ss-flex ss-row-center" @tap="onClickRight">
<text class="sicon-more" />
</view>
</view>
</view>
<view
class="nav-item"
v-for="(item, index) in navList"
:key="index"
:style="[parseImgStyle(item)]"
:class="[{ 'ss-flex ss-col-center ss-row-center': item.type !== 'search' }]"
>
<navbar-item :data="item" :width="parseImgStyle(item).width" />
</view>
</view>
</template>
</navbar>
</template>
<script setup>
/**
* 装修组件 - 自定义标题栏
*
*
* @property {Number | String} alwaysShow = [0,1] - 是否常驻
* @property {Number | String} styleType = [inner] - 是否沉浸式
* @property {String | Number} type - 标题背景模式
* @property {String} color - 页面背景色
* @property {String} src - 页面背景图片
*/
import { computed, unref } from 'vue';
import sheep from '@/sheep';
import Navbar from './components/navbar.vue';
import NavbarItem from './components/navbar-item.vue';
import { showMenuTools } from '@/sheep/hooks/useModal';
const props = defineProps({
data: {
type: Object,
default: () => ({}),
},
showLeftButton: {
type: Boolean,
default: false,
},
});
const hasHistory = sheep.$router.hasHistory();
const sticky = computed(() => {
if (props.data.styleType === 'inner') {
if (props.data.alwaysShow) {
return false;
}
}
if (props.data.styleType === 'normal') {
return false;
}
});
const navList = computed(() => {
// #ifdef MP
return props.data.mpCells || [];
// #endif
return props.data.otherCells || [];
});
// 页面宽度
const windowWidth = sheep.$platform.device.windowWidth;
// 单元格宽度
const cell = computed(() => {
if (unref(navList).length) {
// 默认宽度为8个格子微信公众号右上角有胶囊按钮所以是6个格子
let cell = (windowWidth - 90) / 8;
// #ifdef MP
cell = (windowWidth - 80 - unref(sheep.$platform.capsule).width) / 6;
// #endif
return cell;
}
});
// 解析位置
const parseImgStyle = (item) => {
let obj = {
width: item.width * cell.value + (item.width - 1) * 10 + 'px',
left: item.left * cell.value + (item.left + 1) * 10 + 'px',
'border-radius': item.borderRadius + 'px',
};
return obj;
};
const isAlways = computed(() =>
props.data.styleType === 'inner' ? Boolean(props.data.alwaysShow) : true,
);
const isOpacity = computed(() =>
props.data.styleType === 'normal'
? false
: props.showLeftButton
? false
: props.data.styleType === 'inner',
);
const isPlaceholder = computed(() => props.data.styleType === 'normal');
const bgStyles = computed(() => {
return {
background:
props.data.bgType === 'img' && props.data.bgImg
? `url(${sheep.$url.cdn(props.data.bgImg)}) no-repeat top center / 100% 100%`
: props.data.bgColor,
};
});
// 左侧按钮:返回上一页或首页
function onClickLeft() {
if (hasHistory) {
sheep.$router.back();
} else {
sheep.$router.go('/pages/index/index');
}
}
// 右侧按钮:打开快捷菜单
function onClickRight() {
showMenuTools();
}
</script>
<style lang="scss" scoped>
.nav-box {
width: 750rpx;
position: relative;
height: 100%;
.nav-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.nav-icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 20rpx;
.inner-icon-box {
border: 1px solid rgba(#fff, 0.4);
background: none !important;
}
.icon-box {
background: #ffffff;
box-shadow: 0px 0px 4rpx rgba(51, 51, 51, 0.08), 0px 4rpx 6rpx 2rpx rgba(102, 102, 102, 0.12);
border-radius: 30rpx;
width: 134rpx;
height: 56rpx;
margin-left: 8rpx;
.line {
width: 2rpx;
height: 24rpx;
background: #e5e5e7;
}
.sicon-back {
font-size: 32rpx;
}
.sicon-home {
font-size: 32rpx;
}
.sicon-more {
font-size: 32rpx;
}
.icon-button {
width: 67rpx;
height: 56rpx;
&-left:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 30rpx 0px 0px 30rpx;
}
&-right:hover {
background: rgba(0, 0, 0, 0.16);
border-radius: 0px 30rpx 30rpx 0px;
}
}
}
}
}
</style>

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('buttonColor');
border-radius: 35rpx;
font-weight: 500;
color: v-bind('buttonColor');
font-size: 28rpx;
}
</style>

View File

@@ -0,0 +1,250 @@
<template>
<view
class="page-app"
:class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]"
>
<view class="page-main" :style="[bgMain]">
<!-- 顶部导航栏-情况1默认通用顶部导航栏 -->
<su-navbar
v-if="navbar === 'normal'"
:title="title"
statusBar
:color="color"
:tools="tools"
:opacityBgUi="opacityBgUi"
@search="(e) => emits('search', e)"
:defaultSearch="defaultSearch"
/>
<!-- 顶部导航栏-情况2装修组件导航栏-标准 -->
<s-custom-navbar
v-else-if="navbar === 'custom' && navbarMode === 'normal'"
:data="navbarStyle"
:showLeftButton="showLeftButton"
/>
<view class="page-body" :style="[bgBody]">
<!-- 顶部导航栏-情况3沉浸式头部 -->
<su-inner-navbar v-if="navbar === 'inner'" :title="title" />
<view
v-if="navbar === 'inner'"
:style="[{ paddingTop: sheep.$platform.navbar + 'px' }]"
></view>
<!-- 顶部导航栏-情况4装修组件导航栏-沉浸式 -->
<s-custom-navbar
v-if="navbar === 'custom' && navbarMode === 'inner'"
:data="navbarStyle"
:showLeftButton="showLeftButton"
/>
<!-- 页面内容插槽 -->
<slot />
<!-- 底部导航 -->
<s-tabbar v-if="tabbar !== ''" :path="tabbar" />
</view>
</view>
<view class="page-modal">
<!-- 全局授权弹窗 -->
<s-auth-modal />
<!-- 全局分享弹窗 -->
<s-share-modal :shareInfo="shareInfo" />
<!-- 全局快捷入口 -->
<s-menu-tools />
</view>
</view>
</template>
<script setup>
/**
* 模板组件 - 提供页面公共组件,属性,方法
*/
import { computed, reactive, ref } from 'vue';
import sheep from '@/sheep';
import { isEmpty } from 'lodash-es';
import { onShow } from '@dcloudio/uni-app';
// #ifdef MP-WEIXIN
import { onShareAppMessage } from '@dcloudio/uni-app';
// #endif
const props = defineProps({
title: {
type: String,
default: '',
},
navbar: {
type: String,
default: 'normal',
},
opacityBgUi: {
type: String,
default: 'bg-white',
},
color: {
type: String,
default: '',
},
tools: {
type: String,
default: 'title',
},
keyword: {
type: String,
default: '',
},
navbarStyle: {
type: Object,
default: () => ({
styleType: '',
type: '',
color: '',
src: '',
list: [],
alwaysShow: 0,
}),
},
bgStyle: {
type: Object,
default: () => ({
src: '',
color: 'var(--ui-BG-1)',
}),
},
tabbar: {
type: [String, Boolean],
default: '',
},
onShareAppMessage: {
type: [Boolean, Object],
default: true,
},
leftWidth: {
type: [Number, String],
default: 100,
},
rightWidth: {
type: [Number, String],
default: 100,
},
defaultSearch: {
type: String,
default: '',
},
//展示返回按钮
showLeftButton: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(['search']);
const sysStore = sheep.$store('sys');
const userStore = sheep.$store('user');
const appStore = sheep.$store('app');
const modalStore = sheep.$store('modal');
const sys = computed(() => sysStore);
// 导航栏模式(因为有自定义导航栏 需要计算)
const navbarMode = computed(() => {
if (props.navbar === 'normal' || props.navbarStyle.styleType === 'normal') {
return 'normal';
}
return 'inner';
});
// 背景1
const bgMain = computed(() => {
if (navbarMode.value === 'inner') {
return {
background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
props.bgStyle.backgroundImage,
)}) no-repeat top center / 100% auto`,
};
}
return {};
});
// 背景2
const bgBody = computed(() => {
if (navbarMode.value === 'normal') {
// return {
// background: `${props.bgStyle.backgroundColor} url(${sheep.$url.cdn(
// props.bgStyle.backgroundImage,
// )}) no-repeat top center / 100% auto`,
// };
return {
background: `linear-gradient(to bottom, ${props.bgStyle.backgroundColor} 20%, #fafafa 50%)`,
// background: `linear-gradient( 180deg, #00B85B 0%, rgba(2,189,94,0.74) 66%, #07CC68 100%)`,
};
}
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>

View File

@@ -0,0 +1,118 @@
<!-- 全局 - 快捷入口 -->
<template>
<su-popup :show="show" type="top" round="20" backgroundColor="#F0F0F0" @close="closeMenuTools">
<su-status-bar />
<view class="tools-wrap ss-m-x-30 ss-m-b-16">
<view class="title ss-m-b-34 ss-p-t-20">快捷菜单</view>
<view class="container-list ss-flex ss-flex-wrap">
<view class="list-item ss-m-b-24" v-for="item in list" :key="item.title">
<view class="ss-flex-col ss-col-center">
<button
class="ss-reset-button list-image ss-flex ss-row-center ss-col-center"
@tap="onClick(item)"
>
<image v-if="show" :src="sheep.$url.static(item.icon)" class="list-icon" />
</button>
<view class="list-title ss-m-t-20">{{ item.title }}</view>
</view>
</view>
</view>
</view>
</su-popup>
</template>
<script setup>
import { reactive, computed } from 'vue';
import sheep from '@/sheep';
import { showMenuTools, closeMenuTools } from '@/sheep/hooks/useModal';
const show = computed(() => sheep.$store('modal').menu);
function onClick(item) {
closeMenuTools();
if (item.url) sheep.$router.go(item.url);
}
const list = [
{
url: '/pages/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/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>

View File

@@ -0,0 +1,168 @@
<!-- 海报弹窗 -->
<template>
<su-popup :show="show" round="10" @close="onClosePoster" type="center" class="popup-box">
<view class="ss-flex-col ss-col-center ss-row-center">
<image
v-if="!!painterImageUrl"
class="poster-img"
:src="painterImageUrl"
:style="{
height: poster.css.height+ 'px',
width: poster.css.width + 'px',
}"
:show-menu-by-longpress="true"
/>
</view>
<view
class="poster-btn-box ss-m-t-20 ss-flex ss-row-between ss-col-center"
v-if="!!painterImageUrl"
>
<button class="cancel-btn ss-reset-button" @tap="onClosePoster">取消</button>
<button class="save-btn ss-reset-button ui-BG-Main" @tap="onSavePoster">
{{
['wechatOfficialAccount', 'H5'].includes(sheep.$platform.name)
? '长按图片保存'
: '保存图片'
}}
</button>
</view>
<!-- 海报画板默认隐藏只用来生成海报生成方式为主动调用 -->
<l-painter
isCanvasToTempFilePath
pathType="url"
@success="setPainterImageUrl"
hidden
ref="painterRef"
/>
</su-popup>
</template>
<script setup>
/**
* 海报生成和展示
* 提示:小程序码默认跳转首页,由首页进行 spm 参数解析后跳转到对应的分享页面
* @description 用于生成分享海报,如:分享商品海报。
* @tutorial https://ext.dcloud.net.cn/plugin?id=2389
* @property {Boolean} show 弹出层控制
* @property {Object} shareInfo 分享信息
*/
import { reactive, ref, unref } from 'vue';
import sheep from '@/sheep';
import { getPosterData } from '@/sheep/components/s-share-modal/canvas-poster/poster';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
shareInfo: {
type: Object,
default: () => {
},
},
});
const poster = reactive({
css: {
// 根节点若无尺寸,自动获取父级节点
width: sheep.$platform.device.windowWidth * 0.9,
height: 600,
},
views: [],
});
const emits = defineEmits(['success', 'close']);
const onClosePoster = () => {
emits('close');
};
const painterRef = ref(); // 海报画板
const painterImageUrl = ref(); // 海报 url
// 渲染海报
const renderPoster = async () => {
await painterRef.value.render(unref(poster));
};
// 获得生成的图片
const setPainterImageUrl = (path) => {
painterImageUrl.value = path;
};
// 保存海报图片
const onSavePoster = () => {
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
sheep.$helper.toast('请长按图片保存');
return;
}
// 非H5 保存到相册
uni.saveImageToPhotosAlbum({
filePath: painterImageUrl.value,
success: (res) => {
onClosePoster();
sheep.$helper.toast('保存成功');
},
fail: (err) => {
sheep.$helper.toast('保存失败');
console.log('图片保存失败:', err);
},
});
};
// 获得海报数据
async function getPoster() {
painterImageUrl.value = undefined
poster.views = await getPosterData({
width: poster.css.width,
shareInfo: props.shareInfo,
});
await renderPoster();
}
defineExpose({
getPoster,
});
</script>
<style lang="scss" scoped>
.popup-box {
position: relative;
}
.poster-title {
color: #999;
}
// 分享海报
.poster-btn-box {
width: 600rpx;
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: -80rpx;
.cancel-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
background: $white;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
color: $dark-9;
}
.save-btn {
width: 240rpx;
height: 70rpx;
line-height: 70rpx;
border-radius: 35rpx;
font-size: 28rpx;
font-weight: 500;
}
}
.poster-img {
border-radius: 20rpx;
}
</style>

View File

@@ -0,0 +1,125 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const goods = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.goods_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 16,
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.06,
left: width * 0.22,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.04,
top: width * 0.04,
width: width * 0.14,
height: width * 0.14,
},
},
{
type: 'image',
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
css: {
position: 'fixed',
left: width * 0.03,
top: width * 0.21,
width: width * 0.94,
height: width * 0.94,
},
},
{
type: 'text',
text: poster.shareInfo.poster.title,
css: {
position: 'fixed',
left: width * 0.04,
top: width * 1.18,
color: '#333',
fontSize: 14,
lineHeight: 15,
maxWidth: width * 0.91,
},
},
{
type: 'text',
text: '¥' + poster.shareInfo.poster.price,
css: {
position: 'fixed',
left: width * 0.04,
top: width * 1.31,
fontSize: 20,
fontFamily: 'OPPOSANS',
color: '#333',
},
},
{
type: 'text',
text:
poster.shareInfo.poster.original_price > 0
? '¥' + poster.shareInfo.poster.original_price
: '',
css: {
position: 'fixed',
left: width * 0.3,
top: width * 1.33,
color: '#999',
fontSize: 10,
fontFamily: 'OPPOSANS',
textDecoration: 'line-through',
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
];
};
export default goods;

View File

@@ -0,0 +1,122 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const groupon = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.groupon_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 16,
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.06,
left: width * 0.22,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.04,
top: width * 0.04,
width: width * 0.14,
height: width * 0.14,
},
},
{
type: 'image',
src: formatImageUrlProtocol(poster.shareInfo.poster.image),
css: {
position: 'fixed',
left: width * 0.03,
top: width * 0.21,
width: width * 0.94,
height: width * 0.94,
borderRadius: 10,
},
},
{
type: 'text',
text: poster.shareInfo.poster.title,
css: {
color: '#333',
fontSize: 14,
position: 'fixed',
top: width * 1.18,
left: width * 0.04,
maxWidth: width * 0.91,
lineHeight: 5,
},
},
{
type: 'text',
text: '¥' + poster.shareInfo.poster.price,
css: {
color: '#ff0000',
fontSize: 20,
fontFamily: 'OPPOSANS',
position: 'fixed',
top: width * 1.3,
left: width * 0.04,
},
},
{
type: 'text',
text: '2人团',
css: {
color: '#fff',
fontSize: 12,
fontFamily: 'OPPOSANS',
position: 'fixed',
left: width * 0.84,
top: width * 1.3,
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.5,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.75,
top: width * 1.3,
width: width * 0.2,
height: width * 0.2,
},
},
// #endif
];
};
export default groupon;

View File

@@ -0,0 +1,39 @@
import user from './user';
import goods from './goods';
import groupon from './groupon';
import SocialApi from '@/sheep/api/member/social';
export function getPosterData(options) {
switch (options.shareInfo.poster.type) {
case 'user':
return user(options);
case 'goods':
return goods(options);
case 'groupon':
return groupon(options);
}
}
export function formatImageUrlProtocol(url) {
// #ifdef H5
// H5平台 https协议下需要转换
if (window.location.protocol === 'https:' && url.indexOf('http:') === 0) {
url = url.replace('http:', 'https:');
}
// #endif
// #ifdef MP-WEIXIN
// 小程序平台 需要强制转换为https协议
if (url.indexOf('http:') === 0) {
url = url.replace('http:', 'https:');
}
// #endif
return url;
}
// 获得微信小程序码 Base64 image
export async function getWxaQrcode(path, query) {
const res = await SocialApi.getWxaQrcode(path, query);
return 'data:image/png;base64,' + res.data;
}

View File

@@ -0,0 +1,74 @@
import sheep from '@/sheep';
import { formatImageUrlProtocol, getWxaQrcode } from './index';
const user = async (poster) => {
const width = poster.width;
const userInfo = sheep.$store('user').userInfo;
const wxa_qrcode = await getWxaQrcode(poster.shareInfo.path, poster.shareInfo.query);
return [
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(sheep.$store('app').platform.share.posterInfo.user_bg)),
css: {
width,
position: 'fixed',
'object-fit': 'contain',
top: '0',
left: '0',
zIndex: -1,
},
},
{
type: 'text',
text: userInfo.nickname,
css: {
color: '#333',
fontSize: 14,
textAlign: 'center',
fontFamily: 'sans-serif',
position: 'fixed',
top: width * 0.4,
left: width / 2,
},
},
{
type: 'image',
src: formatImageUrlProtocol(sheep.$url.cdn(userInfo.avatar)),
css: {
position: 'fixed',
left: width * 0.4,
top: width * 0.16,
width: width * 0.2,
height: width * 0.2,
},
},
// #ifndef MP-WEIXIN
{
type: 'qrcode',
text: poster.shareInfo.link,
css: {
position: 'fixed',
left: width * 0.35,
top: width * 0.84,
width: width * 0.3,
height: width * 0.3,
},
},
// #endif
// #ifdef MP-WEIXIN
{
type: 'image',
src: wxa_qrcode,
css: {
position: 'fixed',
left: width * 0.35,
top: width * 0.84,
width: width * 0.3,
height: width * 0.3,
},
},
// #endif
];
};
export default user;

View File

@@ -0,0 +1,196 @@
<!-- 全局分享弹框 -->
<template>
<view>
<su-popup :show="state.showShareGuide" :showClose="false" @close="onCloseGuide" />
<view v-if="state.showShareGuide" class="guide-wrap">
<image class="guide-image" :src="sheep.$url.static('/static/img/shop/share/share_guide.png')" />
</view>
<su-popup :show="show" round="10" :showClose="false" @close="closeShareModal">
<!-- 分享 tools -->
<view class="share-box">
<view class="share-list-box ss-flex">
<!-- 操作 发送给微信好友 -->
<button
v-if="shareConfig.methods.includes('forward')"
class="share-item share-btn ss-flex-col ss-col-center"
open-type="share"
@tap="onShareByForward"
>
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_wx.png')" mode="" />
<text class="share-title">微信好友</text>
</button>
<!-- 操作 生成海报图片 -->
<button
v-if="shareConfig.methods.includes('poster')"
class="share-item share-btn ss-flex-col ss-col-center"
@tap="onShareByPoster"
>
<image
class="share-img"
:src="sheep.$url.static('/static/img/shop/share/share_poster.png')"
mode=""
/>
<text class="share-title">生成海报</text>
</button>
<!-- 操作 生成链接 -->
<button
v-if="shareConfig.methods.includes('link')"
class="share-item share-btn ss-flex-col ss-col-center"
@tap="onShareByCopyLink"
>
<image class="share-img" :src="sheep.$url.static('/static/img/shop/share/share_link.png')" mode="" />
<text class="share-title">复制链接</text>
</button>
</view>
<view class="share-foot ss-flex ss-row-center ss-col-center" @tap="closeShareModal">
取消
</view>
</view>
</su-popup>
<!-- 分享海报对应操作 -->
<canvas-poster
ref="SharePosterRef"
:show="state.showPosterModal"
:shareInfo="shareInfo"
@close="state.showPosterModal = false"
/>
</view>
</template>
<script setup>
/**
* 分享弹窗
*/
import { ref, unref, reactive, computed } from 'vue';
import sheep from '@/sheep';
import canvasPoster from './canvas-poster/index.vue';
import { closeShareModal, showAuthModal } from '@/sheep/hooks/useModal';
const show = computed(() => sheep.$store('modal').share);
const shareConfig = computed(() => sheep.$store('app').platform.share);
const SharePosterRef = ref('');
const props = defineProps({
shareInfo: {
type: Object,
default() {},
},
});
const state = reactive({
showShareGuide: false, // H5 的指引
showPosterModal: false, // 海报弹窗
});
// 操作 ②:生成海报分享
const onShareByPoster = () => {
closeShareModal();
if (!sheep.$store('user').isLogin) {
showAuthModal();
return;
}
console.log(props.shareInfo);
unref(SharePosterRef).getPoster();
state.showPosterModal = true;
};
// 操作 ①:直接转发分享
const onShareByForward = () => {
closeShareModal();
// #ifdef H5
if (['WechatOfficialAccount', 'H5'].includes(sheep.$platform.name)) {
state.showShareGuide = true;
return;
}
// #endif
// #ifdef APP-PLUS
uni.share({
provider: 'weixin',
scene: 'WXSceneSession',
type: 0,
href: props.shareInfo.link,
title: props.shareInfo.title,
summary: props.shareInfo.desc,
imageUrl: props.shareInfo.image,
success: (res) => {
console.log('success:' + JSON.stringify(res));
},
fail: (err) => {
console.log('fail:' + JSON.stringify(err));
},
});
// #endif
};
// 操作 ③:复制链接分享
const onShareByCopyLink = () => {
sheep.$helper.copyText(props.shareInfo.link);
closeShareModal();
};
function onCloseGuide() {
state.showShareGuide = false;
}
</script>
<style lang="scss" scoped>
.guide-image {
right: 30rpx;
top: 0;
position: fixed;
width: 580rpx;
height: 430rpx;
z-index: 10080;
}
// 分享tool
.share-box {
background: $white;
width: 750rpx;
border-radius: 30rpx 30rpx 0 0;
padding-top: 30rpx;
.share-foot {
font-size: 24rpx;
color: $gray-b;
height: 80rpx;
border-top: 1rpx solid $gray-e;
}
.share-list-box {
.share-btn {
background: none;
border: none;
line-height: 1;
padding: 0;
&::after {
border: none;
}
}
.share-item {
flex: 1;
padding-bottom: 20rpx;
.share-img {
width: 70rpx;
height: 70rpx;
background: $gray-f;
border-radius: 50%;
margin-bottom: 20rpx;
}
.share-title {
font-size: 24rpx;
color: $dark-6;
}
}
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<view class="u-page__item" v-if="tabbar?.items?.length > 0">
<su-tabbar
:value="path"
:fixed="true"
:placeholder="true"
:safeAreaInsetBottom="true"
:inactiveColor="tabbar.style.color"
:activeColor="tabbar.style.activeColor"
:midTabBar="tabbar.mode === 2"
:customStyle="tabbarStyle"
>
<su-tabbar-item
v-for="(item, index) in tabbar.items"
:key="item.text"
:text="item.text"
:name="item.url"
:isCenter="getTabbarCenter(index)"
:centerImage="sheep.$url.cdn(item.iconUrl)"
@tap="sheep.$router.go(item.url)"
>
<template v-slot:active-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.activeIconUrl)"></image>
</template>
<template v-slot:inactive-icon>
<image class="u-page__item__slot-icon" :src="sheep.$url.cdn(item.iconUrl)"></image>
</template>
</su-tabbar-item>
</su-tabbar>
</view>
</template>
<script setup>
import { computed, unref } from 'vue';
import sheep from '@/sheep';
const tabbar = computed(() => {
return sheep.$store('app').template.basic?.tabbar;
});
const tabbarStyle = computed(() => {
const backgroundStyle = tabbar.value.style;
if (backgroundStyle.bgType === 'color') {
return { background: backgroundStyle.bgColor };
}
if (backgroundStyle.bgType === 'img')
return {
background: `url(${sheep.$url.cdn(
backgroundStyle.bgImg,
)}) no-repeat top center / 100% auto`,
};
});
const getTabbarCenter = (index) => {
if (unref(tabbar).mode !== 2) return false;
return unref(tabbar).items % 2 > 0
? Math.ceil(unref(tabbar).items.length / 2) === index + 1
: false;
};
const props = defineProps({
path: String,
default: '',
});
</script>
<style lang="scss">
.u-page {
padding: 0;
&__item {
&__title {
color: var(--textSize);
background-color: #fff;
padding: 15px;
font-size: 15px;
&__slot-title {
color: var(--textSize);
font-size: 14px;
}
}
&__slot-icon {
width: 25px;
height: 25px;
}
}
}
</style>

23
sheep/config/index.js Normal file
View File

@@ -0,0 +1,23 @@
// 开发环境配置
export let baseUrl;
export let version;
if (process.env.NODE_ENV === 'development') {
baseUrl = import.meta.env.SHOPRO_DEV_BASE_URL;
} else {
baseUrl = import.meta.env.SHOPRO_BASE_URL;
}
version = import.meta.env.SHOPRO_VERSION;
console.log(`[芋道商城 ${version}] http://doc.iocoder.cn`);
export const apiPath = import.meta.env.SHOPRO_API_PATH;
export const staticUrl = import.meta.env.SHOPRO_STATIC_URL;
export const tenantId = import.meta.env.SHOPRO_TENANT_ID;
export const websocketPath = import.meta.env.SHOPRO_WEBSOCKET_PATH;
export default {
baseUrl,
apiPath,
staticUrl,
tenantId,
websocketPath,
};

20
sheep/config/zIndex.js Normal file
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
sheep/helper/digit.js Normal file
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
sheep/helper/index.js Normal file
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 添加单位如果有rpxupx%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 + '/';
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
sheep/helper/test.js Normal file
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
sheep/helper/throttle.js Normal file
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
sheep/helper/tools.js Normal file
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('');
},
};

172
sheep/helper/utils.js Normal file
View File

@@ -0,0 +1,172 @@
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 (value === '' || value === undefined || value === null){
return true;
}
if (isArray(value)) {
return value.length === 0;
}
if (isObject(value)) {
return Object.keys(value).length === 0;
}
return false
}
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)));
}

0
sheep/hooks/useApp.js Normal file
View File

522
sheep/hooks/useGoods.js Normal file
View File

@@ -0,0 +1,522 @@
import { ref } from 'vue';
import dayjs from 'dayjs';
import $url from '@/sheep/url';
import { formatDate } from '@/sheep/util';
/**
* 格式化销量
* @param {'exact' | string} type 格式类型exact=精确值,其它=大致数量
* @param {number} num 销量
* @return {string} 格式化后的销量字符串
*/
export function formatSales(type, num) {
let prefix = type !== 'exact' && num < 10 ? '销量:' : '已售:';
return formatNum(prefix, type, num);
}
/**
* 格式化兑换量
* @param {'exact' | string} type 格式类型exact=精确值,其它=大致数量
* @param {number} num 销量
* @return {string} 格式化后的销量字符串
*/
export function formatExchange(type, num) {
return formatNum('已兑换', type, num);
}
/**
* 格式化库存
* @param {'exact' | any} type 格式类型exact=精确值,其它=大致数量
* @param {number} num 销量
* @return {string} 格式化后的销量字符串
*/
export function formatStock(type, num) {
return formatNum('库存', type, num);
}
/**
* 格式化数字
* @param {string} prefix 前缀
* @param {'exact' | string} type 格式类型exact=精确值,其它=大致数量
* @param {number} num 销量
* @return {string} 格式化后的销量字符串
*/
export function formatNum(prefix, type, num) {
num = num || 0;
// 情况一:精确数值
if (type === 'exact') {
return prefix + num;
}
// 情况二:小于等于 10
if (num < 10) {
return `${prefix}≤10`;
}
// 情况三:大于 10除第一位外其它位都显示为0
// 例如100 - 199 显示为 100+
// 9000 - 9999 显示为 9000+
const numStr = num.toString();
const first = numStr[0];
const other = '0'.repeat(numStr.length - 1);
return `${prefix}${first}${other}+`;
}
// 格式化价格
export function formatPrice(e) {
return e.length === 1 ? e[0] : e.join('~');
}
// 视频格式后缀列表
const VIDEO_SUFFIX_LIST = ['.avi', '.mp4'];
/**
* 转换商品轮播的链接列表:根据链接的后缀,判断是视频链接还是图片链接
*
* @param {string[]} urlList 链接列表
* @return {{src: string, type: 'video' | 'image' }[]} 转换后的链接列表
*/
export function formatGoodsSwiper(urlList) {
return (
urlList
?.filter((url) => url)
.map((url, key) => {
const isVideo = VIDEO_SUFFIX_LIST.some((suffix) => url.includes(suffix));
const type = isVideo ? 'video' : 'image';
const src = $url.cdn(url);
return {
type,
src,
};
}) || []
);
}
/**
* 格式化订单状态的颜色
*
* @param order 订单
* @return {string} 颜色的 class 名称
*/
export function formatOrderColor(order) {
if (order.status === 0) {
return 'info-color';
}
// if (order.status === 10 || order.status === 20 || (order.status === 30 && !order.commentStatus)) {
// return 'warning-color';
// }
// if (order.status === 30 && order.commentStatus) {
// return 'success-color';
// }
if (order.status === 10 || order.status === 20) {
return 'warning-color';
}
if (order.status === 30) {
return 'success-color';
}
return 'danger-color';
}
/**
* 格式化订单状态
*
* @param order 订单
*/
export function formatOrderStatus(order) {
if (order.status === 0) {
return '待付款';
}
if (order.status === 10 && (order.deliveryType === 1 || order.deliveryType === 3)) {
return '待发货';
}
if (order.status === 11 && (order.deliveryType === 1 || order.deliveryType === 3)) {
return '部分发货';
}
if (order.status === 10 && order.deliveryType === 2) {
return '待核销';
}
if (order.status === 20) {
return '待收货';
}
// if (order.status === 30 && !order.commentStatus) {
// return '待评价';
// }
// if (order.status === 30 && order.commentStatus) {
// return '已完成';
// }
if (order.status === 30) {
return '已完成';
}
if (order.status === 35) {
return '取消中';
}
return '已取消';
}
/**
* 格式化订单状态的描述
*
* @param order 订单
*/
export function formatOrderStatusDescription(order) {
if (order.status === 0) {
return `请在 ${formatDate(order.payExpireTime)} 前完成支付`;
}
if (order.status === 10) {
return '商家未发货,请耐心等待';
}
if (order.status === 20) {
return '商家已发货,请耐心等待';
}
if (order.status === 30 && !order.commentStatus) {
return '已收货,快去评价一下吧';
}
if (order.status === 30 && order.commentStatus) {
return '交易完成,感谢您的支持';
}
return '交易关闭';
}
/**
* 处理订单的 button 操作按钮数组
*
* @param order 订单
*/
export function handleOrderButtons(order) {
order.buttons = [];
if (order.type === 3) {
// 查看拼团
order.buttons.push('combination');
}
if (order.status === 10 || order.status === 11 || order.status === 20) {
// 取消订单,若该订单状态为"待支付"、"待发货"、"部分发货"、"待收货",可取消订单
order.buttons.push('cancel');
}
if (order.status === 20) {
// 确认收货
order.buttons.push('confirm');
}
if (order.logisticsId > 0) {
// 查看物流
order.buttons.push('express');
}
if (order.status === 0) {
// 取消订单 / 发起支付
order.buttons.push('cancel');
order.buttons.push('pay');
}
if (order.status === 30 && !order.commentStatus) {
// 发起评价
order.buttons.push('comment');
}
if (order.status === 40) {
// 删除订单
order.buttons.push('delete');
}
if (order.status === 35) {
// 撤回取消
order.buttons.push('withdraw');
}
}
/**
* 格式化售后状态
*
* @param afterSale 售后
*/
export function formatAfterSaleStatus(afterSale) {
if (afterSale.status === 10) {
return '申请售后';
}
if (afterSale.status === 20) {
return '商品待退货';
}
if (afterSale.status === 30) {
return '商家待收货';
}
if (afterSale.status === 40) {
return '等待退款';
}
if (afterSale.status === 50) {
return '退款成功';
}
if (afterSale.status === 61) {
return '买家取消';
}
if (afterSale.status === 62) {
return '商家拒绝';
}
if (afterSale.status === 63) {
return '商家拒收货';
}
return '未知状态';
}
/**
* 格式化售后状态的描述
*
* @param afterSale 售后
*/
export function formatAfterSaleStatusDescription(afterSale) {
if (afterSale.status === 10) {
return '退款申请待商家处理';
}
if (afterSale.status === 20) {
return '请退货并填写物流信息';
}
if (afterSale.status === 30) {
return '退货退款申请待商家处理';
}
if (afterSale.status === 40) {
return '等待退款';
}
if (afterSale.status === 50) {
return '退款成功';
}
if (afterSale.status === 61) {
return '退款关闭';
}
if (afterSale.status === 62) {
return `商家不同意退款申请,拒绝原因:${afterSale.auditReason}`;
}
if (afterSale.status === 63) {
return `商家拒绝收货,不同意退款,拒绝原因:${afterSale.auditReason}`;
}
return '未知状态';
}
/**
* 处理售后的 button 操作按钮数组
*
* @param afterSale 售后
*/
export function handleAfterSaleButtons(afterSale) {
afterSale.buttons = [];
if ([10, 20, 30].includes(afterSale.status)) {
// 取消订单
afterSale.buttons.push('cancel');
}
if (afterSale.status === 20) {
// 退货信息
afterSale.buttons.push('delivery');
}
}
/**
* 倒计时
* @param toTime 截止时间
* @param fromTime 起始时间,默认当前时间
* @return {{s: string, ms: number, h: string, m: string}} 持续时间
*/
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,
};
}
/**
* 转换为 Dayjs
* @param {any} time 时间
* @return {dayjs.Dayjs}
*/
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));
}
}
/**
* 将分转成元
*
* @param price 分,例如说 100 分
* @returns {string} 元,例如说 1.00 元
*/
export function fen2yuan(price) {
return (price / 100.0).toFixed(2);
}
/**
* 将分转成元
*
* 如果没有小数点,则不展示小数点部分
*
* @param price 分,例如说 100 分
* @returns {string} 元,例如说 1 元
*/
export function fen2yuanSimple(price) {
return fen2yuan(price).replace(/\.?0+$/, '');
}
/**
* 将折扣百分比转化为“打x者”的 x 部分
*
* @param discountPercent
*/
export function formatDiscountPercent(discountPercent) {
return (discountPercent / 10.0).toFixed(1).replace(/\.?0+$/, '');
}
/**
* 从商品 SKU 数组中,转换出商品属性的数组
*
* 类似结构:[{
* id: // 属性的编号
* name: // 属性的名字
* values: [{
* id: // 属性值的编号
* name: // 属性值的名字
* }]
* }]
*
* @param skus 商品 SKU 数组
*/
export function convertProductPropertyList(skus) {
let result = [];
for (const sku of skus) {
if (!sku.properties) {
continue;
}
for (const property of sku.properties) {
// ① 先处理属性
let resultProperty = result.find((item) => item.id === property.propertyId);
if (!resultProperty) {
resultProperty = {
id: property.propertyId,
name: property.propertyName,
values: [],
};
result.push(resultProperty);
}
// ② 再处理属性值
let resultValue = resultProperty.values.find((item) => item.id === property.valueId);
if (!resultValue) {
resultProperty.values.push({
id: property.valueId,
name: property.valueName,
});
}
}
}
return result;
}
export function appendSettlementProduct(spus, settlementInfos) {
if (!settlementInfos || settlementInfos.length === 0) {
return;
}
for (const spu of spus) {
const settlementInfo = settlementInfos.find((info) => info.spuId === spu.id);
if (!settlementInfo) {
return;
}
// 选择价格最小的 SKU 设置到 SPU 上
const settlementSku = settlementInfo.skus
.filter((sku) => sku.promotionPrice > 0)
.reduce((prev, curr) => (prev.promotionPrice < curr.promotionPrice ? prev : curr), []);
if (settlementSku) {
spu.promotionType = settlementSku.promotionType;
spu.promotionPrice = settlementSku.promotionPrice;
}
// 设置【满减送】活动
if (settlementInfo.rewardActivity) {
spu.rewardActivity = settlementInfo.rewardActivity;
}
}
}
// 获得满减送活动的规则描述group
export function getRewardActivityRuleGroupDescriptions(activity) {
if (!activity || !activity.rules || activity.rules.length === 0) {
return [];
}
const result = [
{ name: '满减', values: [] },
{ name: '赠品', values: [] },
{ name: '包邮', values: [] },
];
activity.rules.forEach((rule) => {
const conditionTypeStr =
activity.conditionType === 10 ? `${fen2yuanSimple(rule.limit)}` : `${rule.limit}`;
// 满减
if (rule.limit) {
result[0].values.push(`${conditionTypeStr}${fen2yuanSimple(rule.discountPrice)}`);
}
// 赠品
if (rule.point || (rule.giveCouponTemplateCounts && rule.giveCouponTemplateCounts.length > 0)) {
let tips = [];
if (rule.point) {
tips.push(`${rule.point} 积分`);
}
if (rule.giveCouponTemplateCounts && rule.giveCouponTemplateCounts.length > 0) {
tips.push(`${rule.giveCouponTemplateCounts.length} 张优惠券`);
}
result[1].values.push(`${conditionTypeStr} ${tips.join('、')}`);
}
// 包邮
if (rule.freeDelivery) {
result[2].values.push(`${conditionTypeStr} 包邮`);
}
});
// 移除 values 为空的元素
result.forEach((item) => {
if (item.values.length === 0) {
result.splice(result.indexOf(item), 1);
}
});
return result;
}
// 获得满减送活动的规则描述item
export function getRewardActivityRuleItemDescriptions(activity) {
if (!activity || !activity.rules || activity.rules.length === 0) {
return [];
}
const result = [];
activity.rules.forEach((rule) => {
const conditionTypeStr =
activity.conditionType === 10 ? `${fen2yuanSimple(rule.limit)}` : `${rule.limit}`;
// 满减
if (rule.limit) {
result.push(`${conditionTypeStr}${fen2yuanSimple(rule.discountPrice)}`);
}
// 赠品
if (rule.point) {
result.push(`${conditionTypeStr}${rule.point}积分`);
}
if (rule.giveCouponTemplateCounts && rule.giveCouponTemplateCounts.length > 0) {
result.push(`${conditionTypeStr}${rule.giveCouponTemplateCounts.length}张优惠券`);
}
// 包邮
if (rule.freeDelivery) {
result.push(`${conditionTypeStr}包邮`);
}
});
return result;
}

141
sheep/hooks/useModal.js Normal file
View File

@@ -0,0 +1,141 @@
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 AuthUtil from '@/sheep/api/member/auth';
// 打开授权弹框
export function showAuthModal(type = 'accountLogin') {
const modal = $store('modal');
if (modal.auth !== '') {
// 注意:延迟修改,保证下面的 closeAuthModal 先执行掉
setTimeout(() => {
modal.$patch((state) => {
state.auth = type;
});
}, 500);
closeAuthModal();
} 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;
}
// 只有 mobile 非空时才校验。因为部分场景(修改密码),不需要输入手机
if (mobile && !test.mobile(mobile)) {
$helper.toast('手机号码格式不正确');
return;
}
// 发送验证码 + 更新上次发送验证码时间
let scene = -1;
switch (event) {
case 'resetPassword':
scene = 4;
break;
case 'changePassword':
scene = 3;
break;
case 'changeMobile':
scene = 2;
break;
case 'smsLogin':
scene = 1;
break;
}
AuthUtil.sendSmsCode(mobile, scene).then((res) => {
if (res.code === 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.imgUrl)) {
state.advHistory.push(adv.imgUrl);
}
});
}

150
sheep/hooks/useWebSocket.js Normal file
View File

@@ -0,0 +1,150 @@
import { onBeforeUnmount, reactive, ref } from 'vue';
import { baseUrl, websocketPath } from '@/sheep/config';
import { copyValueToTarget } from '@/sheep/util';
import { getRefreshToken } from '@/sheep/request';
/**
* WebSocket 创建 hook
* @param opt 连接配置
* @return {{options: *}}
*/
export function useWebSocket(opt) {
const options = reactive({
url: (baseUrl + websocketPath).replace('http', 'ws') + '?token=' + getRefreshToken(), // ws 地址
isReconnecting: false, // 正在重新连接
reconnectInterval: 3000, // 重连间隔,单位毫秒
heartBeatInterval: 5000, // 心跳间隔,单位毫秒
pingTimeoutDuration: 1000, // 超过这个时间后端没有返回pong则判定后端断线了。
heartBeatTimer: null, // 心跳计时器
destroy: false, // 是否销毁
pingTimeout: null, // 心跳检测定时器
reconnectTimeout: null, // 重连定时器ID的属性
onConnected: () => {}, // 连接成功时触发
onClosed: () => {}, // 连接关闭时触发
onMessage: (data) => {}, // 收到消息
});
const SocketTask = ref(null); // SocketTask 由 uni.connectSocket() 接口创建
const initEventListeners = () => {
// 监听 WebSocket 连接打开事件
SocketTask.value.onOpen(() => {
console.log('WebSocket 连接成功');
// 连接成功时触发
options.onConnected();
// 开启心跳检查
startHeartBeat();
});
// 监听 WebSocket 接受到服务器的消息事件
SocketTask.value.onMessage((res) => {
try {
if (res.data === 'pong') {
// 收到心跳重置心跳超时检查
resetPingTimeout();
} else {
options.onMessage(JSON.parse(res.data));
}
} catch (error) {
console.error(error);
}
});
// 监听 WebSocket 连接关闭事件
SocketTask.value.onClose((event) => {
// 情况一:实例销毁
if (options.destroy) {
options.onClosed();
} else {
// 情况二:连接失败重连
// 停止心跳检查
stopHeartBeat();
// 重连
reconnect();
}
});
};
// 发送消息
const sendMessage = (message) => {
if (SocketTask.value && !options.destroy) {
SocketTask.value.send({ data: message });
}
};
// 开始心跳检查
const startHeartBeat = () => {
options.heartBeatTimer = setInterval(() => {
sendMessage('ping');
options.pingTimeout = setTimeout(() => {
// 如果在超时时间内没有收到 pong则认为连接断开
reconnect();
}, options.pingTimeoutDuration);
}, options.heartBeatInterval);
};
// 停止心跳检查
const stopHeartBeat = () => {
clearInterval(options.heartBeatTimer);
resetPingTimeout();
};
// WebSocket 重连
const reconnect = () => {
if (options.destroy || !SocketTask.value) {
// 如果WebSocket已被销毁或尚未完全关闭不进行重连
return;
}
// 重连中
options.isReconnecting = true;
// 清除现有的重连标志,以避免多次重连
if (options.reconnectTimeout) {
clearTimeout(options.reconnectTimeout);
}
// 设置重连延迟
options.reconnectTimeout = setTimeout(() => {
// 检查组件是否仍在运行和WebSocket是否关闭
if (!options.destroy) {
// 重置重连标志
options.isReconnecting = false;
// 初始化新的WebSocket连接
initSocket();
}
}, options.reconnectInterval);
};
const resetPingTimeout = () => {
if (options.pingTimeout) {
clearTimeout(options.pingTimeout);
options.pingTimeout = null; // 清除超时ID
}
};
const close = () => {
options.destroy = true;
stopHeartBeat();
if (options.reconnectTimeout) {
clearTimeout(options.reconnectTimeout);
}
if (SocketTask.value) {
SocketTask.value.close();
SocketTask.value = null;
}
};
const initSocket = () => {
options.destroy = false;
copyValueToTarget(options, opt);
SocketTask.value = uni.connectSocket({
url: options.url,
complete: () => {},
success: () => {},
});
initEventListeners();
};
initSocket();
onBeforeUnmount(() => {
close();
});
return { options };
}

52
sheep/index.js Normal file
View File

@@ -0,0 +1,52 @@
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 = {
$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
// TODO 芋艿:可以打印路由
// 同步前端页面到后端
// console.log(ROUTES)
}
export default sheep;

View File

@@ -0,0 +1,32 @@
const fs = require('fs');
const manifestPath = process.env.UNI_INPUT_DIR + '/manifest.json';
let Manifest = fs.readFileSync(manifestPath, {
encoding: 'utf-8'
});
function mpliveMainfestPlugin(isOpen) {
if (process.env.UNI_PLATFORM !== 'mp-weixin') return;
const manifestData = JSON.parse(Manifest)
if (isOpen === '0') {
delete manifestData['mp-weixin'].plugins['live-player-plugin'];
}
if (isOpen === '1') {
manifestData['mp-weixin'].plugins['live-player-plugin'] = {
"version": "1.3.5",
"provider": "wx2b03c6e691cd7370"
}
}
Manifest = JSON.stringify(manifestData, null, 2)
fs.writeFileSync(manifestPath, Manifest, {
"flag": "w"
})
}
export default mpliveMainfestPlugin

246
sheep/libs/permission.js Normal file
View File

@@ -0,0 +1,246 @@
/// null = 未请求1 = 已允许0 = 拒绝|受限, 2 = 系统未开启
var isIOS;
function album() {
var result = 0;
var PHPhotoLibrary = plus.ios.import('PHPhotoLibrary');
var authStatus = PHPhotoLibrary.authorizationStatus();
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(PHPhotoLibrary);
return result;
}
function camera() {
var result = 0;
var AVCaptureDevice = plus.ios.import('AVCaptureDevice');
var authStatus = AVCaptureDevice.authorizationStatusForMediaType('vide');
if (authStatus === 0) {
result = null;
} else if (authStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(AVCaptureDevice);
return result;
}
function location() {
var result = 0;
var cllocationManger = plus.ios.import('CLLocationManager');
var enable = cllocationManger.locationServicesEnabled();
var status = cllocationManger.authorizationStatus();
if (!enable) {
result = 2;
} else if (status === 0) {
result = null;
} else if (status === 3 || status === 4) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(cllocationManger);
return result;
}
function push() {
var result = 0;
var UIApplication = plus.ios.import('UIApplication');
var app = UIApplication.sharedApplication();
var enabledTypes = 0;
if (app.currentUserNotificationSettings) {
var settings = app.currentUserNotificationSettings();
enabledTypes = settings.plusGetAttribute('types');
if (enabledTypes == 0) {
result = 0;
console.log('推送权限没有开启');
} else {
result = 1;
console.log('已经开启推送功能!');
}
plus.ios.deleteObject(settings);
} else {
enabledTypes = app.enabledRemoteNotificationTypes();
if (enabledTypes == 0) {
result = 3;
console.log('推送权限没有开启!');
} else {
result = 4;
console.log('已经开启推送功能!');
}
}
plus.ios.deleteObject(app);
plus.ios.deleteObject(UIApplication);
return result;
}
function contact() {
var result = 0;
var CNContactStore = plus.ios.import('CNContactStore');
var cnAuthStatus = CNContactStore.authorizationStatusForEntityType(0);
if (cnAuthStatus === 0) {
result = null;
} else if (cnAuthStatus == 3) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(CNContactStore);
return result;
}
function record() {
var result = null;
var avaudiosession = plus.ios.import('AVAudioSession');
var avaudio = avaudiosession.sharedInstance();
var status = avaudio.recordPermission();
console.log('permissionStatus:' + status);
if (status === 1970168948) {
result = null;
} else if (status === 1735552628) {
result = 1;
} else {
result = 0;
}
plus.ios.deleteObject(avaudiosession);
return result;
}
function calendar() {
var result = null;
var EKEventStore = plus.ios.import('EKEventStore');
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(0);
if (ekAuthStatus == 3) {
result = 1;
console.log('日历权限已经开启');
} else {
console.log('日历权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function memo() {
var result = null;
var EKEventStore = plus.ios.import('EKEventStore');
var ekAuthStatus = EKEventStore.authorizationStatusForEntityType(1);
if (ekAuthStatus == 3) {
result = 1;
console.log('备忘录权限已经开启');
} else {
console.log('备忘录权限没有开启');
}
plus.ios.deleteObject(EKEventStore);
return result;
}
function requestIOS(permissionID) {
return new Promise((resolve, reject) => {
switch (permissionID) {
case 'push':
resolve(push());
break;
case 'location':
resolve(location());
break;
case 'record':
resolve(record());
break;
case 'camera':
resolve(camera());
break;
case 'album':
resolve(album());
break;
case 'contact':
resolve(contact());
break;
case 'calendar':
resolve(calendar());
break;
case 'memo':
resolve(memo());
break;
default:
resolve(0);
break;
}
});
}
function requestAndroid(permissionID) {
return new Promise((resolve, reject) => {
plus.android.requestPermissions(
[permissionID],
function (resultObj) {
var result = 0;
for (var i = 0; i < resultObj.granted.length; i++) {
var grantedPermission = resultObj.granted[i];
console.log('已获取的权限:' + grantedPermission);
result = 1;
}
for (var i = 0; i < resultObj.deniedPresent.length; i++) {
var deniedPresentPermission = resultObj.deniedPresent[i];
console.log('拒绝本次申请的权限:' + deniedPresentPermission);
result = 0;
}
for (var i = 0; i < resultObj.deniedAlways.length; i++) {
var deniedAlwaysPermission = resultObj.deniedAlways[i];
console.log('永久拒绝申请的权限:' + deniedAlwaysPermission);
result = -1;
}
resolve(result);
},
function (error) {
console.log('result error: ' + error.message);
resolve({
code: error.code,
message: error.message,
});
},
);
});
}
function gotoAppPermissionSetting() {
if (permission.isIOS) {
var UIApplication = plus.ios.import('UIApplication');
var application2 = UIApplication.sharedApplication();
var NSURL2 = plus.ios.import('NSURL');
var setting2 = NSURL2.URLWithString('app-settings:');
application2.openURL(setting2);
plus.ios.deleteObject(setting2);
plus.ios.deleteObject(NSURL2);
plus.ios.deleteObject(application2);
} else {
var Intent = plus.android.importClass('android.content.Intent');
var Settings = plus.android.importClass('android.provider.Settings');
var Uri = plus.android.importClass('android.net.Uri');
var mainActivity = plus.android.runtimeMainActivity();
var intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
var uri = Uri.fromParts('package', mainActivity.getPackageName(), null);
intent.setData(uri);
mainActivity.startActivity(intent);
}
}
const permission = {
get isIOS() {
return typeof isIOS === 'boolean'
? isIOS
: (isIOS = uni.getSystemInfoSync().platform === 'ios');
},
requestIOS: requestIOS,
requestAndroid: requestAndroid,
gotoAppSetting: gotoAppPermissionSetting,
};
export default permission;

184
sheep/libs/sdk-h5-weixin.js Normal file
View File

@@ -0,0 +1,184 @@
/**
* 本模块封装微信浏览器下的一些方法。
* 更多微信网页开发sdk方法,详见:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html
* 有 the permission value is offline verifying 报错请参考 @see https://segmentfault.com/a/1190000042289419 解决
*/
import jweixin from 'weixin-js-sdk';
import $helper from '@/sheep/helper';
import AuthUtil from '@/sheep/api/member/auth';
let configSuccess = false;
export default {
// 判断是否在微信中
isWechat() {
const ua = window.navigator.userAgent.toLowerCase();
// noinspection EqualityComparisonWithCoercionJS
return ua.match(/micromessenger/i) == 'micromessenger';
},
isReady(api) {
jweixin.ready(api);
},
// 初始化 JSSDK
async init(callback) {
if (!this.isWechat()) {
$helper.toast('请使用微信网页浏览器打开');
return;
}
// 调用后端接口,获得 JSSDK 初始化所需的签名
const url = location.href.split('#')[0];
const { code, data } = await AuthUtil.createWeixinMpJsapiSignature(url);
if (code === 0) {
jweixin.config({
debug: false,
appId: data.appId,
timestamp: data.timestamp,
nonceStr: data.nonceStr,
signature: data.signature,
jsApiList: ['chooseWXPay', 'openLocation', 'getLocation','updateTimelineShareData','scanQRCode'], // TODO 芋艿:后续可以设置更多权限;
openTagList: data.openTagList
});
}
// 监听结果
configSuccess = true;
jweixin.error((err) => {
configSuccess = false;
console.error('微信 JSSDK 初始化失败', err);
// $helper.toast('微信JSSDK:' + err.errMsg);
});
jweixin.ready(() => {
if (configSuccess) {
console.log('微信 JSSDK 初始化成功');
}
})
// 回调
if (callback) {
callback(data);
}
},
//在需要定位页面调用 TODO 芋艿:未测试
getLocation(callback) {
this.isReady(() => {
jweixin.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标如果要返回直接给openLocation用的火星坐标可传入'gcj02'
success: function (res) {
callback(res);
},
fail: function (res) {
console.log('%c微信H5sdk,getLocation失败', 'color:green;background:yellow');
},
});
});
},
// 获取微信收货地址
openAddress(callback) {
this.isReady(() => {
jweixin.openAddress({
success: function (res) {
callback.success && callback.success(res);
},
fail: function (err) {
callback.error && callback.error(err);
console.log('%c微信H5sdk,openAddress失败', 'color:green;background:yellow');
},
complete: function (res) {},
});
});
},
// 微信扫码 TODO 芋艿:未测试
scanQRCode(callback) {
this.isReady(() => {
jweixin.scanQRCode({
needResult: 1, // 默认为0扫描结果由微信处理1则直接返回扫描结果
scanType: ['qrCode', 'barCode'], // 可以指定扫二维码还是一维码,默认二者都有
success: function (res) {
callback(res);
},
fail: function (res) {
console.log('%c微信H5sdk,scanQRCode失败', 'color:green;background:yellow');
},
});
});
},
// 更新微信分享信息 TODO 芋艿:未测试
updateShareInfo(data, callback = null) {
this.isReady(() => {
const shareData = {
title: data.title,
desc: data.desc,
link: data.link,
imgUrl: data.image,
success: function (res) {
if (callback) {
callback(res);
}
// 分享后的一些操作,比如分享统计等等
},
cancel: function (res) {},
};
// 新版 分享聊天api
jweixin.updateAppMessageShareData(shareData);
// 新版 分享到朋友圈api
jweixin.updateTimelineShareData(shareData);
});
},
// 打开坐标位置 TODO 芋艿:未测试
openLocation(data, callback) {
this.isReady(() => {
jweixin.openLocation({
...data,
success: function (res) {
console.log(res);
}
});
});
},
// 选择图片 TODO 芋艿:未测试
chooseImage(callback) {
this.isReady(() => {
jweixin.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album'],
success: function (rs) {
callback(rs);
},
});
});
},
// 微信支付
wxpay(data, callback) {
this.isReady(() => {
jweixin.chooseWXPay({
timestamp: data.timeStamp, // 支付签名时间戳注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: data.nonceStr, // 支付签名随机串,不长于 32 位
package: data.packageValue, // 统一支付接口返回的prepay_id参数值提交格式如prepay_id=\*\*\*
signType: data.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: data.paySign, // 支付签名
success: function (res) {
callback.success && callback.success(res);
},
fail: function (err) {
callback.fail && callback.fail(err);
},
cancel: function (err) {
callback.cancel && callback.cancel(err);
},
});
});
},
};

175
sheep/platform/index.js Normal file
View File

@@ -0,0 +1,175 @@
/**
* Shopro 第三方平台功能聚合
* @version 1.0.3
* @author lidongtony
* @param {String} name - 厂商+平台名称
* @param {String} provider - 厂商
* @param {String} platform - 平台名称
* @param {String} os - 系统型号
* @param {Object} device - 设备信息
*/
import { isEmpty } from 'lodash-es';
// #ifdef H5
import { isWxBrowser } from '@/sheep/helper/utils';
// #endif
import wechat from './provider/wechat/index.js';
import apple from './provider/apple';
import share from './share';
import Pay from './pay';
const device = uni.getSystemInfoSync();
const os = device.platform;
let name = '';
let provider = '';
let platform = '';
let isWechatInstalled = true;
// #ifdef H5
if (isWxBrowser()) {
name = 'WechatOfficialAccount';
provider = 'wechat';
platform = 'officialAccount';
} else {
name = 'H5';
platform = 'h5';
}
// #endif
// #ifdef APP-PLUS
name = 'App';
platform = 'openPlatform';
// 检查微信客户端是否安装否则AppleStore会因此拒绝上架
if (os === 'ios') {
isWechatInstalled = plus.ios.import('WXApi').isWXAppInstalled();
}
// #endif
// #ifdef MP-WEIXIN
name = 'WechatMiniProgram';
platform = 'miniProgram';
provider = 'wechat';
// #endif
if (isEmpty(name)) {
uni.showToast({
title: '暂不支持该平台',
icon: 'none',
});
}
// 加载当前平台前置行为
const load = () => {
if (provider === 'wechat') {
wechat.load();
}
};
// 使用厂商独占sdk name = 'wechat' | 'alipay' | 'apple'
const useProvider = (_provider = '') => {
if (_provider === '') _provider = provider;
if (_provider === 'wechat') return wechat;
if (_provider === 'apple') return apple;
};
// 支付服务转发
const pay = (payment, orderType, orderSN) => {
return new Pay(payment, orderType, orderSN);
};
/**
* 检查更新 (只检查小程序和App)
* @param {Boolean} silence - 静默检查
*/
const checkUpdate = (silence = false) => {
let canUpdate;
// #ifdef MP-WEIXIN
useProvider().checkUpdate(silence);
// #endif
// #ifdef APP-PLUS
// TODO: 热更新
// #endif
};
/**
* 检查网络
* @param {Boolean} silence - 静默检查
*/
async function checkNetwork() {
const networkStatus = await uni.getNetworkType();
if (networkStatus.networkType == 'none') {
return Promise.resolve(false);
}
return Promise.resolve(true);
}
// 获取小程序胶囊信息
const getCapsule = () => {
// #ifdef MP
let capsule = uni.getMenuButtonBoundingClientRect();
if (!capsule) {
capsule = {
bottom: 56,
height: 32,
left: 278,
right: 365,
top: 24,
width: 87,
};
}
return capsule;
// #endif
// #ifndef MP
return {
bottom: 56,
height: 32,
left: 278,
right: 365,
top: 24,
width: 87,
};
// #endif
};
const capsule = getCapsule();
// 标题栏高度
const getNavBar = () => {
return device.statusBarHeight + 44;
};
const navbar = getNavBar();
function getLandingPage() {
let page = '';
// #ifdef H5
page = location.href.split('?')[0];
// #endif
return page;
}
// 设置ios+公众号网页落地页 解决微信sdk签名问题
const landingPage = getLandingPage();
const _platform = {
name,
device,
os,
provider,
platform,
useProvider,
checkUpdate,
checkNetwork,
pay,
share,
load,
capsule,
navbar,
landingPage,
isWechatInstalled,
};
export default _platform;

396
sheep/platform/pay.js Normal file
View File

@@ -0,0 +1,396 @@
import sheep from '@/sheep';
// #ifdef H5
import $wxsdk from '@/sheep/libs/sdk-h5-weixin';
// #endif
import { getRootUrl } from '@/sheep/helper';
import PayOrderApi from '@/sheep/api/pay/order';
/**
* 支付
*
* @param {String} payment = ['wechat','alipay','wallet','mock'] - 支付方式
* @param {String} orderType = ['goods','recharge','groupon'] - 订单类型
* @param {String} id - 订单号
*/
export default class SheepPay {
constructor(payment, orderType, id) {
this.payment = payment;
this.id = id;
this.orderType = orderType;
this.payAction();
}
payAction() {
const payAction = {
WechatOfficialAccount: {
wechat: () => {
this.wechatOfficialAccountPay();
},
alipay: () => {
this.redirectPay(); // 现在公众号可以直接跳转支付宝页面
},
wallet: () => {
this.walletPay();
},
mock: () => {
this.mockPay();
},
},
WechatMiniProgram: {
wechat: () => {
this.wechatMiniProgramPay();
},
alipay: () => {
this.copyPayLink();
},
wallet: () => {
this.walletPay();
},
mock: () => {
this.mockPay();
},
},
App: {
wechat: () => {
this.wechatAppPay();
},
alipay: () => {
this.alipay();
},
wallet: () => {
this.walletPay();
},
mock: () => {
this.mockPay();
},
},
H5: {
wechat: () => {
this.wechatWapPay();
},
alipay: () => {
this.redirectPay();
},
wallet: () => {
this.walletPay();
},
mock: () => {
this.mockPay();
},
},
};
return payAction[sheep.$platform.name][this.payment]();
}
// 预支付
prepay(channel) {
return new Promise(async (resolve, reject) => {
// const openid = await sheep.$platform.useProvider('wechat').getOpenid();
let data = {
id: this.id,
channelCode: channel,
channelExtras: {
openid: uni.getStorageSync('openid')
},
};
// 特殊逻辑:微信公众号、小程序支付时,必须传入 openid
if (['wx_pub', 'wx_lite'].includes(channel)) {
const openid = await sheep.$platform.useProvider('wechat').getOpenid();
// 如果获取不到 openid微信无法发起支付此时需要引导
if (!openid) {
this.bindWeixin();
return;
}
// data.channelExtras.openid = openid;
data.channelExtras.openid = uni.getStorageSync('openid');
}
// 发起预支付 API 调用
PayOrderApi.submitOrder(data).then((res) => {
// 成功时
res.code === 0 && resolve(res);
// 失败时
if (res.code !== 0 && res.msg.indexOf('无效的openid') >= 0) {
// 特殊逻辑:微信公众号、小程序支付时,必须传入 openid 不正确的情况
if (
res.msg.indexOf('无效的openid') >= 0 || // 获取的 openid 不正确时,或者随便输入了个 openid
res.msg.indexOf('下单账号与支付账号不一致') >= 0
) {
// https://developers.weixin.qq.com/community/develop/doc/00008c53c347804beec82aed051c00
this.bindWeixin();
}
}
});
});
}
// #ifdef H5
// 微信公众号 JSSDK 支付
async wechatOfficialAccountPay() {
let { code, data } = await this.prepay('wx_pub');
if (code !== 0) {
return;
}
const payConfig = JSON.parse(data.displayContent);
$wxsdk.wxpay(payConfig, {
success: () => {
this.payResult('success');
},
cancel: () => {
sheep.$helper.toast('支付已手动取消');
},
fail: (error) => {
if (error.errMsg.indexOf('chooseWXPay:没有此SDK或暂不支持此SDK模拟') >= 0) {
sheep.$helper.toast(
'发起微信支付失败,原因:可能是微信开发者工具不支持,建议使用微信打开网页后支付',
);
return;
}
this.payResult('fail');
},
});
}
// 浏览器微信 H5 支付 TODO 芋艿待接入注意H5 支付是给普通浏览器,不是微信公众号的支付,绝大多数人用不到,可以忽略)
async wechatWapPay() {
const { error, data } = await this.prepay();
if (error === 0) {
const redirect_url = `${getRootUrl()}pages/pay/result?id=${this.id}&payment=${
this.payment
}&orderType=${this.orderType}`;
location.href = `${data.pay_data.h5_url}&redirect_url=${encodeURIComponent(redirect_url)}`;
}
}
// 支付链接(支付宝 wap 支付)
async redirectPay() {
let { code, data } = await this.prepay('alipay_wap');
if (code !== 0) {
return;
}
location.href = data.displayContent;
}
// #endif
// 微信小程序支付
async wechatMiniProgramPay() {
// let that = this;
let { code, data } = await this.prepay('tonglian_wxlite');
if (code !== 0) {
return;
}
// 调用微信小程序支付
const displayContent = JSON.parse(data.displayContent);
// console.log("支付参数:", displayContent);
const payConfig = displayContent?.body?.wxAppModel;
console.log("支付参数:", payConfig);
uni.requestPayment({
// provider: 'wxpay',
// timeStamp: payConfig.timeStamp,
// timeStamp: payConfig.timestamp,
// nonceStr: payConfig.nonceStr,
// package: payConfig.packageValue,
// package: payConfig.prePayId,
// signType: payConfig.signType,
// paySign: payConfig.sign,
// paySign: payConfig.paySign,
success: (res) => {
this.payResult('success');
},
fail: (err) => {
console.log("err23333",err)
if (err.errMsg === 'requestPayment:fail cancel') {
sheep.$helper.toast('支付已手动取消');
} else {
this.payResult('fail');
}
},
});
//测试通联支付代码
// const payConfig = JSON.parse(data.displayContent);
// const version = wx.getAppBaseInfo().SDKVersion;
// const accountInfo = wx.getAccountInfoSync();
// console.log('小程序版本号', accountInfo.miniProgram.version);
// const params = payConfig?.body?.cashierModel?.extraData;
// if (params) {
// uni.navigateToMiniProgram({
// appId: 'wxef277996acc166c3',
// extraData: params
// });
// }else {
// sheep.$helper.toast('获取支付参数失败');
// }
}
// 余额支付
async walletPay() {
const { code } = await this.prepay('wallet');
code === 0 && this.payResult('success');
}
// 模拟支付
async mockPay() {
const { code } = await this.prepay('mock');
code === 0 && this.payResult('success');
}
// 支付宝复制链接支付(通过支付宝 wap 支付实现)
async copyPayLink() {
let { code, data } = await this.prepay('alipay_wap');
if (code !== 0) {
return;
}
// 引入 showModal 点击确认:复制链接;
uni.showModal({
title: '支付宝支付',
content: '复制链接到外部浏览器',
confirmText: '复制链接',
success: (res) => {
if (res.confirm) {
sheep.$helper.copyText(data.displayContent);
}
},
});
}
// 支付宝支付App TODO 芋艿:待接入【暂时没打包 app所以没接入一般人用不到】
async alipay() {
let that = this;
const { error, data } = await this.prepay();
if (error === 0) {
uni.requestPayment({
provider: 'alipay',
orderInfo: data.pay_data, //支付宝订单数据
success: (res) => {
that.payResult('success');
},
fail: (err) => {
if (err.errMsg === 'requestPayment:fail [paymentAlipay:62001]user cancel') {
sheep.$helper.toast('支付已手动取消');
} else {
that.payResult('fail');
}
},
});
}
}
// 微信支付App TODO 芋艿:待接入:待接入【暂时没打包 app所以没接入一般人用不到】
async wechatAppPay() {
let that = this;
let { error, data } = await this.prepay();
if (error === 0) {
uni.requestPayment({
provider: 'wxpay',
orderInfo: data.pay_data, //微信订单数据(官方说是string。实测为object)
success: (res) => {
that.payResult('success');
},
fail: (err) => {
err.errMsg !== 'requestPayment:fail cancel' && that.payResult('fail');
},
});
}
}
// 支付结果跳转,success:成功fail:失败
payResult(resultType) {
goPayResult(this.id, this.orderType, resultType);
}
// 引导绑定微信
bindWeixin() {
uni.showModal({
title: '微信支付',
content: '请先绑定微信再使用微信支付',
success: function (res) {
if (res.confirm) {
sheep.$platform.useProvider('wechat').bind();
}
},
});
}
}
export function getPayMethods(channels) {
const payMethods = [
// {
// icon: '/static/img/shop/pay/wechat.png',
// title: '微信支付',
// value: 'wechat',
// disabled: true,
// },
// {
// icon: '/static/img/shop/pay/alipay.png',
// title: '支付宝支付',
// value: 'alipay',
// disabled: true,
// },
{
icon: '/static/img/shop/pay/wallet.png',
title: '额度支付',
value: 'wallet',
disabled: true,
},
// {
// icon: '/static/img/shop/pay/apple.png',
// title: 'Apple Pay',
// value: 'apple',
// disabled: true,
// },
// {
// icon: '/static/img/shop/pay/wallet.png',
// title: '模拟支付',
// value: 'mock',
// disabled: true,
// },
];
const platform = sheep.$platform.name;
// 1. 处理【微信支付】
// const wechatMethod = payMethods[0];
// if (
// (platform === 'WechatOfficialAccount' && channels.includes('wx_pub')) ||
// (platform === 'WechatMiniProgram' && channels.includes('tonglian_wxlite')) ||
// (platform === 'App' && channels.includes('wx_app'))
// ) {
// wechatMethod.disabled = false;
// }
// 2. 处理【支付宝支付】
// const alipayMethod = payMethods[1];
// if (
// (platform === 'H5' && channels.includes('alipay_wap')) ||
// (platform === 'WechatOfficialAccount' && channels.includes('alipay_wap')) ||
// (platform === 'WechatMiniProgram' && channels.includes('alipay_wap')) ||
// (platform === 'App' && channels.includes('alipay_app'))
// ) {
// alipayMethod.disabled = false;
// }
// 3. 处理【余额支付】
const walletMethod = payMethods[0];
// const walletMethod = payMethods[1];
if (channels.includes('wallet')) {
walletMethod.disabled = false;
}
// 4. 处理【苹果支付】TODO 芋艿:未来接入
// 5. 处理【模拟支付】
// const mockMethod = payMethods[4];
// if (channels.includes('mock')) {
// mockMethod.disabled = false;
// }
return payMethods;
}
// 支付结果跳转,success:成功fail:失败
export function goPayResult(id, orderType, resultType) {
sheep.$router.redirect('/pages/pay/result', {
id,
orderType,
payState: resultType,
});
}

View File

@@ -0,0 +1,36 @@
// import third from '@/sheep/api/third';
// TODO 芋艿:等后面搞 App 再弄
const login = () => {
return new Promise(async (resolve, reject) => {
const loginRes = await uni.login({
provider: 'apple',
success: () => {
uni.getUserInfo({
provider: 'apple',
success: async (res) => {
if (res.errMsg === 'getUserInfo:ok') {
const payload = res.userInfo;
const { error } = await third.apple.login({
payload,
shareInfo: uni.getStorageSync('shareLog') || {},
});
if (error === 0) {
resolve(true);
} else {
resolve(false);
}
}
},
});
},
fail: (err) => {
resolve(false);
},
});
});
};
export default {
login,
};

View File

@@ -0,0 +1,9 @@
// #ifdef APP-PLUS
import service from './app';
// #endif
let apple = {};
if (typeof service !== 'undefined') {
apple = service;
}
export default apple;

View File

@@ -0,0 +1,15 @@
// #ifdef H5
import service from './officialAccount';
// #endif
// #ifdef MP-WEIXIN
import service from './miniProgram';
// #endif
// #ifdef APP-PLUS
import service from './openPlatform';
// #endif
const wechat = service;
export default wechat;

View File

@@ -0,0 +1,213 @@
import AuthUtil from '@/sheep/api/member/auth';
import SocialApi from '@/sheep/api/member/social';
import UserApi from '@/sheep/api/member/user';
const socialType = 34; // 社交类型 - 微信小程序
let subscribeEventList = [];
// 加载微信小程序
function load() {
checkUpdate();
getSubscribeTemplate();
}
// 微信小程序静默授权登陆
const login = async () => {
return new Promise(async (resolve, reject) => {
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 社交登录
const loginResult = await AuthUtil.socialLogin(socialType, codeResult.code, 'default');
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序手机号授权登陆
const mobileLogin = async (e) => {
return new Promise(async (resolve, reject) => {
if (e.errMsg !== 'getPhoneNumber:ok') {
return resolve(false);
}
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 一键登录
const loginResult = await AuthUtil.weixinMiniAppLogin(e.code, codeResult.code, 'default');
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序绑定
const bind = () => {
return new Promise(async (resolve, reject) => {
// 1. 获得微信 code
const codeResult = await uni.login();
if (codeResult.errMsg !== 'login:ok') {
return resolve(false);
}
// 2. 绑定账号
const bindResult = await SocialApi.socialBind(socialType, codeResult.code, 'default');
if (bindResult.code === 0) {
setOpenid(bindResult.data);
return resolve(true);
} else {
return resolve(false);
}
});
};
// 微信小程序解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 绑定用户手机号
const bindUserPhoneNumber = (e) => {
return new Promise(async (resolve, reject) => {
const { code } = await UserApi.updateUserMobileByWeixin(e.code);
if (code === 0) {
resolve(true);
}
resolve(false);
});
};
// 设置 openid 到本地存储,目前只有 pay 支付时会使用
function setOpenid(openid) {
uni.setStorageSync('openid', openid);
}
// 获得 openid
async function getOpenid(force = false) {
let openid = uni.getStorageSync('openid');
if (!openid && force) {
const info = await getInfo();
if (info && info.openid) {
openid = info.openid;
setOpenid(openid);
}
}
return openid;
}
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
// ========== 非登录相关的逻辑 ==========
// 小程序更新
const checkUpdate = async (silence = true) => {
if (uni.canIUse('getUpdateManager')) {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate(function(res) {
// 请求完新版本信息的回调
if (res.hasUpdate) {
updateManager.onUpdateReady(function() {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success: function(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function() {
// 新的版本下载失败
// uni.showModal({
// title: '已经有新版本了哟~',
// content: '新版本已经上线啦,请您删除当前小程序,重新搜索打开~',
// });
});
} else {
if (!silence) {
uni.showModal({
title: '当前为最新版本',
showCancel: false,
});
}
}
});
}
};
// 获取订阅消息模板
async function getSubscribeTemplate() {
const { code, data } = await SocialApi.getSubscribeTemplateList();
if (code === 0) {
subscribeEventList = data;
}
}
// 订阅消息
function subscribeMessage(event, callback= undefined) {
let tmplIds = [];
if (typeof event === 'string') {
const temp = subscribeEventList.find(item => item.title.includes(event));
if (temp) {
tmplIds.push(temp.id);
}
}
if (typeof event === 'object') {
event.forEach((e) => {
const temp = subscribeEventList.find(item => item.title.includes(e));
if (temp) {
tmplIds.push(temp.id);
}
});
}
if (tmplIds.length === 0) return;
uni.requestSubscribeMessage({
tmplIds,
success: ()=>{
// 不管是拒绝还是同意都触发
callback && callback()
},
fail: (err) => {
console.log(err);
},
});
}
export default {
load,
login,
bind,
unbind,
bindUserPhoneNumber,
mobileLogin,
getInfo,
getOpenid,
subscribeMessage,
checkUpdate,
};

View File

@@ -0,0 +1,106 @@
import $wxsdk from '@/sheep/libs/sdk-h5-weixin';
import { getRootUrl } from '@/sheep/helper';
import AuthUtil from '@/sheep/api/member/auth';
import SocialApi from '@/sheep/api/member/social';
const socialType = 31; // 社交类型 - 微信公众号
// 加载微信公众号JSSDK
async function load() {
$wxsdk.init();
}
// 微信公众号登陆
async function login(code = '', state = '') {
// 情况一:没有 code 时,去获取 code
if (!code) {
const loginUrl = await getLoginUrl();
if (loginUrl) {
uni.setStorageSync('returnUrl', location.href);
window.location = loginUrl;
}
// 情况二:有 code 时,使用 code 去自动登录
} else {
// 解密 code 发起登陆
const loginResult = await AuthUtil.socialLogin(socialType, code, state);
if (loginResult.code === 0) {
setOpenid(loginResult.data.openid);
return loginResult;
}
}
return false;
}
// 微信公众号绑定
async function bind(code = '', state = '') {
// 情况一:没有 code 时,去获取 code
if (code === '') {
const loginUrl = await getLoginUrl('bind');
if (loginUrl) {
uni.setStorageSync('returnUrl', location.href);
window.location = loginUrl;
}
} else {
// 情况二:有 code 时,使用 code 去自动绑定
const loginResult = await SocialApi.socialBind(socialType, code, state);
if (loginResult.code === 0) {
setOpenid(loginResult.data);
return loginResult;
}
}
return false;
}
// 微信公众号解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 获取公众号登陆地址
async function getLoginUrl(event = 'login') {
const page = getRootUrl() + 'pages/index/login'
+ '?event=' + event; // event 目的,区分是 login 还是 bind
const { code, data } = await AuthUtil.socialAuthRedirect(socialType, page);
if (code !== 0) {
return undefined;
}
return data;
}
// 设置 openid 到本地存储,目前只有 pay 支付时会使用
function setOpenid(openid) {
uni.setStorageSync('openid', openid);
}
// 获得 openid
async function getOpenid(force = false) {
let openid = uni.getStorageSync('openid');
if (!openid && force) {
const info = await getInfo();
if (info && info.openid) {
openid = info.openid;
setOpenid(openid);
}
}
return openid;
}
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
export default {
load,
login,
bind,
unbind,
getInfo,
getOpenid,
jsWxSdk: $wxsdk,
};

View File

@@ -0,0 +1,64 @@
// 登录
import third from '@/sheep/api/migration/third';
import SocialApi from '@/sheep/api/member/social';
import $share from '@/sheep/platform/share';
// TODO 芋艿:等后面搞 App 再弄
const socialType = 32; // 社交类型 - 微信开放平台
const load = async () => {};
// 微信开放平台移动应用授权登陆
const login = () => {
return new Promise(async (resolve, reject) => {
const loginRes = await uni.login({
provider: 'weixin',
onlyAuthorize: true,
});
debugger
if (loginRes.errMsg == 'login:ok') {
// TODO third.wechat.login 函数未实现
const res = await third.wechat.login({
platform: 'openPlatform',
shareInfo: uni.getStorageSync('shareLog') || {},
payload: encodeURIComponent(
JSON.stringify({
code: loginRes.code,
}),
),
});
if (res.error === 0) {
$share.bindBrokerageUser()
resolve(true);
}
} else {
uni.showToast({
icon: 'none',
title: loginRes.errMsg,
});
}
resolve(false);
});
};
// 微信 App 解除绑定
const unbind = async (openid) => {
const { code } = await SocialApi.socialUnbind(socialType, openid);
return code === 0;
};
// 获得社交信息
async function getInfo() {
const { code, data } = await SocialApi.getSocialUser(socialType);
if (code !== 0) {
return undefined;
}
return data;
}
export default {
load,
login,
getInfo
};

208
sheep/platform/share.js Normal file
View File

@@ -0,0 +1,208 @@
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import $router from '@/sheep/router';
import $url from '@/sheep/url';
import BrokerageApi from '@/sheep/api/trade/brokerage';
// #ifdef H5
import $wxsdk from '@/sheep/libs/sdk-h5-weixin';
// #endif
// 设置分享的平台渠道: 1=H5,2=微信公众号网页,3=微信小程序,4=App,...按需扩展
const platformMap = ['H5', 'WechatOfficialAccount', 'WechatMiniProgram', 'App'];
// 设置分享方式: 1=直接转发,2=海报,3=复制链接,...按需扩展
const fromMap = ['forward', 'poster', 'link'];
// TODO 芋艿:分享的接入
// 设置分享信息参数
const getShareInfo = (
scene = {
title: '', // 自定义分享标题
desc: '', // 自定义描述
image: '', // 自定义分享图片
params: {}, // 自定义分享参数
},
poster = {
// 自定义海报数据
type: 'user',
},
) => {
const shareInfo = {
title: '', // 分享标题
desc: '', // 描述
image: '', // 分享图片
path: '', // 分享页面+参数
link: '', // 分享Url+参数
query: '', // 分享参数
poster, // 海报所需数据
};
const app = $store('app');
const shareConfig = app.platform.share;
// 自动拼接分享用户参数
const query = buildSpmQuery(scene.params);
shareInfo.query = query;
// 配置分享链接地址
shareInfo.link = buildSpmLink(query, shareConfig.linkAddress);
// 配置页面地址带参数
shareInfo.path = buildSpmPath(query);
// 配置转发参数
if (shareConfig.methods.includes('forward')) {
// TODO puhui999: forward 这块有点问题
if (shareConfig.forwardInfo.title === '' || shareConfig.forwardInfo.image === '') {
console.log('请在平台设置中配置转发信息');
}
// 设置自定义分享信息
shareInfo.title = scene.title || shareConfig.forwardInfo.title;
shareInfo.image = $url.cdn(scene.image || shareConfig.forwardInfo.image);
shareInfo.desc = scene.desc || shareConfig.forwardInfo.subtitle;
shareInfo.path = buildSpmPath(scene.path, query);
}
return shareInfo;
};
/**
* 构造 spm 分享参数
*
* @param params json 格式其中包含1shareId 分享用户的编号2page 页面类型3query 页面 ID参数4platform 平台类型5from 分享来源类型。
* @return 分享串 `spm=${shareId}.${page}.${query}.${platform}.${from}`
*/
const buildSpmQuery = (params) => {
const user = $store('user');
let shareId = '0'; // 设置分享者用户ID
if (typeof params.shareId === 'undefined') {
if (user.isLogin) {
shareId = user.userInfo.id;
}
}
let page = '1'; // 页面类型: 1=首页(默认),2=商品,3=拼团商品,4=秒杀商品,5=邀请参团,6=分销邀请...按需扩展
if (typeof params.page !== 'undefined') {
page = params.page;
}
let query = '0'; // 设置页面ID: 如商品ID、拼团ID等
if (typeof params.query !== 'undefined') {
query = params.query;
}
let platform = platformMap.indexOf($platform.name) + 1;
let from = '1';
if (typeof params.from !== 'undefined') {
from = platformMap.indexOf(params.from) + 1;
}
// spmParams = ... 可按需扩展
return `spm=${shareId}.${page}.${query}.${platform}.${from}`;
};
// 构造页面分享参数: 所有的分享都先到首页进行 spm 参数解析
const buildSpmPath = (query) => {
// 默认是主页,页面 page例如 pages/index/index根路径前不要填加 /
// 不能携带参数参数请放在scene字段里如果不填写这个字段默认跳主页面。scancode_time为系统保留参数不允许配置
return `pages/index/index`;
};
// 构造分享链接
const buildSpmLink = (query, linkAddress = '') => {
return `${linkAddress}?${query}`;
};
// 解析Spm
const decryptSpm = (spm) => {
const user = $store('user');
let shareParamsArray = spm.split('.');
let shareParams = {
spm,
shareId: 0,
page: '',
query: {},
platform: '',
from: '',
};
let query;
shareParams.shareId = shareParamsArray[0];
switch (shareParamsArray[1]) {
case '1':
// 默认首页不跳转
shareParams.page = '/pages/index/index';
break;
case '2':
// 普通商品
shareParams.page = '/pages/goods/index';
shareParams.query = {
id: shareParamsArray[2],
};
break;
case '3':
// 拼团商品
shareParams.page = '/pages/goods/groupon';
query = shareParamsArray[2].split(',');
shareParams.query = {
id: query[0],
activity_id: query[1],
};
break;
case '4':
// 秒杀商品
shareParams.page = '/pages/goods/seckill';
query = shareParamsArray[2].split(',');
shareParams.query = {
id: query[1],
};
break;
case '5':
// 参与拼团
shareParams.page = '/pages/activity/groupon/detail';
shareParams.query = {
id: shareParamsArray[2],
};
break;
}
shareParams.platform = platformMap[shareParamsArray[3] - 1];
shareParams.from = fromMap[shareParamsArray[4] - 1];
if (shareParams.shareId !== 0) {
// 已登录 绑定推广员
if (user.isLogin) {
bindBrokerageUser(shareParams.shareId);
} else {
// 记录分享者编号
uni.setStorageSync('shareId', shareParams.shareId);
}
}
if (shareParams.page !== '/pages/index/index') {
$router.go(shareParams.page, shareParams.query);
}
return shareParams;
};
// 绑定推广员
const bindBrokerageUser = async (val = undefined) => {
try {
const shareId = val || uni.getStorageSync('shareId');
if (!shareId) {
return;
}
await BrokerageApi.bindBrokerageUser({ bindUserId: shareId });
uni.removeStorageSync('shareId');
} catch (e) {
console.error(e);
}
};
// 更新公众号分享sdk
const updateShareInfo = (shareInfo) => {
// #ifdef H5
if ($platform.name === 'WechatOfficialAccount') {
$wxsdk.updateShareInfo(shareInfo);
}
// #endif
};
export default {
getShareInfo,
updateShareInfo,
decryptSpm,
bindBrokerageUser,
};

302
sheep/request/index.js Normal file
View File

@@ -0,0 +1,302 @@
/**
* Shopro-request
* @description api模块管理loading配置请求拦截错误处理
*/
import Request from 'luch-request';
import { apiPath, baseUrl, tenantId } from '@/sheep/config';
import $store from '@/sheep/store';
import $platform from '@/sheep/platform';
import { showAuthModal } from '@/sheep/hooks/useModal';
import AuthUtil from '@/sheep/api/member/auth';
import { getTerminal } from '@/sheep/util/const';
const options = {
// 显示操作成功消息 默认不显示
showSuccess: false,
// 成功提醒 默认使用后端返回值
successMsg: '',
// 显示失败消息 默认显示
showError: true,
// 失败提醒 默认使用后端返回信息
errorMsg: '',
// 显示请求时loading模态框 默认显示
showLoading: true,
// loading提醒文字
loadingMsg: '加载中',
// 需要授权才能请求 默认放开
auth: false,
// ...
};
// Loading全局实例
let LoadingInstance = {
target: null,
count: 0,
};
/**
* 关闭loading
*/
function closeLoading() {
if (LoadingInstance.count > 0) LoadingInstance.count--;
if (LoadingInstance.count === 0) uni.hideLoading();
}
/**
* @description 请求基础配置 可直接使用访问自定义请求
*/
const http = new Request({
baseURL: baseUrl + apiPath,
timeout: 8000,
method: 'GET',
header: {
Accept: 'text/json',
'Content-Type': 'application/json;charset=UTF-8',
platform: $platform.name,
},
// #ifdef APP-PLUS
sslVerify: false,
// #endif
// #ifdef H5
// 跨域请求时是否携带凭证cookies仅H5支持HBuilderX 2.6.15+
withCredentials: false,
// #endif
custom: options,
});
/**
* @description 请求拦截器
*/
http.interceptors.request.use(
(config) => {
// 自定义处理【auth 授权】:必须登录的接口,则跳出 AuthModal 登录弹窗
if (config.custom.auth && !$store('user').isLogin) {
showAuthModal();
return Promise.reject();
}
// 自定义处理【loading 加载中】:如果需要显示 loading则显示 loading
if (config.custom.showLoading) {
LoadingInstance.count++;
LoadingInstance.count === 1 &&
uni.showLoading({
title: config.custom.loadingMsg,
mask: true,
fail: () => {
uni.hideLoading();
},
});
}
// 增加 token 令牌、terminal 终端、tenant 租户的请求头
const token = getAccessToken();
if (token) {
config.header['Authorization'] = token;
}
config.header['terminal'] = getTerminal();
config.header['Accept'] = '*/*';
config.header['tenant-id'] = tenantId;
return config;
},
(error) => {
return Promise.reject(error);
},
);
/**
* @description 响应拦截器
*/
http.interceptors.response.use(
(response) => {
// 约定:如果是 /auth/ 下的 URL 地址,并且返回了 accessToken 说明是登录相关的接口,则自动设置登陆令牌
if (response.config.url.indexOf('/member/auth/') >= 0 && response.data?.data?.accessToken) {
$store('user').setToken(response.data.data.accessToken, response.data.data.refreshToken);
}
// 自定处理【loading 加载中】:如果需要显示 loading则关闭 loading
response.config.custom.showLoading && closeLoading();
// 自定义处理【error 错误提示】:如果需要显示错误提示,则显示错误提示
if (response.data.code !== 0) {
// 特殊:如果 401 错误码,则跳转到登录页 or 刷新令牌
if (response.data.code === 401) {
return refreshToken(response.config);
}
// 错误提示
if (response.config.custom.showError) {
uni.showToast({
title: response.data.msg || '服务器开小差啦,请稍后再试~',
icon: 'none',
mask: true,
});
}
}
// 自定义处理【showSuccess 成功提示】:如果需要显示成功提示,则显示成功提示
if (
response.config.custom.showSuccess &&
response.config.custom.successMsg !== '' &&
response.data.code === 0
) {
uni.showToast({
title: response.config.custom.successMsg,
icon: 'none',
});
}
// 返回结果:包括 code + data + msg
return Promise.resolve(response.data);
},
(error) => {
const userStore = $store('user');
const isLogin = userStore.isLogin;
let errorMessage = '网络请求出错';
if (error !== undefined) {
switch (error.statusCode) {
case 400:
errorMessage = '请求错误';
break;
case 401:
errorMessage = isLogin ? '您的登陆已过期' : '请先登录';
// 正常情况下,后端不会返回 401 错误,所以这里不处理 handleAuthorized
break;
case 403:
errorMessage = '拒绝访问';
break;
case 404:
errorMessage = '请求出错';
break;
case 408:
errorMessage = '请求超时';
break;
case 429:
errorMessage = '请求频繁, 请稍后再访问';
break;
case 500:
errorMessage = '服务器开小差啦,请稍后再试~';
break;
case 501:
errorMessage = '服务未实现';
break;
case 502:
errorMessage = '网络错误';
break;
case 503:
errorMessage = '服务不可用';
break;
case 504:
errorMessage = '网络超时';
break;
case 505:
errorMessage = 'HTTP 版本不受支持';
break;
}
if (error.errMsg.includes('timeout')) errorMessage = '请求超时';
// #ifdef H5
if (error.errMsg.includes('Network'))
errorMessage = window.navigator.onLine ? '服务器异常' : '请检查您的网络连接';
// #endif
}
if (error && error.config) {
if (error.config.custom.showError === false) {
uni.showToast({
title: error.data?.msg || errorMessage,
icon: 'none',
mask: true,
});
}
error.config.custom.showLoading && closeLoading();
}
return false;
},
);
// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
let requestList = []; // 请求队列
let isRefreshToken = false; // 是否正在刷新中
const refreshToken = async (config) => {
// 如果当前已经是 refresh-token 的 URL 地址,并且还是 401 错误,说明是刷新令牌失败了,直接返回 Promise.reject(error)
if (config.url.indexOf('/member/auth/refresh-token') >= 0) {
return Promise.reject('error');
}
// 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
if (!isRefreshToken) {
isRefreshToken = true;
// 1. 如果获取不到刷新令牌,则只能执行登出操作
const refreshToken = getRefreshToken();
if (!refreshToken) {
return handleAuthorized();
}
// 2. 进行刷新访问令牌
try {
const refreshTokenResult = await AuthUtil.refreshToken(refreshToken);
if (refreshTokenResult.code !== 0) {
// 如果刷新不成功,直接抛出 e 触发 2.2 的逻辑
// noinspection ExceptionCaughtLocallyJS
throw new Error('刷新令牌失败');
}
// 2.1 刷新成功,则回放队列的请求 + 当前请求
config.header.Authorization = 'Bearer ' + getAccessToken();
requestList.forEach((cb) => {
cb();
});
requestList = [];
return request(config);
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb) => {
cb();
});
// 提示是否要登出。即不回放当前请求!不然会形成递归
return handleAuthorized();
} finally {
requestList = [];
isRefreshToken = false;
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
config.header.Authorization = 'Bearer ' + getAccessToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
resolve(request(config));
});
});
}
};
/**
* 处理 401 未登录的错误
*/
const handleAuthorized = () => {
const userStore = $store('user');
userStore.logout(true);
showAuthModal();
// 登录超时
return Promise.reject({
code: 401,
msg: userStore.isLogin ? '您的登陆已过期' : '请先登录',
});
};
/** 获得访问令牌 */
export const getAccessToken = () => {
return uni.getStorageSync('token');
};
/** 获得刷新令牌 */
export const getRefreshToken = () => {
return uni.getStorageSync('refresh-token');
};
const request = (config) => {
return http.middleware(config);
};
export default request;

185
sheep/router/index.js Normal file
View File

@@ -0,0 +1,185 @@
import $store from '@/sheep/store';
import { showAuthModal, showShareModal } from '@/sheep/hooks/useModal';
import { isNumber, isString, isEmpty, startsWith, isObject, isNil, clone } from 'lodash-es';
import throttle from '@/sheep/helper/throttle';
const _go = (
path,
params = {},
options = {
redirect: false,
},
) => {
let page = ''; // 跳转页面
let query = ''; // 页面参数
let url = ''; // 跳转页面完整路径
if (isString(path)) {
// 判断跳转类型是 path 还是http
if (startsWith(path, 'http')) {
// #ifdef H5
window.location = path;
return;
// #endif
// #ifndef H5
page = `/pages/public/webview`;
query = `url=${encodeURIComponent(path)}`;
// #endif
} else if (startsWith(path, 'action:')) {
handleAction(path);
return;
} else {
[page, query] = path.split('?');
}
if (!isEmpty(params)) {
let query2 = paramsToQuery(params);
if (isEmpty(query)) {
query = query2;
} else {
query += '&' + query2;
}
}
}
if (isObject(path)) {
page = path.url;
if (!isNil(path.params)) {
query = paramsToQuery(path.params);
}
}
const nextRoute = ROUTES_MAP[page];
// 未找到指定跳转页面
// mark: 跳转404页
if (!nextRoute) {
console.log(`%c跳转路径参数错误<${page || 'EMPTY'}>`, 'color:red;background:yellow');
return;
}
// 页面登录拦截
if (nextRoute.meta?.auth && !$store('user').isLogin) {
showAuthModal();
return;
}
url = page;
if (!isEmpty(query)) {
url += `?${query}`;
}
// 跳转底部导航
if (TABBAR.includes(page)) {
uni.switchTab({
url,
});
return;
}
// 使用redirect跳转
if (options.redirect) {
uni.redirectTo({
url,
});
return;
}
uni.navigateTo({
url,
});
};
// 限流 防止重复点击跳转
function go(...args) {
throttle(() => {
_go(...args);
});
}
function paramsToQuery(params) {
if (isEmpty(params)) {
return '';
}
// return new URLSearchParams(Object.entries(params)).toString();
let query = [];
for (let key in params) {
query.push(key + '=' + params[key]);
}
return query.join('&');
}
function back() {
// #ifdef H5
history.back();
// #endif
// #ifndef H5
uni.navigateBack();
// #endif
}
function redirect(path, params = {}) {
go(path, params, {
redirect: true,
});
}
// 检测是否有浏览器历史
function hasHistory() {
// #ifndef H5
const pages = getCurrentPages();
if (pages.length > 1) {
return true;
}
return false;
// #endif
// #ifdef H5
return !!history.state.back;
// #endif
}
function getCurrentRoute(field = '') {
let currentPage = getCurrentPage();
// #ifdef MP
currentPage.$page['route'] = currentPage.route;
currentPage.$page['options'] = currentPage.options;
// #endif
if (field !== '') {
return currentPage.$page[field];
} else {
return currentPage.$page;
}
}
function getCurrentPage() {
let pages = getCurrentPages();
return pages[pages.length - 1];
}
function handleAction(path) {
const action = path.split(':');
switch (action[1]) {
case 'showShareModal':
showShareModal();
break;
}
}
function error(errCode, errMsg = '') {
redirect('/pages/public/error', {
errCode,
errMsg,
});
}
export default {
go,
back,
hasHistory,
redirect,
getCurrentPage,
getCurrentRoute,
error,
};

View File

@@ -0,0 +1,79 @@
const singleComment = Symbol('singleComment');
const multiComment = Symbol('multiComment');
const stripWithoutWhitespace = () => '';
const stripWithWhitespace = (string, start, end) => string.slice(start, end).replace(/\S/g, ' ');
const isEscaped = (jsonString, quotePosition) => {
let index = quotePosition - 1;
let backslashCount = 0;
while (jsonString[index] === '\\') {
index -= 1;
backslashCount += 1;
}
return Boolean(backslashCount % 2);
};
export default function stripJsonComments(jsonString, { whitespace = true } = {}) {
if (typeof jsonString !== 'string') {
throw new TypeError(
`Expected argument \`jsonString\` to be a \`string\`, got \`${typeof jsonString}\``,
);
}
const strip = whitespace ? stripWithWhitespace : stripWithoutWhitespace;
let isInsideString = false;
let isInsideComment = false;
let offset = 0;
let result = '';
for (let index = 0; index < jsonString.length; index++) {
const currentCharacter = jsonString[index];
const nextCharacter = jsonString[index + 1];
if (!isInsideComment && currentCharacter === '"') {
const escaped = isEscaped(jsonString, index);
if (!escaped) {
isInsideString = !isInsideString;
}
}
if (isInsideString) {
continue;
}
if (!isInsideComment && currentCharacter + nextCharacter === '//') {
result += jsonString.slice(offset, index);
offset = index;
isInsideComment = singleComment;
index++;
} else if (isInsideComment === singleComment && currentCharacter + nextCharacter === '\r\n') {
index++;
isInsideComment = false;
result += strip(jsonString, offset, index);
offset = index;
continue;
} else if (isInsideComment === singleComment && currentCharacter === '\n') {
isInsideComment = false;
result += strip(jsonString, offset, index);
offset = index;
} else if (!isInsideComment && currentCharacter + nextCharacter === '/*') {
result += jsonString.slice(offset, index);
offset = index;
isInsideComment = multiComment;
index++;
continue;
} else if (isInsideComment === multiComment && currentCharacter + nextCharacter === '*/') {
index++;
isInsideComment = false;
result += strip(jsonString, offset, index + 1);
offset = index + 1;
continue;
}
}
return result + (isInsideComment ? strip(jsonString.slice(offset)) : jsonString.slice(offset));
}

View File

@@ -0,0 +1,103 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true,
});
const fs = require('fs');
import stripJsonComments from './strip-json-comments';
import { isArray, isEmpty } from 'lodash';
class TransformPages {
constructor({ includes, pagesJsonDir }) {
this.includes = includes;
this.uniPagesJSON = JSON.parse(stripJsonComments(fs.readFileSync(pagesJsonDir, 'utf-8')));
this.routes = this.getPagesRoutes().concat(this.getSubPackagesRoutes());
this.tabbar = this.getTabbarRoutes();
this.routesMap = this.transformPathToKey(this.routes);
}
/**
* 通过读取pages.json文件 生成直接可用的routes
*/
getPagesRoutes(pages = this.uniPagesJSON.pages, rootPath = null) {
let routes = [];
for (let i = 0; i < pages.length; i++) {
const item = pages[i];
let route = {};
for (let j = 0; j < this.includes.length; j++) {
const key = this.includes[j];
let value = item[key];
if (key === 'path') {
value = rootPath ? `/${rootPath}/${value}` : `/${value}`;
}
if (key === 'aliasPath' && i == 0 && rootPath == null) {
route[key] = route[key] || '/';
} else if (value !== undefined) {
route[key] = value;
}
}
routes.push(route);
}
return routes;
}
/**
* 解析小程序分包路径
*/
getSubPackagesRoutes() {
if (!(this.uniPagesJSON && this.uniPagesJSON.subPackages)) {
return [];
}
const subPackages = this.uniPagesJSON.subPackages;
let routes = [];
for (let i = 0; i < subPackages.length; i++) {
const subPages = subPackages[i].pages;
const root = subPackages[i].root;
const subRoutes = this.getPagesRoutes(subPages, root);
routes = routes.concat(subRoutes);
}
return routes;
}
getTabbarRoutes() {
if (!(this.uniPagesJSON && this.uniPagesJSON.tabBar && this.uniPagesJSON.tabBar.list)) {
return [];
}
const tabbar = this.uniPagesJSON.tabBar.list;
let tabbarMap = [];
tabbar.forEach((bar) => {
tabbarMap.push('/' + bar.pagePath);
});
return tabbarMap;
}
transformPathToKey(list) {
if (!isArray(list) || isEmpty(list)) {
return [];
}
let map = {};
list.forEach((i) => {
map[i.path] = i;
});
return map;
}
}
function uniReadPagesV3Plugin({ pagesJsonDir, includes }) {
let defaultIncludes = ['path', 'aliasPath', 'name'];
includes = [...defaultIncludes, ...includes];
let pages = new TransformPages({
pagesJsonDir,
includes,
});
return {
name: 'uni-read-pages-v3',
config(config) {
return {
define: {
ROUTES: pages.routes,
ROUTES_MAP: pages.routesMap,
TABBAR: pages.tabbar,
},
};
},
};
}
exports.default = uniReadPagesV3Plugin;

354
sheep/scss/_main.scss Normal file
View File

@@ -0,0 +1,354 @@
body {
color: var(--text-a);
background-color: var(--ui-BG-1) !important;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
/* ==================
初始化
==================== */
.ui-link {
cursor: pointer;
}
navigator {
display: inline-flex;
}
navigator.navigator-hover {
background-color: inherit;
transform: translate(1rpx, 1rpx);
// opacity: 1;
}
/* ==================
辅助类
==================== */
.none {
display: none !important;
}
.inline {
display: inline !important;
}
.inline-block {
display: inline-block !important;
}
.block {
display: block !important;
}
.touch-none {
pointer-events: none;
}
.touch-all {
pointer-events: all;
}
.flex {
display: flex !important;
}
.inline-flex {
display: inline-flex !important;
}
.w-100 {
width: 100%;
}
/* -- 浮动 -- */
.cf::after,
.cf::before {
content: '';
display: table;
}
.cf::after {
clear: both;
}
.fl {
float: left;
}
.fr {
float: right;
}
.position-center {
@include position-center;
}
.position-relative {
position: relative;
}
/* -- 工具类 -- */
@function negativify-map($map) {
$result: ();
@each $key, $value in $map {
@if $key != 0 {
$result: map-merge($result, ('n' + $key: (-$value)));
}
}
@return $result;
}
$utilities: () !default;
$utilities: map-merge(
(
'margin': (
responsive: true,
property: margin,
class: m,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-x': (
property: margin-right margin-left,
class: mx,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-y': (
property: margin-top margin-bottom,
class: my,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-top': (
property: margin-top,
class: mt,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-right': (
property: margin-right,
class: mr,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-bottom': (
property: margin-bottom,
class: mb,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'margin-left': (
property: margin-left,
class: ml,
values:
map-merge(
$spacers,
(
auto: auto,
)
),
),
'padding': (
responsive: true,
property: padding,
class: p,
values: $spacers,
),
'padding-x': (
property: padding-right padding-left,
class: px,
values: $spacers,
),
'padding-y': (
property: padding-top padding-bottom,
class: py,
values: $spacers,
),
'padding-top': (
property: padding-top,
class: pt,
values: $spacers,
),
'padding-right': (
property: padding-right,
class: pr,
values: $spacers,
),
'padding-bottom': (
property: padding-bottom,
class: pb,
values: $spacers,
),
'padding-left': (
property: padding-left,
class: pl,
values: $spacers,
),
'font-weight': (
property: font-weight,
class: text,
values: (
light: $font-weight-light,
lighter: $font-weight-lighter,
normal: $font-weight-normal,
bold: $font-weight-bold,
bolder: $font-weight-bolder,
),
),
'text-align': (
property: text-align,
class: text,
values: left right center,
),
'font-color': (
property: color,
class: text,
values:
map-merge(
$colors,
map-merge(
$grays,
map-merge(
$darks,
(
'reset': inherit,
)
)
)
),
),
'line-height': (
property: line-height,
class: lh,
values: (
1: 1,
sm: $line-height-sm,
base: $line-height-base,
lg: $line-height-lg,
),
),
'white-space': (
property: white-space,
class: text,
values: (
nowrap: nowrap,
),
),
'radius': (
property: border-radius,
class: radius,
values: (
null: $radius,
sm: $radius-sm,
lg: $radius-lg,
0: 0,
),
),
'round': (
property: border-radius,
class: round,
values: (
null: $round-pill,
circle: 50%,
),
),
'radius-top': (
property: border-top-left-radius border-top-right-radius,
class: radius-top,
values: (
null: $radius,
),
),
'radius-right': (
property: border-top-right-radius border-bottom-right-radius,
class: radius-right,
values: (
null: $radius,
),
),
'radius-bottom': (
property: border-bottom-right-radius border-bottom-left-radius,
class: radius-bottom,
values: (
null: $radius,
),
),
'radius-left': (
property: border-bottom-left-radius border-top-left-radius,
class: radius-left,
values: (
null: $radius,
),
),
'radius-lr': (
property: border-top-left-radius border-bottom-right-radius,
class: radius-lr,
values: (
null: $radius,
),
),
'radius-lrs': (
property: border-top-right-radius border-bottom-left-radius,
class: radius-lr,
values: (
null: 0,
),
),
'radius-rl': (
property: border-top-right-radius border-bottom-left-radius,
class: radius-rl,
values: (
null: $radius,
),
),
'radius-rls': (
property: border-top-left-radius border-bottom-right-radius,
class: radius-rl,
values: (
null: 0,
),
),
),
$utilities
);
@each $key, $utility in $utilities {
@if type-of($utility) == 'map' {
$values: map-get($utility, values);
@if type-of($values) == 'string' or type-of(nth($values, 1)) != 'list' {
$values: zip($values, $values);
}
@each $key, $value in $values {
$properties: map-get($utility, property);
@if type-of($properties) == 'string' {
$properties: append((), $properties);
}
$property-class: if(
map-has-key($utility, class),
map-get($utility, class),
nth($properties, 1)
);
$property-class: if($property-class == null, '', $property-class);
$property-class-modifier: if($key, if($property-class == '', '', '-') + $key, '');
.#{$property-class + $property-class-modifier} {
@each $property in $properties {
#{$property}: $value !important;
}
}
}
}
}

61
sheep/scss/_mixins.scss Normal file
View File

@@ -0,0 +1,61 @@
@mixin bg-square {
background: {
color: #fff;
image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%),
linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%);
size: 40rpx 40rpx;
position: 0 0, 20rpx 20rpx;
}
}
@mixin flex($direction: row) {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: $direction;
}
@mixin flex-bar {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin arrow {
content: '';
height: 0;
width: 0;
position: absolute;
}
@mixin arrow-top {
@include arrow;
// border-color: transparent transparent $ui-BG;
border-style: none solid solid;
border-width: 0 20rpx 20rpx;
}
@mixin arrow-right {
@include arrow;
// border-color: transparent $ui-BG transparent;
border-style: solid solid solid none;
border-width: 20rpx 20rpx 20rpx 0;
}
@mixin position-center {
position: absolute !important;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
@mixin blur {
-webkit-backdrop-filter: blur(20px);
backdrop-filter: blur(20px);
color: var(--ui-TC);
}

286
sheep/scss/_tools.scss Normal file
View File

@@ -0,0 +1,286 @@
/* ==================
常用工具
==================== */
.ss-bg-opactity-block {
background-color: rgba(#000, 0.2);
color: #fff;
}
/* ==================
flex布局
==================== */
.ss-flex {
display: flex;
flex-direction: row;
align-items: center;
}
.ss-flex-1 {
flex: 1;
}
.ss-flex-col {
display: flex;
flex-direction: column;
}
.ss-flex-wrap {
flex-wrap: wrap;
}
.ss-flex-nowrap {
flex-wrap: nowrap;
}
.ss-col-center {
align-items: center;
}
.ss-col-top {
align-items: flex-start;
}
.ss-col-bottom {
align-items: flex-end;
}
.ss-col-stretch {
align-items: stretch;
}
.ss-row-center {
justify-content: center;
}
.ss-row-left {
justify-content: flex-start;
}
.ss-row-right {
justify-content: flex-end;
}
.ss-row-between {
justify-content: space-between;
}
.ss-row-around {
justify-content: space-around;
}
.ss-self-start {
align-self: flex-start;
}
.ss-self-end {
align-self: flex-end;
}
.ss-self-center {
align-self: center;
}
.ss-h-100 {
height: 100%;
}
.ss-w-100 {
width: 100%;
}
/* ==================
margin padding: 内外边距
==================== */
@for $i from 0 through 100 {
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
// 得出u-margin-30或者u-m-30
.ss-margin-#{$i},
.ss-m-#{$i} {
margin: $i + rpx;
}
.ss-m-x-#{$i} {
margin-left: $i + rpx;
margin-right: $i + rpx;
}
.ss-m-y-#{$i} {
margin-top: $i + rpx;
margin-bottom: $i + rpx;
}
// 得出u-padding-30或者u-p-30
.ss-padding-#{$i},
.ss-p-#{$i} {
padding: $i + rpx;
}
.ss-p-x-#{$i} {
padding-left: $i + rpx;
padding-right: $i + rpx;
}
.ss-p-y-#{$i} {
padding-top: $i + rpx;
padding-bottom: $i + rpx;
}
@each $short, $long in l left, t top, r right, b bottom {
// 缩写版,结果如: u-m-l-30
// 定义外边距
.ss-m-#{$short}-#{$i} {
margin-#{$long}: $i + rpx;
}
// 定义内边距
.ss-p-#{$short}-#{$i} {
padding-#{$long}: $i + rpx;
}
// 完整版结果如u-margin-left-30
// 定义外边距
.ss-margin-#{$long}-#{$i} {
margin-#{$long}: $i + rpx;
}
// 定义内边距
.ss-padding-#{$long}-#{$i} {
padding-#{$long}: $i + rpx;
}
}
}
}
/* ==================
radius
==================== */
@for $i from 0 through 100 {
// 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 {
.ss-radius-#{$i},
.ss-r-#{$i} {
border-radius: $i + rpx;
}
.ss-r-t-#{$i} {
border-top-left-radius: $i + rpx;
border-top-right-radius: $i + rpx;
}
.ss-r-b-#{$i} {
border-bottom-left-radius: $i + rpx;
border-bottom-right-radius: $i + rpx;
}
@each $short, $long in tl 'top-left', tr 'top-right', bl 'bottom-right', br 'bottom-right' {
// 定义外边距
.ss-r-#{$short}-#{$i} {
border-#{$long}-radius: $i + rpx;
}
// 定义内边距
.ss-radius-#{$long}-#{$i} {
border-#{$long}-radius: $i + rpx;
}
}
}
}
/* ==================
溢出省略号
@param {Number} 行数
==================== */
@mixin ellipsis($rowCount: 1) {
// @if $rowCount <=1 {
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// } @else {
// min-width: 0;
// overflow: hidden;
// text-overflow: ellipsis;
// display: -webkit-box;
// -webkit-line-clamp: $rowCount;
// -webkit-box-orient: vertical;
// }
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: $rowCount;
-webkit-box-orient: vertical;
}
@for $i from 1 through 6 {
.ss-line-#{$i} {
@include ellipsis($i);
}
}
/* ==================
hover
==================== */
.ss-hover-class {
background-color: $gray-c;
opacity: 0.6;
}
.ss-hover-btn {
transform: translate(1px, 1px);
}
/* ==================
底部安全区域
==================== */
.ss-safe-bottom {
padding-bottom: 0;
padding-bottom: calc(constant(safe-area-inset-bottom) / 5 * 3);
padding-bottom: calc(env(safe-area-inset-bottom) / 5 * 3);
}
/* ==================
字体大小
==================== */
@for $i from 20 through 50 {
.ss-font-#{$i} {
font-size: $i + rpx;
}
}
/* ==================
按钮
==================== */
.ss-reset-button {
padding: 0;
margin: 0;
font-size: inherit;
background-color: transparent;
color: inherit;
position: relative;
border: 0rpx;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
align-items: center;
justify-content: center;
box-sizing: border-box;
text-align: center;
text-decoration: none;
white-space: nowrap;
vertical-align: baseline;
transform: translate(0, 0);
}
.ss-reset-button.button-hover {
transform: translate(1px, 1px);
background: none;
}
.ss-reset-button::after {
border: none;
}

163
sheep/scss/_var.scss Normal file
View File

@@ -0,0 +1,163 @@
@import './mixins';
//颜色 渐变背景60%
$yellow: #ffc300; //ss-黄
$orange: #ff6000; //ss-橘
$red: #ff3000; //ss-红
$pink: #e03997;
$mauve: #b745cb;
$purple: #652abf; //rgba(101, 42, 191, 1); // ss-紫
$blue: #0081ff;
$cyan: #37c0fe;
// $green: #2aae67; //ss-绿
$green: #00b85b; //ss-绿
$olive: #8dc63f;
$grey: #8799a3;
$brown: #a5673f;
$black: #484848; //ss-黑
$golden: #e9b461; //ss-金
$colors: ();
$colors: map-merge(
(
'yellow': $yellow,
'orange': $orange,
'red': $red,
'pink': $pink,
'mauve': $mauve,
'purple': $purple,
'violet': $purple,
'blue': $blue,
'cyan': $cyan,
'green': $green,
'olive': $olive,
'grey': $grey,
'brown': $brown,
'black': $black,
'golden': $golden,
),
$colors
);
//灰度
$bg-page: #f6f6f6;
$white: #ffffff;
$gray-f: #f8f9fa;
$gray-e: #eeeeee;
$gray-d: #dddddd;
$gray-c: #cccccc;
$gray-b: #bbbbbb;
$gray-a: #aaaaaa;
$dark-9: #999999;
$dark-8: #888888;
$dark-7: #777777;
$dark-6: #666666;
$dark-5: #555555;
$dark-4: #484848; //ss-黑
$dark-3: #333333;
$dark-2: #222222;
$dark-1: #111111;
$black: #000000;
$grays: ();
$grays: map-merge(
(
'white': $white,
'gray-f': $gray-f,
'gray-e': $gray-e,
'gray-d': $gray-d,
'gray-c': $gray-c,
'gray-b': $gray-b,
'gray-a': $gray-a,
'gray': $gray-a,
),
$grays
);
$darks: ();
$darks: map-merge(
(
'dark-9': $dark-9,
'dark-8': $dark-8,
'dark-7': $dark-7,
'dark-6': $dark-6,
'dark-5': $dark-5,
'dark-4': $dark-4,
'dark-3': $dark-3,
'dark-2': $dark-2,
'dark-1': $dark-1,
'black': $black,
),
$darks
);
// 边框
$border-width: 1rpx !default; // 边框大小
$border-color: $gray-d !default; // 边框颜色
// 圆角
$radius: 10rpx !default; // 默认圆角大小
$radius-lg: 40rpx !default; // 大圆角
$radius-sm: 6rpx !default; // 小圆角
$round-pill: 1000rpx !default; // 半圆
// 动画过渡
$transition-base: all 0.2s ease-in-out !default; // 默认过渡
$transition-base-out: all 0.04s ease-in-out !default; // 进场过渡
$transition-fade: opacity 0.15s linear !default; // 透明过渡
$transition-collapse: height 0.35s ease !default; // 收缩过渡
// 间距
$spacer: 20rpx !default;
$spacers: () !default;
$spacers: map-merge(
(
0: 0,
1: $spacer * 0.25,
2: $spacer * 0.5,
3: $spacer,
4: $spacer * 1.5,
5: $spacer * 3,
6: $spacer * 5,
),
$spacers
);
// 字形
$font-weight-lighter: lighter !default;
$font-weight-light: 300 !default;
$font-weight-normal: 400 !default;
$font-weight-bold: 700 !default;
$font-weight-bolder: 900 !default;
$fontsize: () !default;
$fontsize: map-merge(
(
xs: 20,
sm: 24,
df: 28,
lg: 32,
xl: 36,
xxl: 44,
sl: 80,
xsl: 120,
),
$fontsize
);
// 段落
$line-height-base: 1.5 !default;
$line-height-lg: 2 !default;
$line-height-sm: 1.25 !default;
// 图标
$iconsize: () !default;
$iconsize: map-merge(
(
xs: 0.5,
sm: 0.75,
df: 1,
lg: 1.25,
xl: 1.5,
xxl: 2,
sl: 6,
xsl: 10,
),
$iconsize
);

Binary file not shown.

File diff suppressed because one or more lines are too long

181
sheep/scss/icon/_icon.scss Normal file
View File

@@ -0,0 +1,181 @@
@font-face {
font-family: 'colorui'; /* Project id 2620914 */
src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAA08AAsAAAAAIIAAAAzuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACIUgqqHKM2ATYCJAOBJAtUAAQgBYR5B4MwGwIcs6JmclIAsv9LAT3W/EiCkXnK3Xny3Onomo8T7OwIi5b6OurgI7NQyMA0DecgDbMUXzZtybquth6v1ed4jzlbhWe8oZQGrbWZlVt/3xesuXQIRTwSkka/imlMNEIkda7mMvhA4790wiQPssgK0F3uNkCb2eyTq63TFabC13bqKnT/f9r2XRijHkYPBoxYQdioY200RxgzwE6Gn4NsYyX+qsD9VRvVQwAcs1sVVQBPpMFHVTiQ0SY0khn2Z8ycgyURv1jvJyjWdAPEHyCPBYW6EyErvLhvBgDU54DA7uCctjbtpwoHYQbOEsApsYk9Q2BggezBcZ+bo/+fmistkypBinEV7nXZqewmBwsHezD9/D5TnvCgwKyQciUAWeHIo6/ynRppC6ZrdCN/KHpRo/+MrZoHW6HrYDSjXzAweu/6SUCAN2ef1Ty/fTAGK8mlMeSwViRETblKImm0Hp1UQTJZa0G9Yhj8okHsxS/As/ft+UiSgYCgQ7AutDE508bX574s8v4P3VE/lPo82JkFQAMkYMowlxgUF7Bv2AGa8oiXfRpOYI4LXCTyWoFlG+zFKuqMab5PL8+X18tXf7FkeEnXczQarXBs0Sv6CQppD6S+92ewm2hT9tS1yjKWvNppv6d++Ar+Gf/X8z/lsbZxdLK1s1dSVdNxMFTWUeEFUVLR1dVTUCBBUloGZuYmVhILU0syUWhcBzJvvPJEcETWREBkQwRFjkRIptI4RLZETWRH1EX2RE+kBDEQaUFsRNrEjumAHsAcQE9ghqDPwNqjr2BGoO9gKqAfYKqgn2BqoF9g6qB/YBqg/xBpQhoRGZPGRLqkCZEeaRKj0QyYImgWTAY0ByYLmgeTAy2AyYMWgYGWgIG2wMxA22DmoDMwE9A1mBXoFkwCeoTIgv8GM/VoH+0swVlc5ok/igfKvyxXpsudskcG8ixSB8jocEkfJ6IThI+wkJDGpULiYQNCXq7Uqn0FpjTnzsr4aIpxIHfzRQpAVD23bETXqCa6tPK6zRhFqR+qD9RLM5Cjyzm3ZWSqD+e2dKdUpVStVqCcVVE4Kev1eHn5F5a7yUBScDhVqvfzsazL0czrlY407QMbxpfhPkHiWXd5Tgk771nuZqNCNp7T2HSoUDaYtDH1EEshIf2TcnB+gjWn++SFQt11rOufxDS/zcDpRUgt9wi7/3YS8vwnEddvEnT+OPpYYJjERIpR8cUD2Di5NDOC2FLH5/TJSLf4rpNHRzVwx2WWMzyRACnwdytZloIwScBjJbB0iE/ybmMSFHLKbupqsZvm2Jo8OtOPn5UgbpdKMEXLck6mEgz7zHC/lmW4fzUvh66me6Z9xvgBdcE+afyYIzMzWgiag8AdQ0IDoDpdaiSrlTUyp4h3GBgNxql/KnkE2VFmUsdEIGO3v7Pjm5GpNGKz45sX/021DR1Dig2386gn2PMeRtYS1JyPTq9ngOAToofLeXQ2uh6XsnhwHAmmyz9hlt3G1QBgO5xKYrkuJhDfzV+s8MKaPbsbvs9P0ef4Ib0SEG0A7y4DfAALepMfo8edgliTpS13fj94y+MnLUuH95qxPX/y4iVapVK+E9wyzSqmgZUAHsBKBg+ztxFDu11io9k6AXssDwBTw0Lq3GlhR8GuC0kFsuTJTinNTH8YzXDc+AzgdXYKmml5jZVLUHPqdjWWRkxiEwnQsdyvP/yzouBvFPmvQH9egEYtXLNe85Aew3+NUBlEyiJYZSZ9NfS3cMr4G7rhCJgml8yFcKlybogqj/VKxI3FkN8Znr5o8FZqpqbSuvOYpRnyJwt818FeEcfjp1LHbhsO7gAPfLt/+NIusKB3XzZc3XuQl9tzpbAJFehj3yOgf2S4t/XsDcfpY3bdAHW1GgGgR+2RxRV+p2DLZZVPBbjHONiPTjeQ5Gso+/LR2Wp2dhiqks+h7PtnQJmVFXRbi2N56tEZtx16JBumdNOeueP7W97VxpF9J0XS3NaDfHS2jQOpY6OBT9c/eh4jOl0xPPDU5ao3qG6qqjq3plKlrlFW17+h4oGjUqFWq3LVuarKgw1OE+VE87OhPE+9Kd1Ahs0BF+78MkxA44encRqfPowHj7ZCxQqOszhD4aDIkMCQTo8TC0xN7VffMHwX29/i/dF3OckiAtFOLbj7+64wnK6mllkgcP2QDiTajrvYmcUmbEsd1HXZtJVGXpWcXCXX/OyeJ1dVJWvkP2f0rY2kJl9GicWULN+MlJ1T4nyZmb1EKJ13fpMJH++JNWW19UjGwyUr9F0RlV6VvVKp7vJJM8+ZN+Z8NVLvOhZoroi+I2J4P+g/Di/GesJ2e5d/oufW1KvBif5du2yrUSTP2ZfDi3G9Csnih52StufbeuKMWS962doJGkXfi65X1hqdpyOuORnZ9cyiY0GNAjsHr2yQAEkOufEpROAEOjVMRIJABsGN6DtspT4lZQJ3be+VrMCJPSfpmCw0EDFka80QdCZsrK2sGb6QPxHpM088MOO0sat8ARBgqb1qeVVSUpVc/bNSdl6VpJb/zBevuH3Wl56z3Uz0Ukkkql4SlUriiy1uk83GAKbOMdnOqVJeZPgEPsno9cwQ1Un92sBX5i/qcnkSrV87OdFMz1hmHJVZy3zl2RF8fcNWA5tyw+sHsicRgdhlPECezfNccBelqdHRwnjtwoebyYLWjS4k/ahVlv334/VDlJPThW5PIExEwvbQp0/LEIGo8H/mdjTEvgjEOglNROL20P9yytCH9BDlTSW2sc5/MKcciQNMAWKkLB/0t1Y1PACR49Aco9z+mT9+eFo2fViG2bOKBYIk7sICiU9vxtPzOE4uaMIkb/emlgQk5H24X+ANMBxnw8lPUibmP1kaK4ZSLboDbIbrbHF+9tfPGtLKdEhHLoik55+9qjwh3p4yZS70bHlA6y1A+sFR1pSxz/5DQ5mmPVK8ElnT/FO71B/bHGBT3lQg17Nckr9qSGyoTxyeUmpuSJxicYbJxX6/WG+A2I82xsc7VQuqndiGlIByYlPl6C+QN7sFIkuXmE1RrGNtlGvypNsvXZpnFkL6z9z8PEm9fW0sEP3JlqmAOE7Z8ZgsWgBHNzrK8Bu2qlA0uFJ5RGcHAVYteO1XwceCX18jPX3QajWyPAlFSfJkZqVcUELJ8jS+RBLhm53V2Zk1jAQeDkh2jlVT3BpkGOmFZWEJjvvHXT67j1bdFQERyBUx27cTYq6FwjJnLXxPm8ZVKiqx8++3VY3z20d2Vb3AaydhdHQMBFi4C+NCHyISCzhrE7/Fis1v675sShv1TDZ5Lo02pf3pJXw9o/3SS9Kr9frTz9f5en/p5kti3vEqHLjkaUr2bKAD35zw2gEWSvOWpdNily6rwLo8URdQb1Fkh2QGBWWGZJtCsjJsZnB2sCkrGHCWZYrpZGYGZYUc4fb0HOWC8PXDl19av37D+pdeKigAN1FbN7AW7O2/+86asr7qHFUqIey25YQwTxe0143GawGM6Oru3XOYY2fPHBUxdpBCvz8CUYMZQRkzDCbAQfL4ibc5wMrfKJBnWi4lHxy7YUITzAntTjDAUw/TslY0hL1iP7SACb2KU05jZJKfwkSwXJAFOoulAQBg6dwTl4zTTjITdYo4+lAV+SPs2V4BYDTnQ6AzbZLUuhW+/T9WY5ZtGSCr4kXkcZ9yS1A5xe3EamQMA/CTWHldRm/AHf1YeOpFhwc+6FssOIY1QCrFveu+y4GTC1i+mUubpDDlv8+nhlXRTEWpo3wsQ6Jgrff322zCCv4v8jDHZenBNAxOvUKcqfrfciEwnpf9uxzZ4EHj3jTKBMDb8wH/d85p+B2WJZOhQC6hPAiCMSRIznCwCp8RtGBVKDhrgjfp5upB05yNYQdgwsdFoIZPQaj4EaSG72AV/idobf9CoZFhgrcX2LrBYCTzd04IJIMWwUokCsvQutxRmCOx5ncQzXJJjya/9AmoTigRE3+crb6AEqiei9TzaMocipCwEOca7w7yHEVFmIJiP2autkejUPYQvsLC6FhHgMSAJgRWYtSEgkohZ50kyh9sJP92d0BkJidRkZ+M+gSA6keIvRMmfGNK8AUpaT5Nal2bi0yxrYaE05mggnBuJAf5AxcqOUrf6ivMFyuUVbaN7JmFVOK3ayn2GMFK9Gt94uMZQElFzYKGlo6egZGJFVZZo2iG5XhBlGRF1XTDtGx2h9Pl9nh9tYYxqCzApe1YOzF9D13CWakbMRYwsOOe1gGdsKtyrKFLId7t6fsR97D7YZR6MInOzYVMOCmjtgdqlN4MKhtX8H7GAgkaGnJgaLkNPGxLravauoHqoc3rOEkZYuMqV2s/B5cTzqEhNHUzR6n1lzUUVqub0MWN7E0ANWqQpGInkCprhkgt34Z2JREu2pqw8jQuymbAZ5U7KNzRrbQ7XS/M99AwAA==')
format('woff2');
/* #ifdef MP-ALIPAY */
src: url('//at.alicdn.com/t/font_2620914_57y9q5zpbel.woff?t=1624238023908') format('woff'),
url('//at.alicdn.com/t/font_2620914_57y9q5zpbel.ttf?t=1624238023908') format('truetype');
/* #endif */
}
[class*='_icon-'] {
font-family: 'colorui' !important;
display: inline-block;
}
@font-face {
font-family: 'ui-num';
src: url('data:application/x-font-ttf;base64,AAEAAAAKAIAAAwAgT1MvMla+dCkAAACsAAAAYGNtYXAQUxhKAAABDAAAAVJnbHlmS86JUQAAAmAAAAUUaGVhZA7I1xIAAAd0AAAANmhoZWEFqgF3AAAHrAAAACRobXR4BycBzgAAB9AAAAAibG9jYQZmB5wAAAf0AAAAHG1heHAAEQBDAAAIEAAAACBuYW1lGVKlzAAACDAAAAGtcG9zdADDAJYAAAngAAAAPAAEAewBkAAFAAACmQLMAAAAjwKZAswAAAHrADMBCQAAAgAGAwAAAAAAAAAAAAEQAAAAAAAAAAAAAABQZkVkAMAALAA5Ayz/LABcAywA1AAAAAEAAAAAAxgAAAAAACAAAQAAAAMAAAADAAAAHAABAAAAAABMAAMAAQAAABwABAAwAAAACAAIAAIAAAAsAC4AOf//AAAALAAuADD////V/9T/0wABAAAAAAAAAAAAAAEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAgADBAUGBwgJCgsMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAiAAABMgKqAAMABwAANxEhESczESMiARDuzMwAAqr9ViICZgAAAAEAUP9hAMcAdwADAAAXNSMRx3c9tP7qAAEAUAAAAM0AfQADAAA3NSMVzX0AfX0AAAIAPv/6AeMC3wASACQAACUDJicmJwYHBgcRFhcWFzY3NjcHFAcGByYnJjURNDc2NxYXFhUB7wwCPDxZWTs7AwM7O1lZPDwOdB0bMzIbHBwbMjMbHdABPmM3NgEBNjdj/r1jNzYBATY3aAI2ICABASAgNgE9Nx8gAQEgHzcAAAAAAQB1AAABbALZAAYAACURIwcVNxEBbGmOjgAC2Xt0ff2ZAAAAAQBBAAAB6ALfAB4AACU1IRM2NzY1JicmJwYHBgczNjc2FxYXFhUUBwYHARUB6P7X5SIREQE5OV9fOjkCaAIfHywzGxwJCRX+6ABdARgoJCIvYDY2AQE3N189GhsBAR4dMxoYFhn+q10AAAAAAQAr//gB6QLgADUAACUmJyYnNjc2NSYnJicGBwYHMzY3NjMyFxYXFAcGByMVMxYXFhUGBwYjIicmJyMWFxY3Mjc2NwH1DRocLysYGAI5O15ZOzwGaQQcHTAuHh8BGxw4ERE+Hh4BISE0LyIhBWgGQD9aXkA/DtI+KioVFCcmOl03NwEBNDNeMRscHRw4Mh0eAVsBHyA4Oh8gGxk7azEyATU1bwABACQAAAH+AtkADgAAJTUjNSMVIwEjARUhFTM1Af5OZbUBAHH+/wEnZW5hqqoCCv32YW5uAAAAAAEAQf/5AewC2QA3AAAlJicmJyYnJiMiBwYHNSE1IREzNjc2NxYXFgcWBwYHBgcGIyInJicjFhcWFxYXFhc2NzY3Njc2NwH2Cg0MKBcgISsoHx8TASv+d18IGhosPRgWAQEHBhcOExMYMRkaBmgCDAwdFygoNDYmJRknDAwK+i4yMioXDAwLCxTBXf5yGxMSAQErKkIlIiIXDwcHGxkxJiQjHhgQDwEBDxEYKDAvQQAAAgA5//oB6ALZABcAKAAAJSYnJiciBwYHEyMDBgcGFRYXFhc2NzY3BwYHBgcmJyYnNjc2MxYXFhcB9A42NlERERAPnW+mGQ4QAjs7YGE6Og5rCh4eMzIdHgEBHh0yNR0eCd1cOTgBAgMGATn+ri8sLCxmOjkBATs8awJAISIBASIhOzshIgEjIzIAAAABAEEAAAHzAtkACAAAATUhFTM1MwMzAfP+TmTe9XECfF3Qc/2EAAAAAwAw//oB8gLfACAAMQBCAAAlJicmJzY3NjcmJyYnBgcGBxYXFhcGBwYHFhcWFzY3NjcnBgcGByYnJic2NzY3FhcWFwMGBwYHJicmJzY3NjcWFxYXAf4NHh4oJRkZAQI7PFxbOzwCARoZJCceHgECQD5gYT9ADmwLIiA1NCEhAQEhITQ1ICILDAoeHTEwHR0BAR0dMDEdHgrTOyoqFxUnJzpcNjYBATY2XDonJxUXKipAZTc3AQE3N2oCOSIiAQEiIjQ0IiMBASMiLwFKPh4eAQEeHjEyHh8BAR8eJQAAAAACADkAAAHoAt8AFwAoAAABJicmJwYHBgcWFxYXMjc2NwMzEzY3NjcHBgcGIyYnJjU2NzY3FhcWFwH0Djo7YWA6OwICNjZRERERDpxvphkODwxrCh4eMzQdHQEeHTIzHh4KAhJaOTkBATs8ZmE5OAEDAgb+xwFSLywsOQNHISIBIyM3OyIhAQEhIi8AAAEAAAABAADHiynwXw889QALBAAAAAAA1sTJ5wAAAADWxMntACL/YQH+AuAAAAAIAAIAAAAAAAAAAQAAAyz/LABcAiIAIgAkAf4AAQAAAAAAAAAAAAAAAAAAAAQBdgAiARcAUAEdAFACIgA+AHUAQQArACQAQQA5AEEAMAA5AAAAAAAUACAALABsAH4AtAEGASIBegHAAdQCRAKKAAEAAAANAEMAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAJYAAQAAAAAAAQAKAAAAAQAAAAAAAgAGAAoAAQAAAAAAAwAbABAAAQAAAAAABAAKACsAAQAAAAAABQAeADUAAQAAAAAABgAKAFMAAwABBAkAAQAUAF0AAwABBAkAAgAMAHEAAwABBAkAAwA2AH0AAwABBAkABAAUALMAAwABBAkABQA8AMcAAwABBAkABgAUAQNmb250ZWRpdG9yTWVkaXVtRm9udEVkaXRvciAxLjAgOiBmb250ZWRpdG9yZm9udGVkaXRvclZlcnNpb24gMS4wOyBGb250RWRpdG9yICh2MS4wKWZvbnRlZGl0b3IAZgBvAG4AdABlAGQAaQB0AG8AcgBNAGUAZABpAHUAbQBGAG8AbgB0AEUAZABpAHQAbwByACAAMQAuADAAIAA6ACAAZgBvAG4AdABlAGQAaQB0AG8AcgBmAG8AbgB0AGUAZABpAHQAbwByAFYAZQByAHMAaQBvAG4AIAAxAC4AMAA7ACAARgBvAG4AdABFAGQAaQB0AG8AcgAgACgAdgAxAC4AMAApAGYAbwBuAHQAZQBkAGkAdABvAHIAAAAAAgAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAADQANAAAADwARABMAFAAVABYAFwAYABkAGgAbABw=')
format('woff2');
font-weight: normal;
font-style: normal;
}
._icon-checkbox:before {
content: '\e713';
}
._icon-box:before {
content: '\e714';
}
._icon-checkbox-o:before {
content: '\e715';
}
._icon-round:before {
content: '\e716';
}
._icon-home-o:before {
content: '\e70a';
}
._icon-home:before {
content: '\e70d';
}
._icon-edit:before {
content: '\e649';
}
._icon-close:before {
content: '\e6ed';
}
._icon-check-round:before {
content: '\e6f1';
}
._icon-check-round-o:before {
content: '\e6f2';
}
._icon-close-round:before {
content: '\e6f3';
}
._icon-close-round-o:before {
content: '\e6f4';
}
._icon-waiting:before {
content: '\e6f8';
}
._icon-waiting-o:before {
content: '\e6f9';
}
._icon-warn:before {
content: '\e662';
}
._icon-warn-o:before {
content: '\e675';
}
._icon-more:before {
content: '\e688';
}
._icon-delete:before {
content: '\e707';
}
._icon-delete-o:before {
content: '\e709';
}
._icon-add-round:before {
content: '\e717';
}
._icon-add-round-o:before {
content: '\e718';
}
._icon-add:before {
content: '\e6e4';
}
._icon-info:before {
content: '\e6ef';
}
._icon-info-o:before {
content: '\e705';
}
._icon-move:before {
content: '\e768';
}
._icon-title:before {
content: '\e82f';
}
._icon-titles:before {
content: '\e745';
}
._icon-loading:before {
content: '\e746';
}
._icon-copy-o:before {
content: '\e7bc';
}
._icon-copy:before {
content: '\e85c';
}
._icon-loader:before {
content: '\e76d';
}
._icon-search:before {
content: '\e782';
}
._icon-back:before {
content: '\e600';
}
._icon-forward:before {
content: '\e601';
}
._icon-arrow:before {
content: '\e608';
}
._icon-drop-down:before {
content: '\e61c';
}
._icon-drop-up:before {
content: '\e61d';
}
._icon-check:before {
content: '\e69f';
}
._icon-move-round:before {
content: '\e602';
}
._icon-move-round-o:before {
content: '\e603';
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
@import './icon'; //核心图标库
@import './coloricon'; //扩展图标库
@import './sheepicon';
.icon-spin {
animation: icon-spin 2s infinite linear;
}
.icon-pulse {
animation: icon-spin 1s infinite steps(8);
}
@keyframes icon-spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.icon-90 {
transform: rotate(90deg);
}
.icon-180 {
transform: rotate(180deg);
}
.icon-270 {
transform: rotate(270deg);
}
.icon-x {
transform: scale(-1, 1);
}
.icon-y {
transform: scale(1, -1);
}
.icon-fw {
width: calc(18em / 14);
text-align: center;
}
@each $class, $value in $iconsize {
.icon-#{$class} {
transform: scale(#{$value});
}
}

27
sheep/scss/index.scss Normal file
View File

@@ -0,0 +1,27 @@
@import './tools';
@import './ui';
/* 字体文件 */
@font-face {
font-family: OPPOSANS;
src: url('~@/sheep/scss/font/OPPOSANS-M-subfont.ttf');
}
.font-OPPOSANS {
font-family: OPPOSANS;
}
page {
-webkit-overflow-scrolling: touch; // 解决ios滑动不流畅
height: 100%;
width: 100%;
// font-family: OPPOSANS;
word-break: break-all; //英文文本不换行
white-space: normal;
background-color: $bg-page;
color: $dark-3;
}
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
display: none;
}

View File

View File

@@ -0,0 +1,204 @@
/* ==================
背景
==================== */
/* -- 基础色 -- */
@each $color, $value in map-merge($colors, $darks) {
.bg-#{$color} {
background-color: $value !important;
@if $color == 'yellow' {
color: #333333 !important;
} @else {
color: #ffffff !important;
}
}
}
/* -- 浅色 -- */
@each $color, $value in $colors {
.bg-#{$color}-light {
background-image: linear-gradient(45deg, white, mix(white, $value, 85%)) !important;
color: $value !important;
}
.bg-#{$color}-thin {
background-color: rgba($value, var(--ui-BG-opacity)) !important;
color: $value !important;
}
}
/* -- 渐变色 -- */
@each $color, $value in $colors {
@each $colorsub, $valuesub in $colors {
@if $color != $colorsub {
.bg-#{$color}-#{$colorsub} {
// background-color: $value !important;
background-image: linear-gradient(130deg, $value, $valuesub) !important;
color: #ffffff !important;
}
}
}
}
.bg-yellow-gradient {
background-image: linear-gradient(45deg, #f5fe00, #ff6600) !important;
color: $dark-3 !important;
}
.bg-orange-gradient {
background-image: linear-gradient(90deg, #ff6000, #fe832a) !important;
color: $white !important;
}
.bg-red-gradient {
background-image: linear-gradient(45deg, #f33a41, #ed0586) !important;
color: $white !important;
}
.bg-pink-gradient {
background-image: linear-gradient(45deg, #fea894, #ff1047) !important;
color: $white !important;
}
.bg-mauve-gradient {
background-image: linear-gradient(45deg, #c01f95, #7115cc) !important;
color: $white !important;
}
.bg-purple-gradient {
background-image: linear-gradient(45deg, #9829ea, #5908fb) !important;
color: $white !important;
}
.bg-blue-gradient {
background-image: linear-gradient(45deg, #00b8f9, #0166eb) !important;
color: $white !important;
}
.bg-cyan-gradient {
background-image: linear-gradient(45deg, #06edfe, #48b2fe) !important;
color: $white !important;
}
.bg-green-gradient {
background-image: linear-gradient(45deg, #3ab54a, #8cc63f) !important;
color: $white !important;
}
.bg-olive-gradient {
background-image: linear-gradient(45deg, #90e630, #39d266) !important;
color: $white !important;
}
.bg-grey-gradient {
background-image: linear-gradient(45deg, #9aadb9, #354855) !important;
color: $white !important;
}
.bg-brown-gradient {
background-image: linear-gradient(45deg, #ca6f2e, #cb1413) !important;
color: $white !important;
}
@each $color, $value in $grays {
.bg-#{$color} {
background-color: $value !important;
color: #333333 !important;
}
}
.bg-square {
@include bg-square;
}
.bg-none {
background: transparent !important;
color: inherit !important;
}
[class*='bg-mask'] {
position: relative;
//background: transparent !important;
color: #ffffff !important;
> view,
> text {
position: relative;
z-index: 1;
color: #ffffff;
}
&::before {
content: '';
border-radius: inherit;
width: 100%;
height: 100%;
@include position-center;
background-color: rgba(0, 0, 0, 0.4);
z-index: 0;
}
@at-root .bg-mask-80::before {
background: rgba(0, 0, 0, 0.8) !important;
}
@at-root .bg-mask-50::before {
background: rgba(0, 0, 0, 0.5) !important;
}
@at-root .bg-mask-20::before {
background: rgba(0, 0, 0, 0.2) !important;
}
@at-root .bg-mask-top::before {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.618), rgba(0, 0, 0, 0.01));
}
@at-root .bg-white-top {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.3));
}
@at-root .bg-mask-bottom::before {
background-color: rgba(0, 0, 0, 0);
background-image: linear-gradient(rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 0.618), rgba(0, 0, 0, 1));
}
}
.bg-img {
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
[class*='bg-blur'] {
position: relative;
> view,
> text {
position: relative;
z-index: 1;
}
&::before {
content: '';
width: 100%;
height: 100%;
@include position-center;
border-radius: inherit;
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
}
}
@supports (-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px)) {
.bg-blur::before {
@include blur;
background-color: var(--ui-Blur-1);
}
.bg-blur-1::before {
@include blur;
background-color: var(--ui-Blur-2);
}
.bg-blur-2::before {
@include blur;
background-color: var(--ui-Blur-3);
}
}
@supports not (backdrop-filter: blur(5px)) {
.bg-blur {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG);
}
}
.bg-blur-1 {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG-1);
}
}
.bg-blur-2 {
color: var(--ui-TC);
&::before {
background-color: var(--ui-BG-2);
}
}
}

View File

@@ -0,0 +1,140 @@
/* ==================
边框
==================== */
/* -- 实线 -- */
.border {
overflow: initial !important;
@at-root [class*='border'],
[class*='dashed'] {
position: relative;
&.dline {
--ui-Border: var(--ui-BG-3);
}
&::after {
content: ' ';
width: 200%;
height: 200%;
position: absolute;
z-index: 0;
top: 0;
left: 0;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
border-radius: inherit;
}
&.radius::after {
border-radius: calc(#{$radius} * 2);
}
&.round::after {
border-radius: #{$round-pill};
}
}
&::after {
border: 1px solid var(--ui-Border);
}
&s::after {
border: 4rpx solid var(--ui-Border);
}
&ss::after {
border: 8rpx solid var(--ui-Border);
}
@each $value in (top, right, bottom, left) {
&-#{$value}::after {
border-#{$value}: 1px solid var(--ui-Border);
}
&s-#{$value}::after {
border-#{$value}: 4rpx solid var(--ui-Border);
}
&ss-#{$value}::after {
border-#{$value}: 8rpx solid var(--ui-Border);
}
}
}
/* -- 虚线 -- */
.dashed {
&::after {
border: 4rpx dashed var(--ui-Border);
}
&s::after {
border: 6rpx dashed var(--ui-Border);
}
@each $value in (top, right, bottom, left) {
&-#{$value}::after {
border-#{$value}: 4rpx dashed var(--ui-Border);
}
&s-#{$value}::after {
border-#{$value}: 6rpx dashed var(--ui-Border);
}
}
}
@each $color, $value in map-merge($colors, map-merge($darks, $grays)) {
.border-#{$color}::after,
.border-#{$color}[class*='-shine']::before {
border-color: $value !important;
}
}
@each $value in (a, b, c, d, e) {
.main-#{$value}-border::after,
.main-#{$value}-border[class*='-shine']::before {
border-color: var(--main-#{$value}) !important;
}
}
.dashed-shine,
.dasheds-shine {
position: relative;
overflow: hidden;
&::after,
&::before {
border-style: dashed;
border-color: var(--ui-Border);
animation: shineafter 1s infinite linear;
width: calc(200% + 40px);
height: 200%;
border-width: 2px 0;
}
&::before {
content: ' ';
position: absolute;
transform: scale(0.5);
transform-origin: 0 0;
pointer-events: none;
box-sizing: border-box;
animation: shinebefore 1s infinite linear;
width: 200%;
height: calc(200% + 40px);
border-width: 0 2px;
}
}
.dasheds-shine {
&::after,
&::before {
border-width: 4px 0;
}
&::before {
border-width: 0 4px;
}
}
@keyframes shineafter {
0% {
top: 0;
left: -22px;
}
100% {
top: 0px;
left: 0px;
}
}
@keyframes shinebefore {
0% {
top: -22px;
left: 0;
}
100% {
top: 0px;
left: 0px;
}
}

View File

@@ -0,0 +1,87 @@
.ui-btn-box {
display: inline-block;
}
.ui-btn {
position: relative;
border: 0rpx;
display: inline-block;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0.7857em 1.5em 0.7857em;
font-size: 28rpx;
line-height: 1em;
text-align: center;
text-decoration: none;
overflow: visible;
margin: 0 0.25em 0 0;
transform: translate(0rpx, 0rpx);
border-radius: $radius;
white-space: nowrap;
color: var(--text-a);
background-color: var(--ui-BG);
vertical-align: baseline;
&:first-child:last-child {
margin: 0;
}
&:not([class*='round'])::after {
border-radius: calc(#{$radius} * 2);
}
&:not([class*='border'])::after {
// content: ' ';
// width: 200%;
// height: 200%;
// display: block;
// position: absolute;
// z-index: 0;
// top: 0;
// left: 0;
// transform: scale(0.5);
// transform-origin: 0 0;
// pointer-events: none;
// box-sizing: border-box;
display: none;
}
&.round::after {
border-radius: #{$round-pill};
}
&.icon {
padding: 0.8em 0.8em;
}
&.sm {
font-size: 24rpx;
}
&.lg {
font-size: 32rpx;
}
&.xl {
font-size: 36rpx;
}
&.block {
width: 100%;
display: block;
font-size: 32rpx;
}
&[disabled] {
opacity: 0.6;
}
&.none-style {
background-color: transparent !important;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
}
}
.ui-btn:not(.icon) [class*='icon-'] {
margin: 0 0.25em;
}

353
sheep/scss/style/_card.scss Normal file
View File

@@ -0,0 +1,353 @@
/* ==================
卡片
==================== */
.ui-cards {
display: block;
overflow: hidden;
& .ui-btn.badge {
top: 0;
right: 0;
font-size: 24rpx;
padding: 0rpx 15rpx;
height: 40rpx;
}
&.no-card > .ui-item {
margin: 0rpx;
border-radius: 0rpx;
}
& > .ui-item {
display: block;
overflow: hidden;
border-radius: 10rpx;
margin: 30rpx;
}
& > .ui-item.shadow-blur {
overflow: initial;
}
.grid.grid-square {
margin-bottom: -20rpx;
}
&.article {
display: block;
& > .ui-item {
padding: 30rpx;
background-color: var(--box-bg);
display: flex;
align-items: flex-start;
}
& > .time {
padding: 30rpx 0 0 30rpx;
}
& > .ui-item .title {
font-size: 30rpx;
font-weight: 900;
color: #333333;
}
& > .ui-item .content {
flex: 1;
}
& > .ui-item > image {
width: 240rpx;
height: 6.4em;
margin-left: 20rpx;
border-radius: 6rpx;
}
& > .ui-item .content .desc {
font-size: 12px;
color: var(--text-c);
}
& > .ui-item .content .text-content {
font-size: 28rpx;
color: #888;
}
}
&.case {
.image {
position: relative;
image {
width: 100%;
display: block;
}
.ui-tag {
position: absolute;
right: 0;
top: 0;
}
.ui-bar {
position: absolute;
bottom: 0;
width: 100%;
background-color: transparent;
padding: 0rpx 30rpx;
}
.bg-black {
position: absolute;
bottom: 0;
width: 100%;
background-color: rgba(0, 0, 0, 0.6);
}
}
&.no-card .image {
margin: 30rpx 30rpx 0;
overflow: hidden;
border-radius: 10rpx;
}
}
&.dynamic {
display: block;
& > .ui-item {
display: block;
overflow: hidden;
& > .text-content {
padding: 0 30rpx 0;
font-size: 30rpx;
margin-bottom: 20rpx;
}
& .square-img {
width: 100%;
height: 200rpx;
border-radius: 6rpx;
}
& .only-img {
width: 100%;
height: 320rpx;
border-radius: 6rpx;
}
}
}
&.goods {
display: block;
& > .ui-item {
padding: 30rpx;
display: flex;
position: relative;
background-color: var(--ui-BG);
& + .ui-item {
border-top: 1rpx solid #eeeeee;
}
.content {
width: 410rpx;
padding: 0rpx;
}
.title {
font-size: 30rpx;
font-weight: 900;
color: #333333;
line-height: 1.4;
height: 1.4em;
overflow: hidden;
}
}
&.col-goods.col-twice {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(50% - 30rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
.content {
padding: 20rpx;
}
}
& > .ui-item:nth-child(2n) {
margin-left: 0rpx;
}
}
&.col-goods > .ui-item {
padding: 0rpx;
display: block;
border: 0px;
.content {
width: 100%;
padding: 30rpx;
}
}
&.no-card > .ui-item .content {
width: 470rpx;
padding: 0rpx;
}
&.no-card > .ui-item .title,
&.col-goods > .ui-item .title {
height: 3em;
overflow: hidden;
}
& > .ui-item .text-linecut-2 {
-webkit-line-clamp: 1;
}
&.no-card > .ui-item .text-linecut-2,
&.col-goods > .ui-item .text-linecut-2 {
-webkit-line-clamp: 2;
line-height: 1.6em;
height: 3.2em;
}
& > .ui-item > image {
width: 200rpx;
height: 200rpx;
margin-right: 20rpx;
border-radius: 6rpx;
}
&.no-card > .ui-item > image {
width: 220rpx;
height: 170rpx;
}
&.col-goods > .ui-item > image {
width: 100%;
height: 340rpx;
border-bottom-left-radius: 0rpx;
border-bottom-right-radius: 0rpx;
display: block;
}
&.col-goods.col-twice > .ui-item > image {
height: 236rpx;
}
}
&.loan {
display: block;
& > .ui-item {
padding: 30rpx 0 30rpx 30rpx;
display: flex;
position: relative;
background-color: var(--box-bg);
.content {
width: 450rpx;
padding: 0rpx;
.tag-list {
width: 450rpx;
display: flex;
flex-wrap: wrap;
font-size: 12px;
margin-top: 18rpx;
}
}
.action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
}
}
&.houses {
display: block;
& > .ui-item {
padding: 20rpx;
display: flex;
position: relative;
background-color: var(--box-bg);
.image {
width: 230rpx;
height: 180rpx;
margin-right: 20rpx;
border-radius: 6rpx;
}
.content {
width: 400rpx;
padding: 0rpx;
.tag-list {
width: 400rpx;
display: flex;
flex-wrap: wrap;
font-size: 12px;
margin-top: 10rpx;
.ui-item {
height: 20px;
line-height: 20px;
}
}
}
.action {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
}
}
&.product {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(100% - 15rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
background-color: var(--box-bg);
position: relative;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
// display: flex;
// flex-wrap: wrap;
.content {
padding: 20rpx;
// width: calc(100% - 345rpx);
.text-cut {
font-size: 16px;
}
}
.image {
width: 100%;
height: 240rpx;
border-radius: 6rpx 0 0 6rpx;
display: block;
}
.ui-progress-tag {
width: 4em;
text-align: right;
font-size: 12px;
}
.border-top {
width: 100%;
}
.ui-tag {
position: absolute;
top: 0;
left: 0;
border-radius: 6rpx 0 6rpx 0;
}
}
// & > .ui-item:nth-child(2n) {
// margin-left: 0rpx;
// }
}
&.shop {
display: flex;
flex-wrap: wrap;
padding-bottom: 30rpx;
& > .ui-item {
width: calc(50% - 30rpx);
margin: 20rpx 20rpx 0rpx 20rpx;
background-color: var(--box-bg);
padding: 20rpx;
.content {
margin-top: 15rpx;
}
.image {
width: 100%;
height: 285rpx;
border-radius: 6rpx;
display: block;
}
}
& > .ui-item:nth-child(2n) {
margin-left: 0rpx;
}
}
&.orders .ui-item {
margin-top: 30rpx;
.address-box {
padding: 15rpx;
margin: 0 30rpx 30rpx;
border: 1px solid;
border-color: var(--main-a);
border-radius: 10px;
position: relative;
.ui-form-group {
min-height: 10px;
}
}
}
}

View File

@@ -0,0 +1,55 @@
.ui-code {
font-family: Monaco, Menlo, Consolas, 'Courier New';
font-size: 90%;
position: relative;
z-index: 1;
color: var(--ui-TC);
.ui-rich-text {
display: inline-block;
}
&.code {
display: inline-block;
padding: 0 10rpx;
margin: 0 10rpx;
border-radius: $radius-sm;
line-height: 1.6;
vertical-align: baseline;
}
&.pre {
display: block;
margin: 1em 0;
line-height: 1.6;
&.hasTitle {
margin: 3.2em 0 1em;
}
// border-radius: $radius-sm;
.ui-code-title {
position: absolute;
top: -2.2em;
color: var(--ui-TC-2);
left: 0;
}
.ui-rich-text {
padding: 40rpx;
white-space: pre-wrap;
word-break: break-all;
word-wrap: break-word;
}
.ui-scroll-view {
&.ui-scroll {
max-height: 500px;
white-space: pre;
}
}
.ui-copy-btn {
position: absolute;
z-index: 2;
top: 0;
right: 0;
padding: 0.8em;
border-radius: 0 $radius-sm 0 $radius-sm;
}
}
}

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