This commit is contained in:
Ankkaya 2024-04-30 17:45:03 +08:00
commit 035b98b15a
57 changed files with 6083 additions and 0 deletions

22
.env Normal file
View File

@ -0,0 +1,22 @@
# 版本号
MALL_VERSION = v1.0.0
# 后端接口 - 正式环境(通过 process.env.NODE_ENV 非 development
# MALL_BASE_URL = http://api-dashboard.yudao.iocoder.cn
# 后端接口 - 测试环境(通过 process.env.NODE_ENV = development
# MALL_DEV_BASE_URL = http://127.0.0.1:48080
# MALL_DEV_BASE_URL = https://mall-backend-local.jiandyb.cn
MALL_DEV_BASE_URL = http://mall-backend-dev.jiandyb.cn:7001
# 后端接口前缀(一般不建议调整)
MALL_API_PATH = /app-api
# 开发环境运行端口
MALL_DEV_PORT = 3000
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
MALL_STATIC_URL = https://file.sheepjs.com
# 是否开启直播 1 开启直播 | 0 关闭直播 (小程序官方后台未审核开通直播权限时请勿开启)
MALL_MPLIVE_ON = 0

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
unpackage/*
node_modules/*
.idea/*
deploy.sh
.hbuilderx/
.vscode/
**/.DS_Store
package-lock.json
*.keystore

23
App.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup>
import { onLaunch, onShow, onError } from '@dcloudio/uni-app'
import { peachInit } from './peach'
onLaunch(() => {
// uni.hideTabBar()
peachInit()
})
onError((err) => {
console.log('AppOnError:', err)
})
onShow((options) => {
console.log('AppOnShow:', options)
})
</script>
<style lang="scss">
/*每个页面公共css */
@import '@/peach/scss/index.scss';
</style>

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

12
main.js Normal file
View File

@ -0,0 +1,12 @@
import App from './App'
import { createSSRApp } from 'vue'
import { setupPinia } from './peach/store'
export function createApp() {
const app = createSSRApp(App)
setupPinia(app)
return {
app,
}
}

72
manifest.json Normal file
View File

@ -0,0 +1,72 @@
{
"name" : "商城商家端",
"appid" : "__UNI__4A6B7A8",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "mall-app-b",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Ankkaya",
"license": "ISC",
"dependencies": {
"dayjs": "^1.11.10",
"lodash": "^4.17.21",
"luch-request": "^3.1.1",
"pinia": "^2.1.7",
"pinia-plugin-persist-uni": "^1.3.1"
},
"devDependencies": {
"vconsole": "^3.15.1"
}
}

31
pages.json Normal file
View File

@ -0,0 +1,31 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^p-(.*)": "@/peach/components/p-$1/p-$1.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
},
"meta": {
"auth": false,
"sync": true,
"title": "首页",
"group": "商家端"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationStyle": "custom"
},
"uniIdRouter": {}
}

12
pages/index/index.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<p-layout title="首页" navbar="normal" tabbar="/pages/index/index" :bgStyle="bgStyle"></p-layout>
</template>
<script setup>
const bgStyle = {
backgroundImage:
'http://101.43.181.163:9001/mall-backend/24dd085ca57fbfaa27c3e16788237b1d7a95c854c01b5e3e219aad6709bbb748.jpg',
backgroundColor: '',
description: '',
}
</script>

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

@ -0,0 +1,132 @@
import request from '@/peach/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

84
peach/api/member/user.js Normal file
View File

@ -0,0 +1,84 @@
import request from '@/peach/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: '修改成功',
},
})
},
// 重置密码
resetUserPassword: (data) => {
return request({
url: '/member/user/reset-password',
method: 'PUT',
data,
custom: {
loadingMsg: '验证中',
showSuccess: true,
successMsg: '修改成功',
},
})
},
}
export default UserApi

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

@ -0,0 +1,68 @@
import request from '@/peach/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,101 @@
import request from '@/peach/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,
},
})
},
// 获得匹配指定商品的优惠劵列表
getMatchCouponList: (price, spuIds, skuIds, categoryIds) => {
return request({
url: '/promotion/coupon/match-list',
method: 'GET',
params: {
price,
spuIds: spuIds.join(','),
skuIds: skuIds.join(','),
categoryIds: categoryIds.join(','),
},
custom: {
showError: false,
showLoading: false, // 避免影响 settlementOrder 结算的结果
},
})
},
}
export default CouponApi

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

@ -0,0 +1,50 @@
import request from '@/peach/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

139
peach/api/trade/order.js Normal file
View File

@ -0,0 +1,139 @@
import request from '@/peach/request'
const OrderApi = {
// 计算订单信息
settlementOrder: (data) => {
const data2 = {
...data,
}
// 移除多余字段
if (!(data.couponId > 0)) {
delete data2.couponId
}
if (!(data.addressId > 0)) {
delete data2.addressId
}
if (!(data.combinationActivityId > 0)) {
delete data2.combinationActivityId
}
if (!(data.combinationHeadId > 0)) {
delete data2.combinationHeadId
}
if (!(data.seckillActivityId > 0)) {
delete data2.seckillActivityId
}
// 解决 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,
},
})
},
// 创建订单
createOrder: (data) => {
return request({
url: `/trade/order/create`,
method: 'POST',
data,
})
},
// 获得订单
getOrder: (id) => {
return request({
url: `/trade/order/get-detail`,
method: 'GET',
params: {
id,
},
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) => {
return request({
url: `/trade/order/cancel`,
method: 'DELETE',
params: {
id,
},
})
},
// 删除订单
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,
})
},
}
export default OrderApi

View File

@ -0,0 +1,209 @@
<template>
<view class="ui-fixed">
<view
class="ui-fixed-box"
:id="`fixed-${uuid}`"
:class="[{ fixed: state.fixed }]"
:style="[
{
left: sticky ? 'auto' : '0px',
top: state.fixed && !bottom ? (noNav ? val : val + sys_navBar) + 'px' : 'auto',
bottom: insetHeight,
zIndex: index + peach.$zIndex.navbar,
},
!alway ? { opacity: state.opacityVal } : '',
]"
>
<view class="ui-fixed-content" @tap="toTop" :style="[{ zIndex: index + peach.$zIndex.navbar }]">
<slot></slot>
<view
v-if="safeAreaInsets.bottom && bottom && isInset"
class="inset-bottom"
:style="[{ height: safeAreaInsets.bottom + 'px' }]"
></view>
</view>
<view class="ui-fixed-bottom" :class="[bg]" v-if="bottom"></view>
<view
class="ui-fixed-bg"
:class="[ui, bg]"
:style="[
{ zIndex: index + peach.$zIndex.navbar - 1 },
bgStyles,
opacity ? { opacity: state.opacityVal } : '',
]"
></view>
</view>
<view
class="skeleton"
:style="[{ height: state.content.height + 'px', width: width + 'px' }]"
v-if="sticky ? state.fixed : placeholder && state.fixed"
></view>
</view>
</template>
<script setup>
import { onPageScroll } from '@dcloudio/uni-app'
import { getCurrentInstance, unref, onMounted, reactive, nextTick, computed } from 'vue'
import peach from '@/peach'
const { safeAreaInsets } = peach.$platform.device
const vm = getCurrentInstance()
const uuid = peach.$helper.guid()
const sys_navBar = peach.$platform.navbar
const state = reactive({
content: {},
fixed: true,
scrollTop: 0,
opacityVal: 0,
})
const insetHeight = computed(() => {
if (state.fixed && props.bottom) {
if (props.isInset) {
return props.val + 'px'
} else {
return props.val + safeAreaInsets.bottom + 'px'
}
} else {
return 'auto'
}
})
const props = defineProps({
noNav: {
type: Boolean,
default: false,
},
bottom: {
type: Boolean,
default: false,
},
bg: {
type: String,
default: '',
},
bgStyles: {
type: Object,
default() {},
},
val: {
type: Number,
default: 0,
},
width: {
type: [String, Number],
default: 0,
},
alway: {
type: Boolean,
default: true,
},
opacity: {
type: Boolean,
default: false,
},
index: {
type: [Number, String],
default: 0,
},
placeholder: {
type: [Boolean],
default: false,
},
sticky: {
type: [Boolean],
default: false,
},
noFixed: {
type: Boolean,
default: false,
},
ui: {
type: String,
default: '',
},
clickTo: {
type: Boolean,
default: false,
},
//
isInset: {
type: Boolean,
default: true,
},
})
state.fixed = !unref(props.sticky)
onPageScroll((e) => {
let top = e.scrollTop
state.scrollTop = top
state.opacityVal = top > peach.$platform.navbar ? 1 : top * 0.01
})
onMounted(() => {
nextTick(() => {
computedQuery()
})
})
const computedQuery = () => {
uni.createSelectorQuery()
.in(vm)
.select(`#fixed-${uuid}`)
.boundingClientRect((data) => {
if (data != null) {
state.content = data
if (unref(props.sticky)) {
setFixed(state.scrollTop)
}
}
})
.exec()
}
const setFixed = (value) => {
if (unref(props.bottom)) {
state.fixed =
value >=
state.content.bottom - peach.$platform.device.windowHeight + state.content.height + unref(props.val)
} else {
state.fixed =
value >=
state.content.top - (unref(props.noNav) ? unref(props.val) : unref(props.val) + peach.$platform.navbar)
}
}
const toTop = () => {
if (props.hasToTop) {
uni.pageScrollTo({
scrollTop: state.content.top,
duration: 100,
})
}
}
</script>
<style lang="scss">
.ui-fixed {
.ui-fixed-box {
position: relative;
width: 100%;
&.fixed {
position: fixed;
}
.ui-fixed-content {
position: relative;
}
.ui-fixed-bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 1;
pointer-events: none;
}
}
}
.inset-bottom {
background: #fff;
}
</style>

View File

@ -0,0 +1,362 @@
<template>
<p-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"
>
<p-status-bar />
<view class="ui-navbar-box">
<view
class="ui-bar ss-p-x-20"
:class="state.isDark ? 'text-white' : 'text-black'"
:style="[{ height: sys_navBar - sys_statusBar + 'px' }]"
>
<view class="icon-box ss-flex">
<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>
<slot name="center">
<view class="center navbar-title">{{ title }}</view>
</slot>
<!-- #ifdef MP -->
<view :style="[state.capsuleStyle]"></view>
<!-- #endif -->
</view>
</view>
</p-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} noFixed = false - 是否浮动
* @param {String} ui = '' - 公共Class
* @param {Boolean} capsule = false - 是否开启胶囊返回
* @param {Boolean} stopBack = false - 是否禁用返回
* @param {Boolean} placeholder = true - 是否开启占位
* @param {Object} bgStyles = {} - 背景样式
*
*/
import { reactive, onBeforeMount, ref } from 'vue'
import peach from '@/peach'
import { onPageScroll } from '@dcloudio/uni-app'
import { showMenuTools } from '@/peach/hooks/useModal'
//
const state = reactive({
statusCur: '',
capsuleStyle: {},
capsuleBack: {},
isDark: true,
})
const sys_statusBar = peach.$platform.device.statusBarHeight
const sys_navBar = peach.$platform.navbar
const props = defineProps({
zIndex: {
type: Number,
default: 100,
},
title: {
//
type: String,
default: '',
},
bg: {
type: String,
default: 'bg-white',
},
//
alway: {
type: Boolean,
default: true,
},
opacity: {
//
type: Boolean,
default: true,
},
noFixed: {
//
type: Boolean,
default: true,
},
ui: {
type: String,
default: '',
},
capsule: {
//
type: Boolean,
default: false,
},
stopBack: {
type: Boolean,
default: false,
},
placeholder: {
type: [Boolean],
default: false,
},
bgStyles: {
type: Object,
default() {},
},
})
const emits = defineEmits(['navback', 'clickLeft'])
const hasHistory = peach.$router.hasHistory()
onBeforeMount(() => {
init()
})
onPageScroll((e) => {
let top = e.scrollTop
state.isDark = top < peach.$platform.navbar
})
function onClickLeft() {
if (hasHistory) {
peach.$router.back()
} else {
peach.$router.go('/pages/index/index')
}
emits('clickLeft')
}
function onClickRight() {
showMenuTools()
}
//
const init = () => {
// #ifdef MP-ALIPAY
my.hideAllFavoriteMenu()
// #endif
state.capsuleStyle = {
width: peach.$platform.capsule.width + 'px',
height: peach.$platform.capsule.height + 'px',
}
state.capsuleBack = state.capsuleStyle
}
</script>
<style lang="scss" scoped>
.icon-box {
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;
border: 1px solid rgba(#fff, 0.4);
.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;
}
}
}
.navbar-title {
font-size: 36rpx;
}
.tools-icon {
font-size: 40rpx;
}
.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;
position: absolute;
left: 50%;
transform: translateX(-50%);
.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,215 @@
<template>
<view class="page-app" :class="['theme-' + sys.mode, 'main-' + sys.theme, 'font-' + sys.fontSize]">
<view class="page-main" :style="[bgMain]">
<!-- 顶部导航栏-情况1默认通用顶部导航栏 -->
<p-navbar
v-if="navbar === 'normal'"
:title="title"
statusBar
:color="color"
:tools="tools"
:opacityBgUi="opacityBgUi"
@search="(e) => emits('search', e)"
:defaultSearch="defaultSearch"
/>
<view class="page-body" :style="[bgBody]">
<!-- 顶部导航栏-情况2沉浸式头部 -->
<p-inner-navbar v-if="navbar === 'inner'" :title="title" />
<view v-if="navbar === 'inner'" :style="[{ paddingTop: peach.$platform.navbar + 'px' }]"></view>
<!-- 页面内容插槽 -->
<slot />
<!-- 底部导航 -->
<s-tabbar v-if="tabbar !== ''" :path="tabbar" />
</view>
</view>
<view class="page-modal">
<!-- 全局授权弹窗 -->
<!-- <p-auth-modal /> -->
<!-- 全局分享弹窗 -->
<!-- <p-share-modal :shareInfo="shareInfo" /> -->
<!-- 全局快捷入口 -->
<!-- <p-menu-tools /> -->
</view>
</view>
</template>
<script setup>
/**
* 模板组件 - 提供页面公共组件属性方法
*/
import { computed, reactive, ref } from 'vue'
import peach from '@/peach'
import { isEmpty } from 'lodash'
import { onShow } from '@dcloudio/uni-app'
// #ifdef MP-WEIXIN
import { onShareAppMessage } from '@dcloudio/uni-app'
// #endif
const props = defineProps({
title: {
type: String,
default: '',
},
navbar: {
type: String,
default: 'normal',
},
opacityBgUi: {
type: String,
default: 'bg-white',
},
color: {
type: String,
default: '',
},
tools: {
type: String,
default: 'title',
},
keyword: {
type: String,
default: '',
},
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 = peach.$store('sys')
const userStore = peach.$store('user')
const appStore = peach.$store('app')
const modalStore = peach.$store('modal')
const sys = computed(() => sysStore)
// ( )
const navbarMode = computed(() => {
if (props.navbar === 'normal') {
return 'normal'
}
return 'inner'
})
// 1
const bgMain = computed(() => {
if (navbarMode.value === 'inner') {
return {
background: `${props.bgStyle.backgroundColor} url(${peach.$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(${peach.$url.cdn(
props.bgStyle.backgroundImage
)}) no-repeat top center / 100% auto`,
}
}
return {}
})
//
const shareInfo = computed(() => {
if (props.onShareAppMessage === true) {
return peach.$platform.share.getShareInfo()
} else {
if (!isEmpty(props.onShareAppMessage)) {
peach.$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)) {
peach.$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,433 @@
<!-- 自定义导航栏 -->
<template>
<view class="uni-navbar" :class="{ 'uni-dark': dark }">
<view
:class="{
'uni-navbar--fixed': fixed,
'uni-navbar--shadow': shadow,
'uni-navbar--border': border,
}"
class="uni-navbar__content"
>
<view class="fixed-bg" :class="[opacity ? '' : opacityBgUi]"></view>
<p-status-bar v-if="statusBar" />
<view
:style="{
color: themeColor,
height: navbarHeight,
background: backgroundColor,
}"
class="uni-navbar__header"
>
<view class="uni-navbar__header-btns uni-navbar__header-btns-left" :style="{ width: leftIconWidth }">
<slot name="left">
<view class="uni-navbar__content_view" v-if="leftIcon.length > 0">
<view class="icon-box ss-flex">
<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="showMenuTools">
<text class="sicon-more" />
</view>
</view>
</view>
<view
:class="{ 'uni-navbar-btn-icon-left': !leftIcon.length > 0 }"
class="uni-navbar-btn-text"
v-if="
titleAlign === 'left' &&
title.length &&
peach.$platform.name !== 'WechatOfficialAccount'
"
>
<text :style="{ color: themeColor, fontSize: '18px' }">{{ title }}</text>
</view>
</slot>
</view>
<view v-if="tools === 'search'" class="ss-flex-1">
<slot name="center">
<uni-search-bar
class="ss-flex-1 search-box"
:radius="20"
placeholder="请输入关键词"
cancelButton="none"
v-model="searchModel"
@confirm="onSearch"
/>
</slot>
</view>
<view v-else class="uni-navbar__header-container" @tap="onClickTitle">
<slot name="center">
<view
v-if="tools === 'title' && titleAlign === 'center' && title.length"
class="uni-navbar__header-container-inner"
>
<text :style="{ color: themeColor, fontSize: '36rpx' }" class="ss-line-1">{{ title }}</text>
</view>
</slot>
</view>
</view>
</view>
<view class="uni-navbar__placeholder" v-if="placeholder">
<p-status-bar v-if="statusBar" />
<view class="uni-navbar__placeholder-view" :style="{ height: navbarHeight }" />
</view>
</view>
</template>
<script setup>
import peach from '@/peach'
import { onLoad } from '@dcloudio/uni-app'
import { showMenuTools, closeMenuTools } from '@/peach/hooks/useModal'
import { computed } from 'vue'
/**
* NavBar 自定义导航栏
* @description 导航栏组件主要用于头部导航
* @property {Boolean} dark 开启黑暗模式
* @property {String} title 标题文字
* @property {String} rightText 右侧按钮文本
* @property {String} leftIcon 左侧按钮图标
* @property {String} rightIcon 右侧按钮图标
* @property {String} color 图标和文字颜色
* @property {String} backgroundColor 导航栏背景颜色
* @property {Boolean} fixed = [true|false] 是否固定顶部
* @property {Boolean} statusBar = [true|false] 是否包含状态栏
* @property {Boolean} shadow = [true|false] 导航栏下是否有阴影
* @event {Function} clickLeft 左侧按钮点击时触发
* @event {Function} clickRight 右侧按钮点击时触发
* @event {Function} clickTitle 中间标题点击时触发
*/
const getVal = (val) => (typeof val === 'number' ? val + 'px' : val)
const emits = defineEmits(['clickLeft', 'clickRight', 'clickTitle', 'search'])
const props = defineProps({
dark: {
type: Boolean,
default: false,
},
modelValue: {
type: String,
default: '',
},
title: {
type: String,
default: '',
},
titleAlign: {
type: String,
default: 'center', // left | center
},
rightText: {
type: String,
default: '',
},
leftIcon: {
type: String,
default: 'left',
},
rightIcon: {
type: String,
default: '',
},
fixed: {
type: [Boolean, String],
default: true,
},
placeholder: {
type: [Boolean, String],
default: true,
},
color: {
type: String,
default: '',
},
backgroundColor: {
type: String,
default: '',
},
opacity: {
type: [Boolean, String],
default: false,
},
opacityBgUi: {
type: String,
default: 'bg-white',
},
statusBar: {
type: [Boolean, String],
default: false,
},
shadow: {
type: [Boolean, String],
default: false,
},
border: {
type: [Boolean, String],
default: false,
},
height: {
type: [Number, String],
default: 44,
},
leftWidth: {
type: [Number, String],
default: 80,
},
rightWidth: {
type: [Number, String],
default: 0,
},
tools: {
type: String,
default: 'title',
},
defaultSearch: {
type: String,
default: '',
},
})
const searchModel = computed(() => {
return props.defaultSearch
})
const themeColor = computed(() => {
if (props.dark) {
//
if (props.color) {
return props.color
} else {
return props.dark ? '#fff' : '#333'
}
}
return props.color || '#333'
})
const navbarHeight = computed(() => {
return getVal(props.height)
})
const leftIconWidth = computed(() => {
return getVal(props.leftWidth)
})
function onSearch(e) {
emits('search', e.value)
}
onLoad(() => {
if (uni.report && props.title !== '') {
uni.report('title', props.title)
}
})
const hasHistory = peach.$router.hasHistory()
function onClickLeft() {
if (hasHistory) {
peach.$router.back()
} else {
peach.$router.go('/pages/index/index')
}
emits('clickLeft')
}
function onClickTitle() {
emits('clickTitle')
}
</script>
<style lang="scss" scoped>
.bg-main {
background: linear-gradient(90deg, var(--ui-BG-Main), var(--ui-BG-Main-gradient)) !important;
color: #fff !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;
color: #000;
}
.sicon-home {
font-size: 32rpx;
color: #000;
}
.sicon-more {
font-size: 32rpx;
color: #000;
}
.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;
}
}
}
$nav-height: 44px;
.fixed-bg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: 1;
pointer-events: none;
}
.uni-nav-bar-text {
/* #ifdef APP-PLUS */
font-size: 34rpx;
/* #endif */
/* #ifndef APP-PLUS */
font-size: 14px;
/* #endif */
}
.uni-nav-bar-right-text {
font-size: 12px;
}
.uni-navbar__content {
position: relative;
background-color: transparent;
}
.uni-navbar__content_view {
// box-sizing: border-box;
}
.uni-navbar-btn-text {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
justify-content: flex-start;
align-items: center;
line-height: 18px;
}
.uni-navbar__header {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
padding: 0 10px;
flex-direction: row;
justify-content: space-between;
height: $nav-height;
font-size: 12px;
position: relative;
z-index: 2;
}
.uni-navbar__header-btns {
/* #ifndef APP-NVUE */
overflow: hidden;
display: flex;
/* #endif */
flex-wrap: nowrap;
flex-direction: row;
min-width: 40rpx;
justify-content: center;
align-items: center;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.uni-navbar__header-btns-left {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
width: 120rpx;
justify-content: flex-start;
align-items: center;
}
.uni-navbar__header-btns-right {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
justify-content: flex-end;
align-items: center;
}
.uni-navbar__header-container {
position: absolute;
left: 50%;
transform: translateX(-50%) translateY(-50%);
top: 50%;
}
.uni-navbar__header-container-inner {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex: 1;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 12px;
overflow: hidden;
}
.uni-navbar__placeholder-view {
height: $nav-height;
}
.uni-navbar--fixed {
position: fixed;
z-index: 998;
/* #ifdef H5 */
left: var(--window-left);
right: var(--window-right);
/* #endif */
/* #ifndef H5 */
left: 0;
right: 0;
/* #endif */
}
.uni-navbar--shadow {
box-shadow: 0 1px 6px #ccc;
}
.uni-navbar--border {
border-bottom-width: 1rpx;
border-bottom-style: solid;
border-bottom-color: #eee;
}
.uni-ellipsis-1 {
overflow: hidden;
/* #ifndef APP-NVUE */
white-space: nowrap;
text-overflow: ellipsis;
/* #endif */
/* #ifdef APP-NVUE */
lines: 1;
text-overflow: ellipsis;
/* #endif */
}
//
.uni-dark {
}
</style>

View File

@ -0,0 +1,15 @@
<!-- 自定义状态栏 -->
<template>
<view :style="{ height: statusBarHeight }" class="uni-status-bar"><slot /></view>
</template>
<script setup>
import peach from '@/peach'
const statusBarHeight = peach.$platform.device.statusBarHeight + 'px'
</script>
<style lang="scss">
.uni-status-bar {
height: var(--status-bar-height);
}
</style>

19
peach/config/index.js Normal file
View File

@ -0,0 +1,19 @@
// 开发环境配置
export let baseUrl
export let version
if (process.env.NODE_ENV === 'development') {
baseUrl = import.meta.env.MALL_DEV_BASE_URL
} else {
baseUrl = import.meta.env.MALL_BASE_URL
}
version = import.meta.env.MALL_VERSION
console.log(`[🍑商城 ${version}] http://ankkaya.top`)
export const apiPath = import.meta.env.MALL_API_PATH
export const staticUrl = import.meta.env.MALL_STATIC_URL
export default {
baseUrl,
apiPath,
staticUrl,
}

11
peach/config/zindex.js Normal file
View File

@ -0,0 +1,11 @@
export default {
toast: 10090,
noNetwork: 10080,
popup: 10075, // popup包含popupactionsheetkeyboardpicker的值
mask: 10070,
navbar: 980,
topTips: 975,
sticky: 970,
indexListSticky: 965,
popover: 960,
}

165
peach/helper/digit.js Normal file
View File

@ -0,0 +1,165 @@
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,
}

707
peach/helper/index.js Normal file
View File

@ -0,0 +1,707 @@
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,
}

293
peach/helper/test.js Normal file
View File

@ -0,0 +1,293 @@
/**
* 验证银行卡号
*/
function bankCode(value) {
return /^[1-9]\d{15,19}$/.test(value)
}
/**
* 验证电子邮箱格式
*/
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,
bankCode,
}

31
peach/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

168
peach/helper/utils.js Normal file
View File

@ -0,0 +1,168 @@
export function isArray(value) {
if (typeof Array.isArray === 'function') {
return Array.isArray(value)
} else {
return Object.prototype.toString.call(value) === '[object Array]'
}
}
export function isObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
export function isNumber(value) {
return !isNaN(Number(value))
}
export function isFunction(value) {
return typeof value == 'function'
}
export function isString(value) {
return typeof value == 'string'
}
export function isEmpty(value) {
if (isArray(value)) {
return value.length === 0
}
if (isObject(value)) {
return Object.keys(value).length === 0
}
return value === '' || value === undefined || value === null
}
export function isBoolean(value) {
return typeof value === 'boolean'
}
export function last(data) {
if (isArray(data) || isString(data)) {
return data[data.length - 1]
}
}
export function cloneDeep(obj) {
const d = isArray(obj) ? obj : {}
if (isObject(obj)) {
for (const key in obj) {
if (obj[key]) {
if (obj[key] && typeof obj[key] === 'object') {
d[key] = cloneDeep(obj[key])
} else {
d[key] = obj[key]
}
}
}
}
return d
}
export function clone(obj) {
return Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
}
export function deepMerge(a, b) {
let k
for (k in b) {
a[k] = a[k] && a[k].toString() === '[object Object]' ? deepMerge(a[k], b[k]) : (a[k] = b[k])
}
return a
}
export function contains(parent, node) {
while (node && (node = node.parentNode)) if (node === parent) return true
return false
}
export function orderBy(list, key) {
return list.sort((a, b) => a[key] - b[key])
}
export function deepTree(list) {
const newList = []
const map = {}
list.forEach((e) => (map[e.id] = e))
list.forEach((e) => {
const parent = map[e.parentId]
if (parent) {
;(parent.children || (parent.children = [])).push(e)
} else {
newList.push(e)
}
})
const fn = (list) => {
list.map((e) => {
if (e.children instanceof Array) {
e.children = orderBy(e.children, 'orderNum')
fn(e.children)
}
})
}
fn(newList)
return orderBy(newList, 'orderNum')
}
export function revDeepTree(list = []) {
const d = []
let id = 0
const deep = (list, parentId) => {
list.forEach((e) => {
if (!e.id) {
e.id = id++
}
e.parentId = parentId
d.push(e)
if (e.children && isArray(e.children)) {
deep(e.children, e.id)
}
})
}
deep(list || [], null)
return d
}
export function basename(path) {
let index = path.lastIndexOf('/')
index = index > -1 ? index : path.lastIndexOf('\\')
if (index < 0) {
return path
}
return path.substring(index + 1)
}
export function isWxBrowser() {
const ua = navigator.userAgent.toLowerCase()
if (ua.match(/MicroMessenger/i) == 'micromessenger') {
return true
} else {
return false
}
}
/**
* @description 如果value小于min取min如果value大于max取max
* @param {number} min
* @param {number} max
* @param {number} value
*/
export function range(min = 0, max = 0, value = 0) {
return Math.max(min, Math.min(max, Number(value)))
}

140
peach/hooks/useModal.js Normal file
View File

@ -0,0 +1,140 @@
import $store from '@/peach/store'
import $helper from '@/peach/helper'
import dayjs from 'dayjs'
import { ref } from 'vue'
import test from '@/peach/helper/test.js'
import AuthUtil from '@/peach/api/member/auth'
// 打开授权弹框
export function showAuthModal(type = 'smsLogin') {
const modal = $store('modal')
if (modal.auth !== '') {
closeAuthModal()
setTimeout(() => {
modal.$patch((state) => {
state.auth = type
})
}, 100)
} else {
modal.$patch((state) => {
state.auth = type
})
}
}
// 关闭授权弹框
export function closeAuthModal() {
$store('modal').$patch((state) => {
state.auth = ''
})
}
// 打开分享弹框
export function showShareModal() {
$store('modal').$patch((state) => {
state.share = true
})
}
// 关闭分享弹框
export function closeShareModal() {
$store('modal').$patch((state) => {
state.share = false
})
}
// 打开快捷菜单
export function showMenuTools() {
$store('modal').$patch((state) => {
state.menu = true
})
}
// 关闭快捷菜单
export function closeMenuTools() {
$store('modal').$patch((state) => {
state.menu = false
})
}
// 发送短信验证码 60秒
export function getSmsCode(event, mobile) {
const modalStore = $store('modal')
const lastSendTimer = modalStore.lastTimer[event]
if (typeof lastSendTimer === 'undefined') {
$helper.toast('短信发送事件错误')
return
}
const duration = dayjs().unix() - lastSendTimer
const canSend = duration >= 60
if (!canSend) {
$helper.toast('请稍后再试')
return
}
// 只有 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)
}
})
}

51
peach/index.js Normal file
View File

@ -0,0 +1,51 @@
import $url from '@/peach/url'
import $router from '@/peach/router'
import $platform from '@/peach/platform'
import $helper from '@/peach/helper'
import zIndex from '@/peach/config/zIndex.js'
import $store from '@/peach/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 peach = {
$store,
$url,
$router,
$platform,
$helper,
$zIndex: zIndex,
}
// 加载底层依赖
export async function peachInit() {
// 应用初始化
await $store('app').init()
// 平台初始化加载(各平台provider提供不同的加载流程)
$platform.load()
if (process.env.NODE_ENV === 'development') {
debug()
}
}
// 开发模式
function debug() {
// 开发环境引入vconsole调试
// #ifdef H5
// import("vconsole").then(vconsole => {
// new vconsole.default();
// });
// #endif
// TODO 芋艿:可以打印路由
// 同步前端页面到后端
// console.log(ROUTES)
}
export default peach

121
peach/platform/index.js Normal file
View File

@ -0,0 +1,121 @@
// #ifdef H5
import { isWxBrowser } from '@/peach/helper/utils'
// #endif
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
/**
* 检查网络
* @param {Boolean} silence - 静默检查
*/
async function checkNetwork() {
const networkStatus = await uni.getNetworkType()
if (networkStatus.networkType == 'none') {
return Promise.resolve(false)
}
return Promise.resolve(true)
}
/**
* 检查更新 (只检查小程序和App)
* @param {Boolean} silence - 静默检查
*/
const checkUpdate = (silence = false) => {
let canUpdate
// #ifdef MP-WEIXIN
// #endif
// #ifdef APP-PLUS
// TODO: 热更新
// #endif
}
// 获取小程序胶囊信息
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()
// 加载当前平台前置行为
const load = () => {
// if (provider === 'wechat') {
// wechat.load()
// }
}
const _platform = {
name,
device,
checkUpdate,
checkNetwork,
capsule,
navbar,
platform,
load,
}
export default _platform

301
peach/request/index.js Normal file
View File

@ -0,0 +1,301 @@
/**
* @author Ankkaya
* @description api 模块管理loading 配置请求拦截错误处理
*/
import Request from 'luch-request'
import { baseUrl, apiPath } from '@/peach/config'
import $store from '@/peach/store'
import $platform from '@/peach/platform'
import { showAuthModal } from '@/peach/hooks/useModal'
import AuthUtil from '@/peach/api/member/auth'
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()
}
/**
* Request 基础配置
*/
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['Accept'] = '*/*'
return config
},
(error) => {
return Promise.reject(error)
}
)
/**
* @description 响应拦截器
*/
http.interceptors.response.use(
(response) => {
console.log('response', 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,
})
return Promise.reject(false)
}
}
// 自定义处理【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) => {
console.log('error', 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 === true) {
uni.showToast({
title: error.data?.msg || errorMessage,
icon: 'none',
mask: true,
})
}
error.config.custom.showLoading && closeLoading()
}
return Promise.reject(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 ? '您的登陆已过期' : '请先登录',
})
}
/** 获得访问令牌 */
const getAccessToken = () => {
return uni.getStorageSync('token')
}
/** 获得刷新令牌 */
const getRefreshToken = () => {
return uni.getStorageSync('refresh-token')
}
const request = (config) => {
return http.middleware(config)
}
export default request

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

@ -0,0 +1,185 @@
import $store from '@/peach/store'
import { showAuthModal, showShareModal } from '@/peach/hooks/useModal'
import { isNumber, isString, isEmpty, startsWith, isObject, isNil, clone } from 'lodash'
import throttle from '@/peach/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;

3
peach/scss/_main.scss Normal file
View File

@ -0,0 +1,3 @@
body {
color: var(--text-a);
}

61
peach/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
peach/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;
}

162
peach/scss/_var.scss Normal file
View File

@ -0,0 +1,162 @@
@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-绿
$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.

28
peach/scss/index.scss Normal file
View File

@ -0,0 +1,28 @@
@import './tools';
@import './ui';
@font-face {
font-family: OPPOSANS;
src: url('~@/peach/scss/font/OPPOSANS-M-subfont.ttf');
}
.font-OPPOSANS {
font-family: OPPOSANS;
}
page {
-webkit-overflow-scrolling: touch; // 解决 ios 滑动不流畅
height: 100%;
width: 100%;
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

@ -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);
}
}
}

106
peach/scss/style/_text.scss Normal file
View File

@ -0,0 +1,106 @@
@use 'sass:math';
.font-0 {
font-size: 24rpx;
--textSize: -4rpx;
}
.font-1 {
font-size: 28rpx;
--textSize: 0rpx;
}
.font-2 {
font-size: 32rpx;
--textSize: 4rpx;
}
.font-3 {
font-size: 36rpx;
--textSize: 8rpx;
}
.font-4 {
font-size: 40rpx;
--textSize: 12rpx;
}
/**
* ??? var(--textSize) 能取到值吗
*/
.text {
@each $class, $value in $fontsize {
&-#{$class},
&-#{math.div($value ,2)} {
font-size: calc(#{$value}rpx + var(--textSize)) !important;
}
}
&-cut {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@at-root [class*='text-linecut'] {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-all;
}
@for $i from 2 through 10 {
&-linecut-#{$i} {
-webkit-line-clamp: #{$i};
}
}
&-justify {
text-align: justify;
}
&-justify-line {
text-align: justify;
line-height: 0.5em;
margin-top: 0.5em;
&::after {
content: '.';
display: inline-block;
width: 100%;
}
}
&-Abc {
text-transform: Capitalize !important;
}
&-ABC {
text-transform: Uppercase !important;
}
&-abc {
text-transform: Lowercase !important;
}
&-del,
&-line {
text-decoration: line-through !important;
}
&-bottomline {
text-decoration: underline !important;
}
&-italic {
font-style: italic !important;
}
&-style-none {
text-decoration: none !important;
}
&-break {
word-break: break-word !important;
overflow-wrap: break-word !important;
}
&-reset {
color: inherit !important;
}
&-price::before {
content: '¥';
font-size: 80%;
margin-right: 4rpx;
}
&-hide {
font: 0/0 a;
color: transparent;
text-shadow: none;
background-color: transparent;
border: 0;
}
}

View File

@ -0,0 +1,39 @@
// 核心主题样式文件
@mixin theme-dark {
// 背景色
--ui-BG: #393939;
--ui-BG-1: #333333;
--ui-BG-2: #2c2c2c;
--ui-BG-3: #292929;
--ui-BG-4: #222222;
// 文本色
--ui-TC: #ffffff;
--ui-TC-1: #d4d4d4;
--ui-TC-2: #919191;
--ui-TC-3: #6a6a6a;
--ui-TC-4: #474747;
// 模糊
--ui-Blur: rgba(38, 38, 38, 0.98);
--ui-Blur-1: rgba(38, 38, 38, 0.75);
--ui-Blur-2: rgba(38, 38, 38, 0.25);
--ui-Blur-3: rgba(38, 38, 38, 0.05);
// 边框
--ui-Border: rgba(119, 119, 119, 0.25);
--ui-Outline: rgba(255, 255, 255, 0.1);
--ui-Line: rgba(119, 119, 119, 0.25);
// 透明与阴影
--ui-Shadow: 0 0.5em 1em rgba(0, 0, 0, 0.45);
--ui-Shadow-sm: 0 0.125em 0.25em rgba(0, 0, 0, 0.475);
--ui-Shadow-lg: 0 1em 3em rgba(0, 0, 0, 0.475);
--ui-Shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.475);
--ui-Shadow-opacity: 0.55;
--ui-Shadow-opacity-sm: 0.175;
--ui-Shadow-opacity-lg: 0.75;
--ui-BG-opacity: 0.1;
}

View File

@ -0,0 +1,39 @@
// 核心主题样式文件
@mixin theme-light {
// 背景色
--ui-BG: #ffffff;
--ui-BG-1: #f6f6f6;
--ui-BG-2: #f1f1f1;
--ui-BG-3: #e8e8e8;
--ui-BG-4: #e0e0e0;
// 文本色
--ui-TC: #303030;
--ui-TC-1: #525252;
--ui-TC-2: #777777;
--ui-TC-3: #9e9e9e;
--ui-TC-4: #c6c6c6;
// 模糊
--ui-Blur: rgba(255, 255, 255, 0.98);
--ui-Blur-1: rgba(255, 255, 255, 0.75);
--ui-Blur-2: rgba(255, 255, 255, 0.25);
--ui-Blur-3: rgba(255, 255, 255, 0.05);
// 边框
--ui-Border: rgba(119, 119, 119, 0.25);
--ui-Outline: rgba(0, 0, 0, 0.1);
--ui-Line: rgba(119, 119, 119, 0.25);
// 透明与阴影
--ui-Shadow: 0 0.5em 1em rgba(0, 0, 0, 0.15);
--ui-Shadow-sm: 0 0.125em 0.25em rgba(0, 0, 0, 0.075);
--ui-Shadow-lg: 0 1em 3em rgba(0, 0, 0, 0.175);
--ui-Shadow-inset: inset 0 0.1em 0.2em rgba(0, 0, 0, 0.075);
--ui-Shadow-opacity: 0.45;
--ui-Shadow-opacity-sm: 0.075;
--ui-Shadow-opacity-lg: 0.65;
--ui-BG-opacity: 0.1;
}

View File

@ -0,0 +1,37 @@
@import './light';
@import './dark';
.theme-light {
@include theme-light;
}
.theme-dark {
@include theme-dark;
}
.theme-auto {
@include theme-light;
}
@media (prefers-color-scheme: dark) {
.theme-auto {
@include theme-dark;
}
}
@each $color, $value in $colors {
.main-#{$color} {
--ui-BG-Main: #{$value};
--ui-BG-Main-tag: #{rgba($value, 0.05)};
--ui-BG-Main-gradient: #{rgba($value, 0.6)};
--ui-BG-Main-light: #{rgba($value, 0.2)};
--ui-BG-Main-opacity-1: #{rgba($value, 0.1)};
--ui-BG-Main-opacity-4: #{rgba($value, 0.4)};
--ui-Main-box-shadow: 0 0.2em 0.5em #{rgba($value, var(--ui-Shadow-opacity))};
--ui-BG-Main-1: #{mix(rgba(255, 255, 255, 0.7), desaturate($value, 20%), 10%)};
--ui-BG-Main-2: #{mix(rgba(255, 255, 255, 0.6), desaturate($value, 40%), 20%)};
--ui-BG-Main-3: #{mix(rgba(119, 119, 119, 0.2), desaturate($value, 40%), 40%)};
--ui-BG-Main-4: #{mix(rgba(119, 119, 119, 0.1), desaturate($value, 40%), 60%)};
--ui-BG-Main-TC: #ffffff !important;
}
}

4
peach/scss/ui.scss Normal file
View File

@ -0,0 +1,4 @@
@import './theme/_style';
@import './main';
@import './style/background';

97
peach/store/app.js Normal file
View File

@ -0,0 +1,97 @@
import { defineStore } from 'pinia'
import $platform from '@/peach/platform'
import $router from '@/peach/router'
import user from './user'
import sys from './sys'
const app = defineStore({
id: 'app',
state: () => ({
info: {
// 应用信息
name: '', // 商城名称
logo: '', // logo
version: '', // 版本号
copyright: '', // 版权信息 I
copytime: '', // 版权信息 II
cdnurl: '', // 云存储域名
filesystem: '', // 云存储平台
},
platform: {
share: {
methods: [], // 支持的分享方式
forwardInfo: {}, // 默认转发信息
posterInfo: {}, // 海报信息
linkAddress: '', // 复制链接地址
},
bind_mobile: 0, // 登陆后绑定手机号提醒 (弱提醒,可手动关闭)
},
chat: {},
shareInfo: {}, // 全局分享信息
has_wechat_trade_managed: 0, // 小程序发货信息管理 0 没有 || 1 有
}),
actions: {
// 获取应用配置和模板
async init(templateId = null) {
// 检查网络
const networkStatus = await $platform.checkNetwork()
if (!networkStatus) {
$router.error('NetworkError')
}
if (true) {
this.info = {
name: '🍑商城',
logo: 'https://static.iocoder.cn/ruoyi-vue-pro-logo.png',
version: '1.0.0',
copyright: '全部开源,个人与企业可 100% 免费使用',
copytime: 'Copyright© 2018-2024',
cdnurl: 'https://file.sheepjs.com', // 云存储域名
filesystem: 'qcloud', // 云存储平台
}
this.platform = {
share: {
methods: ['poster', 'link'],
linkAddress: 'https://shopro.sheepjs.com/#/',
posterInfo: {
user_bg: '/static/img/shop/config/user-poster-bg.png',
goods_bg: '/static/img/shop/config/goods-poster-bg.png',
groupon_bg: '/static/img/shop/config/groupon-poster-bg.png',
},
},
bind_mobile: 0,
}
this.chat = {
chat_domain: 'https://api.shopro.sheepjs.com/chat',
room_id: 'admin',
}
this.has_wechat_trade_managed = 0
// 加载主题
const sysStore = sys()
sysStore.setTheme()
// 模拟用户登录
const userStore = user()
if (userStore.isLogin) {
userStore.loginAfter()
}
return Promise.resolve(true)
} else {
$router.error('InitError', res.msg || '加载失败')
}
},
},
persist: {
enabled: true,
strategies: [
{
key: 'app-store',
},
],
},
})
export default app

106
peach/store/cart.js Normal file
View File

@ -0,0 +1,106 @@
import { defineStore } from 'pinia'
import CartApi from '@/peach/api/trade/cart'
const cart = defineStore({
id: 'cart',
state: () => ({
list: [], // 购物车列表
selectedIds: [], // 已选列表
isAllSelected: false, // 是否全选
totalPriceSelected: 0, // 选中项总金额
}),
actions: {
// 获取购物车列表
async getList() {
const { data, code } = await CartApi.getCartList()
if (code === 0) {
this.list = data.validList
// 计算各种关联属性
this.selectedIds = []
this.isAllSelected = true
this.totalPriceSelected = 0
this.list.forEach((item) => {
if (item.selected) {
this.selectedIds.push(item.id)
this.totalPriceSelected += item.count * item.sku.price
} else {
this.isAllSelected = false
}
})
}
},
// 添加购物车
async add(goodsInfo) {
// 添加购物项
const { code } = await CartApi.addCart({
skuId: goodsInfo.id,
count: goodsInfo.goods_num,
})
// 刷新购物车列表
if (code === 0) {
await this.getList()
}
},
// 更新购物车
async update(goodsInfo) {
const { code } = await CartApi.updateCartCount({
id: goodsInfo.goods_id,
count: goodsInfo.goods_num,
})
if (code === 0) {
await this.getList()
}
},
// 移除购物车
async delete(ids) {
const { code } = await CartApi.deleteCart(ids.join(','))
if (code === 0) {
await this.getList()
}
},
// 单选购物车商品
async selectSingle(goodsId) {
const { code } = await CartApi.updateCartSelected({
ids: [goodsId],
selected: !this.selectedIds.includes(goodsId), // 取反
})
if (code === 0) {
await this.getList()
}
},
// 全选购物车商品
async selectAll(flag) {
const { code } = await CartApi.updateCartSelected({
ids: this.list.map((item) => item.id),
selected: flag,
})
if (code === 0) {
await this.getList()
}
},
// 清空购物车。注意,仅用于用户退出时,重置数据
emptyList() {
this.list = []
this.selectedIds = []
this.isAllSelected = true
this.totalPriceSelected = 0
},
},
persist: {
enabled: true,
strategies: [
{
key: 'cart-store',
},
],
},
})
export default cart

20
peach/store/index.js Normal file
View File

@ -0,0 +1,20 @@
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist-uni'
// 自动注入所有pinia模块
const files = import.meta.globEager('./*.js')
const modules = {}
Object.keys(files).forEach((key) => {
modules[key.replace(/(.*\/)*([^.]+).*/gi, '$2')] = files[key].default
})
export const setupPinia = (app) => {
const pinia = createPinia()
pinia.use(piniaPersist)
app.use(pinia)
}
export default (name) => {
return modules[name]()
}

29
peach/store/modal.js Normal file
View File

@ -0,0 +1,29 @@
import { defineStore } from 'pinia'
const modal = defineStore({
id: 'modal',
state: () => ({
auth: '', // 授权弹框 accountLogin|smsLogin|resetPassword|changeMobile|changePassword|changeUsername
share: false, // 分享弹框
menu: false, // 快捷菜单弹框
advHistory: [], // 广告弹框记录
lastTimer: {
// 短信验证码计时器,为了防止刷新请求做了持久化
smsLogin: 0,
changeMobile: 0,
resetPassword: 0,
changePassword: 0,
},
}),
persist: {
enabled: true,
strategies: [
{
key: 'modal-store',
paths: ['lastTimer', 'advHistory'],
},
],
},
})
export default modal

31
peach/store/sys.js Normal file
View File

@ -0,0 +1,31 @@
import { defineStore } from 'pinia'
const sys = defineStore({
id: 'sys',
state: () => ({
theme: '', // 主题,
mode: 'light', // 明亮模式、暗黑模式(暂未支持)
modeAuto: false, // 跟随系统
fontSize: 1, // 设置默认字号等级(0-4)
}),
getters: {},
actions: {
setTheme(theme = '') {
if (theme === '') {
this.theme = 'orange'
} else {
this.theme = theme
}
},
},
persist: {
enabled: true,
strategies: [
{
key: 'sys-store',
},
],
},
})
export default sys

132
peach/store/user.js Normal file
View File

@ -0,0 +1,132 @@
import { defineStore } from 'pinia'
import { isEmpty, cloneDeep, clone } from 'lodash'
import UserApi from '@/peach/api/member/user'
import PayWalletApi from '@/peach/api/pay/wallet'
import OrderApi from '@/peach/api/trade/order'
import CouponApi from '@/peach/api/promotion/coupon'
import cart from './cart'
// 默认用户信息
const defaultUserInfo = {
avatar: '', // 头像
nickname: '', // 昵称
gender: 0, // 性别
mobile: '', // 手机号
point: 0, // 积分
}
// 默认钱包信息
const defaultUserWallet = {
balance: 0, // 余额
}
// 默认订单、优惠券等其他资产信息
const defaultNumData = {
unusedCouponCount: 0,
orderCount: {
allCount: 0,
unpaidCount: 0,
undeliveredCount: 0,
deliveredCount: 0,
uncommentedCount: 0,
afterSaleCount: 0,
},
}
const user = defineStore({
id: 'user',
state: () => ({
userInfo: clone(defaultUserInfo),
userWallet: clone(defaultUserWallet),
numData: cloneDeep(defaultNumData),
isLogin: !!uni.getStorageSync('token'),
lastUpdateTime: 0,
}),
actions: {
// 获取用户信息
async getInfo() {
const { code, data } = await UserApi.getUserInfo()
if (code !== 0) {
return
}
this.userInfo = data
return Promise.resolve(data)
},
// 获得用户钱包
async getWallet() {
const { code, data } = await PayWalletApi.getPayWallet()
if (code !== 0) {
return
}
this.userWallet = data
},
// 获取订单、优惠券等其他资产信息
getNumData() {
OrderApi.getOrderCount().then((res) => {
if (res.code === 0) {
this.numData.orderCount = res.data
}
})
CouponApi.getUnusedCouponCount().then((res) => {
if (res.code === 0) {
this.numData.unusedCouponCount = res.data
}
})
},
// 设置 token
setToken(token = '', refreshToken = '') {
if (token === '') {
this.isLogin = false
uni.removeStorageSync('token')
uni.removeStorageSync('refresh-token')
} else {
this.isLogin = true
uni.setStorageSync('token', token)
uni.setStorageSync('refresh-token', refreshToken)
this.loginAfter()
}
return this.isLogin
},
// 登录后,加载各种信息
async loginAfter() {
await this.updateUserData()
// 提醒绑定手机号
if (app().platform.bind_mobile && !this.userInfo.mobile) {
showAuthModal('changeMobile')
}
// 添加分享记录
const shareLog = uni.getStorageSync('shareLog')
if (!isEmpty(shareLog)) {
this.addShareLog({
...shareLog,
})
}
},
// 重置用户默认数据
resetUserData() {
// 清空 token
this.setToken()
// 清空用户相关的缓存
this.userInfo = clone(defaultUserInfo)
this.userWallet = clone(defaultUserWallet)
this.numData = cloneDeep(defaultNumData)
// 清空购物车的缓存
cart().emptyList()
},
// 登出系统
async logout() {
this.resetUserData()
return !this.isLogin
},
},
})
export default user

199
peach/url/index.js Normal file
View File

@ -0,0 +1,199 @@
import $store from '@/peach/store'
import { staticUrl } from '@/peach/config'
const cdn = (url = '', cdnurl = '') => {
if (!url) return ''
if (url.indexOf('http') === 0) {
return url
}
if (cdnurl === '') {
cdnurl = $store('app').info.cdnurl
}
return cdnurl + url
}
export default {
// 添加cdn域名前缀
cdn,
// 对象存储自动剪裁缩略图
thumb: (url = '', params) => {
url = cdn(url)
return append_thumbnail_params(url, params)
},
// 静态资源地址
static: (url = '', staticurl = '') => {
if (staticurl === '') {
staticurl = staticUrl
}
if (staticurl !== 'local') {
url = cdn(url, staticurl)
}
return url
},
// css背景图片地址
css: (url = '', staticurl = '') => {
if (staticurl === '') {
staticurl = staticUrl
}
if (staticurl !== 'local') {
url = cdn(url, staticurl)
}
// #ifdef APP-PLUS
if (staticurl === 'local') {
url = plus.io.convertLocalFileSystemURL(url)
}
// #endif
return `url(${url})`
},
}
/**
* 追加对象存储自动裁剪/压缩参数
*
* @return string
*/
function append_thumbnail_params(url, params) {
const filesystem = $store('app').info.filesystem
if (filesystem === 'public') {
return url
}
let width = params.width || '200' // 宽度
let height = params.height || '200' // 高度
let mode = params.mode || 'lfit' // 缩放模式
let quality = params.quality || 90 // 压缩质量
let gravity = params.gravity || 'center' // 剪裁质量
let suffix = ''
let crop_str = ''
let quality_str = ''
let size = width + 'x' + height
switch (filesystem) {
case 'aliyun':
// 裁剪
if (!gravity && gravity != 'center') {
// 指定了裁剪区域
mode = 'mfit'
crop_str = '/crop,g_' + gravityFormatMap('aliyun', gravity) + ',w_' + width + ',h_' + height
}
// 质量压缩
if (quality > 0 && quality < 100) {
quality_str = '/quality,q_' + quality
}
// 缩放参数
suffix = 'x-oss-process=image/resize,m_' + mode + ',w_' + width + ',h_' + height
// 拼接裁剪和质量压缩
suffix += crop_str + quality_str
break
case 'qcloud':
let mode_str = 'thumbnail'
if (mode == 'fill' || (!gravity && gravity != 'center')) {
// 指定了裁剪区域
mode_str = 'crop'
mode = 'fill'
crop_str = '/gravity/' + gravityFormatMap('qcloud', gravity)
}
// 质量压缩
if (quality > 0 && quality < 100) {
quality_str = '/rquality/' + quality
}
switch (mode) {
case 'lfit':
size = '' + size + '>'
break
case 'mfit':
size = '!' + size + 'r'
case 'fill':
break
case 'pad':
size = size + '/pad/1'
break
case 'fixed':
size = size + '!'
break
}
suffix = 'imageMogr2/' + mode_str + '/' + size + crop_str + quality_str
break
case 'qiniu':
if (mode == 'fill' || (!gravity && gravity != 'center')) {
// 指定了裁剪区域,全部转为 mfit
mode = 'mfit'
crop_str = '/gravity/' + gravityFormatMap('qiniu', gravity) + '/crop/' + size
}
// 质量压缩
if (quality > 0 && quality < 100) {
quality_str = '/quality/' + quality
}
switch (mode) {
case 'lfit':
case 'pad': // 七牛不支持在缩放之后,尺寸不足时,填充背景色,所以这里和 lfit 模式一样
size = size + '>'
break
case 'mfit':
size = '!' + size + 'r'
break
case 'fill':
// 会被转为 mfit
break
case 'fixed':
size = size + '!'
break
}
suffix = 'imageMogr2/thumbnail/' + size + crop_str + quality_str
break
}
return url + '?' + suffix
}
/**
* 裁剪区域格式转换
*
* @param string $type aliyun|qcloud|qiniu
* @param string $gravity 统一的裁剪区域字符
*
* @return string
*/
function gravityFormatMap(type, gravity) {
let gravityFormat = {
aliyun: {
north_west: 'nw', // 左上
north: 'north', // 中上
north_east: 'ne', // 右上
west: 'west', // 左中
center: 'center', // 中部
east: 'east', // 右中
south_west: 'sw', // 左下
south: 'south', // 中下
south_east: 'se', // 右下
},
qcloud: {
northwest: 'nw', // 左上
north: 'north', // 中上
northeast: 'ne', // 右上
west: 'west', // 左中
center: 'center', // 中部
east: 'east', // 右中
southwest: 'sw', // 左下
south: 'south', // 中下
southeast: 'se', // 右下
},
qiniu: {
NorthWest: 'nw', // 左上
North: 'north', // 中上
NorthEast: 'ne', // 右上
West: 'west', // 左中
Center: 'center', // 中部
East: 'east', // 右中
SouthWest: 'sw', // 左下
South: 'south', // 中下
SouthEast: 'se', // 右下
},
}
return gravityFormat[type][gravity]
}

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

10
uni.promisify.adaptor.js Normal file
View File

@ -0,0 +1,10 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
});
},
});

78
uni.scss Normal file
View File

@ -0,0 +1,78 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
@import '@/peach/scss/_var.scss';
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; //基本色
$uni-text-color-inverse: #fff; //反色
$uni-text-color-grey: #999; //辅助灰色如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #ffffff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; //点击状态颜色
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); //遮罩颜色
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle: 26px;
$uni-color-paragraph: #3f536e; // 文章段落颜色
$uni-font-size-paragraph: 15px;

8
vite.config.js Normal file
View File

@ -0,0 +1,8 @@
import uni from '@dcloudio/vite-plugin-uni'
// https://vitejs.dev/config/
export default (command, mode) => {
return {
plugins: [uni()],
}
}