commit 035b98b15a42f4514885fef6f58e37072daed553 Author: Ankkaya Date: Tue Apr 30 17:45:03 2024 +0800 init diff --git a/.env b/.env new file mode 100644 index 0000000..c8af66c --- /dev/null +++ b/.env @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b3526b --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +unpackage/* +node_modules/* +.idea/* +deploy.sh +.hbuilderx/ +.vscode/ +**/.DS_Store +package-lock.json +*.keystore diff --git a/App.vue b/App.vue new file mode 100644 index 0000000..ae82c24 --- /dev/null +++ b/App.vue @@ -0,0 +1,23 @@ + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..c3ff205 --- /dev/null +++ b/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..8306a90 --- /dev/null +++ b/main.js @@ -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, + } +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..8078adf --- /dev/null +++ b/manifest.json @@ -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" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + /* 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" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8119a1f --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/pages.json b/pages.json new file mode 100644 index 0000000..f068b8a --- /dev/null +++ b/pages.json @@ -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": {} +} diff --git a/pages/index/index.vue b/pages/index/index.vue new file mode 100644 index 0000000..e420ca9 --- /dev/null +++ b/pages/index/index.vue @@ -0,0 +1,12 @@ + + + diff --git a/peach/api/member/auth.js b/peach/api/member/auth.js new file mode 100644 index 0000000..f49df3a --- /dev/null +++ b/peach/api/member/auth.js @@ -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 diff --git a/peach/api/member/user.js b/peach/api/member/user.js new file mode 100644 index 0000000..0e3e2d9 --- /dev/null +++ b/peach/api/member/user.js @@ -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 diff --git a/peach/api/pay/wallet.js b/peach/api/pay/wallet.js new file mode 100644 index 0000000..7f712b1 --- /dev/null +++ b/peach/api/pay/wallet.js @@ -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 diff --git a/peach/api/promotion/coupon.js b/peach/api/promotion/coupon.js new file mode 100644 index 0000000..ff0a143 --- /dev/null +++ b/peach/api/promotion/coupon.js @@ -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 diff --git a/peach/api/trade/cart.js b/peach/api/trade/cart.js new file mode 100644 index 0000000..e8e5afe --- /dev/null +++ b/peach/api/trade/cart.js @@ -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 diff --git a/peach/api/trade/order.js b/peach/api/trade/order.js new file mode 100644 index 0000000..a92f3ed --- /dev/null +++ b/peach/api/trade/order.js @@ -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 参数的问题 + 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 diff --git a/peach/components/p-fixed/p-fixed.vue b/peach/components/p-fixed/p-fixed.vue new file mode 100644 index 0000000..f09426a --- /dev/null +++ b/peach/components/p-fixed/p-fixed.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/peach/components/p-inner-navbar/p-inner-navbar.vue b/peach/components/p-inner-navbar/p-inner-navbar.vue new file mode 100644 index 0000000..ea24009 --- /dev/null +++ b/peach/components/p-inner-navbar/p-inner-navbar.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/peach/components/p-layout/p-layout.vue b/peach/components/p-layout/p-layout.vue new file mode 100644 index 0000000..dbe0649 --- /dev/null +++ b/peach/components/p-layout/p-layout.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/peach/components/p-navbar/p-navbar.vue b/peach/components/p-navbar/p-navbar.vue new file mode 100644 index 0000000..d636724 --- /dev/null +++ b/peach/components/p-navbar/p-navbar.vue @@ -0,0 +1,433 @@ + + + + + + diff --git a/peach/components/p-status-bar/p-status-bar.vue b/peach/components/p-status-bar/p-status-bar.vue new file mode 100644 index 0000000..2d5dbae --- /dev/null +++ b/peach/components/p-status-bar/p-status-bar.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/peach/config/index.js b/peach/config/index.js new file mode 100644 index 0000000..0197bba --- /dev/null +++ b/peach/config/index.js @@ -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, +} diff --git a/peach/config/zindex.js b/peach/config/zindex.js new file mode 100644 index 0000000..3073ed1 --- /dev/null +++ b/peach/config/zindex.js @@ -0,0 +1,11 @@ +export default { + toast: 10090, + noNetwork: 10080, + popup: 10075, // popup包含popup,actionsheet,keyboard,picker的值 + mask: 10070, + navbar: 980, + topTips: 975, + sticky: 970, + indexListSticky: 965, + popover: 960, +} diff --git a/peach/helper/digit.js b/peach/helper/digit.js new file mode 100644 index 0000000..ba2e8a3 --- /dev/null +++ b/peach/helper/digit.js @@ -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, +} diff --git a/peach/helper/index.js b/peach/helper/index.js new file mode 100644 index 0000000..59eaf61 --- /dev/null +++ b/peach/helper/index.js @@ -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 添加单位,如果有rpx,upx,%,px等单位结尾或者值为auto,直接返回,否则加上px单位结尾 + * @param {string|number} value 需要添加单位的值 + * @param {string} unit 添加的单位名 比如px + */ +export function addUnit(value = 'auto', unit = 'px') { + value = String(value) + return test.number(value) ? `${value}${unit}` : value +} + +/** + * @description 深度克隆 + * @param {object} obj 需要深度克隆的对象 + * @returns {*} 克隆后的对象或者原值(不是对象) + */ +function deepClone(obj) { + // 对常见的“非”值,直接返回原来值 + if ([null, undefined, NaN, false].includes(obj)) return obj + if (typeof obj !== 'object' && typeof obj !== 'function') { + // 原始类型直接返回 + return obj + } + const o = test.array(obj) ? [] : {} + for (const i in obj) { + if (obj.hasOwnProperty(i)) { + o[i] = typeof obj[i] === 'object' ? deepClone(obj[i]) : obj[i] + } + } + return o +} + +/** + * @description JS对象深度合并 + * @param {object} target 需要拷贝的对象 + * @param {object} source 拷贝的来源对象 + * @returns {object|boolean} 深度合并后的对象或者false(入参有不是对象) + */ +export function deepMerge(target = {}, source = {}) { + target = deepClone(target) + if (typeof target !== 'object' || typeof source !== 'object') return false + for (const prop in source) { + if (!source.hasOwnProperty(prop)) continue + if (prop in target) { + if (typeof target[prop] !== 'object') { + target[prop] = source[prop] + } else if (typeof source[prop] !== 'object') { + target[prop] = source[prop] + } else if (target[prop].concat && source[prop].concat) { + target[prop] = target[prop].concat(source[prop]) + } else { + target[prop] = deepMerge(target[prop], source[prop]) + } + } else { + target[prop] = source[prop] + } + } + return target +} + +/** + * @description error提示 + * @param {*} err 错误内容 + */ +function error(err) { + // 开发环境才提示,生产环境不会提示 + if (process.env.NODE_ENV === 'development') { + console.error(`SheepJS:${err}`) + } +} + +/** + * @description 打乱数组 + * @param {array} array 需要打乱的数组 + * @returns {array} 打乱后的数组 + */ +function randomArray(array = []) { + // 原理是sort排序,Math.random()产生0<= x < 1之间的数,会导致x-0.05大于或者小于0 + return array.sort(() => Math.random() - 0.5) +} + +// padStart 的 polyfill,因为某些机型或情况,还无法支持es7的padStart,比如电脑版的微信小程序 +// 所以这里做一个兼容polyfill的兼容处理 +if (!String.prototype.padStart) { + // 为了方便表示这里 fillString 用了ES6 的默认参数,不影响理解 + String.prototype.padStart = function (maxLength, fillString = ' ') { + if (Object.prototype.toString.call(fillString) !== '[object String]') { + throw new TypeError('fillString must be String') + } + const str = this + // 返回 String(str) 这里是为了使返回的值是字符串字面量,在控制台中更符合直觉 + if (str.length >= maxLength) return String(str) + + const fillLength = maxLength - str.length + let times = Math.ceil(fillLength / fillString.length) + while ((times >>= 1)) { + fillString += fillString + if (times === 1) { + fillString += fillString + } + } + return fillString.slice(0, fillLength) + str + } +} + +/** + * @description 格式化时间 + * @param {String|Number} dateTime 需要格式化的时间戳 + * @param {String} fmt 格式化规则 yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合 默认yyyy-mm-dd + * @returns {string} 返回格式化后的字符串 + */ +function timeFormat(dateTime = null, formatStr = 'yyyy-mm-dd') { + let date + // 若传入时间为假值,则取当前时间 + if (!dateTime) { + date = new Date() + } + // 若为unix秒时间戳,则转为毫秒时间戳(逻辑有点奇怪,但不敢改,以保证历史兼容) + else if (/^\d{10}$/.test(dateTime?.toString().trim())) { + date = new Date(dateTime * 1000) + } + // 若用户传入字符串格式时间戳,new Date无法解析,需做兼容 + else if (typeof dateTime === 'string' && /^\d+$/.test(dateTime.trim())) { + date = new Date(Number(dateTime)) + } + // 其他都认为符合 RFC 2822 规范 + else { + // 处理平台性差异,在Safari/Webkit中,new Date仅支持/作为分割符的字符串时间 + date = new Date(typeof dateTime === 'string' ? dateTime.replace(/-/g, '/') : dateTime) + } + + const timeSource = { + y: date.getFullYear().toString(), // 年 + m: (date.getMonth() + 1).toString().padStart(2, '0'), // 月 + d: date.getDate().toString().padStart(2, '0'), // 日 + h: date.getHours().toString().padStart(2, '0'), // 时 + M: date.getMinutes().toString().padStart(2, '0'), // 分 + s: date.getSeconds().toString().padStart(2, '0'), // 秒 + // 有其他格式化字符需求可以继续添加,必须转化成字符串 + } + + for (const key in timeSource) { + const [ret] = new RegExp(`${key}+`).exec(formatStr) || [] + if (ret) { + // 年可能只需展示两位 + const beginIndex = key === 'y' && ret.length === 2 ? 2 : 0 + formatStr = formatStr.replace(ret, timeSource[key].slice(beginIndex)) + } + } + + return formatStr +} + +/** + * @description 时间戳转为多久之前 + * @param {String|Number} timestamp 时间戳 + * @param {String|Boolean} format + * 格式化规则如果为时间格式字符串,超出一定时间范围,返回固定的时间格式; + * 如果为布尔值false,无论什么时间,都返回多久以前的格式 + * @returns {string} 转化后的内容 + */ +function timeFrom(timestamp = null, format = 'yyyy-mm-dd') { + if (timestamp == null) timestamp = Number(new Date()) + timestamp = parseInt(timestamp) + // 判断用户输入的时间戳是秒还是毫秒,一般前端js获取的时间戳是毫秒(13位),后端传过来的为秒(10位) + if (timestamp.toString().length == 10) timestamp *= 1000 + let timer = new Date().getTime() - timestamp + timer = parseInt(timer / 1000) + // 如果小于5分钟,则返回"刚刚",其他以此类推 + let tips = '' + switch (true) { + case timer < 300: + tips = '刚刚' + break + case timer >= 300 && timer < 3600: + tips = `${parseInt(timer / 60)}分钟前` + break + case timer >= 3600 && timer < 86400: + tips = `${parseInt(timer / 3600)}小时前` + break + case timer >= 86400 && timer < 2592000: + tips = `${parseInt(timer / 86400)}天前` + break + default: + // 如果format为false,则无论什么时间戳,都显示xx之前 + if (format === false) { + if (timer >= 2592000 && timer < 365 * 86400) { + tips = `${parseInt(timer / (86400 * 30))}个月前` + } else { + tips = `${parseInt(timer / (86400 * 365))}年前` + } + } else { + tips = timeFormat(timestamp, format) + } + } + return tips +} + +/** + * @description 去除空格 + * @param String str 需要去除空格的字符串 + * @param String pos both(左右)|left|right|all 默认both + */ +function trim(str, pos = 'both') { + str = String(str) + if (pos == 'both') { + return str.replace(/^\s+|\s+$/g, '') + } + if (pos == 'left') { + return str.replace(/^\s*/, '') + } + if (pos == 'right') { + return str.replace(/(\s*$)/g, '') + } + if (pos == 'all') { + return str.replace(/\s+/g, '') + } + return str +} + +/** + * @description 对象转url参数 + * @param {object} data,对象 + * @param {Boolean} isPrefix,是否自动加上"?" + * @param {string} arrayFormat 规则 indices|brackets|repeat|comma + */ +function queryParams(data = {}, isPrefix = true, arrayFormat = 'brackets') { + const prefix = isPrefix ? '?' : '' + const _result = [] + if (['indices', 'brackets', 'repeat', 'comma'].indexOf(arrayFormat) == -1) arrayFormat = 'brackets' + for (const key in data) { + const value = data[key] + // 去掉为空的参数 + if (['', undefined, null].indexOf(value) >= 0) { + continue + } + // 如果值为数组,另行处理 + if (value.constructor === Array) { + // e.g. {ids: [1, 2, 3]} + switch (arrayFormat) { + case 'indices': + // 结果: ids[0]=1&ids[1]=2&ids[2]=3 + for (let i = 0; i < value.length; i++) { + _result.push(`${key}[${i}]=${value[i]}`) + } + break + case 'brackets': + // 结果: ids[]=1&ids[]=2&ids[]=3 + value.forEach((_value) => { + _result.push(`${key}[]=${_value}`) + }) + break + case 'repeat': + // 结果: ids=1&ids=2&ids=3 + value.forEach((_value) => { + _result.push(`${key}=${_value}`) + }) + break + case 'comma': + // 结果: ids=1,2,3 + let commaStr = '' + value.forEach((_value) => { + commaStr += (commaStr ? ',' : '') + _value + }) + _result.push(`${key}=${commaStr}`) + break + default: + value.forEach((_value) => { + _result.push(`${key}[]=${_value}`) + }) + } + } else { + _result.push(`${key}=${value}`) + } + } + return _result.length ? prefix + _result.join('&') : '' +} + +/** + * 显示消息提示框 + * @param {String} title 提示的内容,长度与 icon 取值有关。 + * @param {Number} duration 提示的延迟时间,单位毫秒,默认:2000 + */ +function toast(title, duration = 2000) { + uni.showToast({ + title: String(title), + icon: 'none', + duration, + }) +} + +/** + * @description 根据主题type值,获取对应的图标 + * @param {String} type 主题名称,primary|info|error|warning|success + * @param {boolean} fill 是否使用fill填充实体的图标 + */ +function type2icon(type = 'success', fill = false) { + // 如果非预置值,默认为success + if (['primary', 'info', 'error', 'warning', 'success'].indexOf(type) == -1) type = 'success' + let iconName = '' + // 目前(2019-12-12),info和primary使用同一个图标 + switch (type) { + case 'primary': + iconName = 'info-circle' + break + case 'info': + iconName = 'info-circle' + break + case 'error': + iconName = 'close-circle' + break + case 'warning': + iconName = 'error-circle' + break + case 'success': + iconName = 'checkmark-circle' + break + default: + iconName = 'checkmark-circle' + } + // 是否是实体类型,加上-fill,在icon组件库中,实体的类名是后面加-fill的 + if (fill) iconName += '-fill' + return iconName +} + +/** + * @description 数字格式化 + * @param {number|string} number 要格式化的数字 + * @param {number} decimals 保留几位小数 + * @param {string} decimalPoint 小数点符号 + * @param {string} thousandsSeparator 千分位符号 + * @returns {string} 格式化后的数字 + */ +function priceFormat(number, decimals = 0, decimalPoint = '.', thousandsSeparator = ',') { + number = `${number}`.replace(/[^0-9+-Ee.]/g, '') + const n = !isFinite(+number) ? 0 : +number + const prec = !isFinite(+decimals) ? 0 : Math.abs(decimals) + const sep = typeof thousandsSeparator === 'undefined' ? ',' : thousandsSeparator + const dec = typeof decimalPoint === 'undefined' ? '.' : decimalPoint + let s = '' + + s = (prec ? round(n, prec) + '' : `${Math.round(n)}`).split('.') + const re = /(-?\d+)(\d{3})/ + while (re.test(s[0])) { + s[0] = s[0].replace(re, `$1${sep}$2`) + } + + if ((s[1] || '').length < prec) { + s[1] = s[1] || '' + s[1] += new Array(prec - s[1].length + 1).join('0') + } + return s.join(dec) +} + +/** + * @description 获取duration值 + * 如果带有ms或者s直接返回,如果大于一定值,认为是ms单位,小于一定值,认为是s单位 + * 比如以30位阈值,那么300大于30,可以理解为用户想要的是300ms,而不是想花300s去执行一个动画 + * @param {String|number} value 比如: "1s"|"100ms"|1|100 + * @param {boolean} unit 提示: 如果是false 默认返回number + * @return {string|number} + */ +function getDuration(value, unit = true) { + const valueNum = parseInt(value) + if (unit) { + if (/s$/.test(value)) return value + return value > 30 ? `${value}ms` : `${value}s` + } + if (/ms$/.test(value)) return valueNum + if (/s$/.test(value)) return valueNum > 30 ? valueNum : valueNum * 1000 + return valueNum +} + +/** + * @description 日期的月或日补零操作 + * @param {String} value 需要补零的值 + */ +function padZero(value) { + return `00${value}`.slice(-2) +} + +/** + * @description 获取某个对象下的属性,用于通过类似'a.b.c'的形式去获取一个对象的的属性的形式 + * @param {object} obj 对象 + * @param {string} key 需要获取的属性字段 + * @returns {*} + */ +function getProperty(obj, key) { + if (!obj) { + return + } + if (typeof key !== 'string' || key === '') { + return '' + } + if (key.indexOf('.') !== -1) { + const keys = key.split('.') + let firstObj = obj[keys[0]] || {} + + for (let i = 1; i < keys.length; i++) { + if (firstObj) { + firstObj = firstObj[keys[i]] + } + } + return firstObj + } + return obj[key] +} + +/** + * @description 设置对象的属性值,如果'a.b.c'的形式进行设置 + * @param {object} obj 对象 + * @param {string} key 需要设置的属性 + * @param {string} value 设置的值 + */ +function setProperty(obj, key, value) { + if (!obj) { + return + } + // 递归赋值 + const inFn = function (_obj, keys, v) { + // 最后一个属性key + if (keys.length === 1) { + _obj[keys[0]] = v + return + } + // 0~length-1个key + while (keys.length > 1) { + const k = keys[0] + if (!_obj[k] || typeof _obj[k] !== 'object') { + _obj[k] = {} + } + const key = keys.shift() + // 自调用判断是否存在属性,不存在则自动创建对象 + inFn(_obj[k], keys, v) + } + } + + if (typeof key !== 'string' || key === '') { + } else if (key.indexOf('.') !== -1) { + // 支持多层级赋值操作 + const keys = key.split('.') + inFn(obj, keys, value) + } else { + obj[key] = value + } +} + +/** + * @description 获取当前页面路径 + */ +function page() { + const pages = getCurrentPages() + // 某些特殊情况下(比如页面进行redirectTo时的一些时机),pages可能为空数组 + return `/${pages[pages.length - 1]?.route ?? ''}` +} + +/** + * @description 获取当前路由栈实例数组 + */ +function pages() { + const pages = getCurrentPages() + return pages +} + +/** + * 获取H5-真实根地址 兼容hash+history模式 + */ +export function getRootUrl() { + let url = '' + // #ifdef H5 + url = location.origin + '/' + + 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, +} diff --git a/peach/helper/test.js b/peach/helper/test.js new file mode 100644 index 0000000..dac4ad3 --- /dev/null +++ b/peach/helper/test.js @@ -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, +} diff --git a/peach/helper/throttle.js b/peach/helper/throttle.js new file mode 100644 index 0000000..858fa74 --- /dev/null +++ b/peach/helper/throttle.js @@ -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 diff --git a/peach/helper/utils.js b/peach/helper/utils.js new file mode 100644 index 0000000..8211c72 --- /dev/null +++ b/peach/helper/utils.js @@ -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))) +} diff --git a/peach/hooks/useModal.js b/peach/hooks/useModal.js new file mode 100644 index 0000000..60a0af9 --- /dev/null +++ b/peach/hooks/useModal.js @@ -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) + } + }) +} diff --git a/peach/index.js b/peach/index.js new file mode 100644 index 0000000..9589458 --- /dev/null +++ b/peach/index.js @@ -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 diff --git a/peach/platform/index.js b/peach/platform/index.js new file mode 100644 index 0000000..fffaf0c --- /dev/null +++ b/peach/platform/index.js @@ -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 diff --git a/peach/request/index.js b/peach/request/index.js new file mode 100644 index 0000000..b8aeac8 --- /dev/null +++ b/peach/request/index.js @@ -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 diff --git a/peach/router/index.js b/peach/router/index.js new file mode 100644 index 0000000..1dec74a --- /dev/null +++ b/peach/router/index.js @@ -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, +} diff --git a/peach/router/utils/strip-json-comments.js b/peach/router/utils/strip-json-comments.js new file mode 100644 index 0000000..5995992 --- /dev/null +++ b/peach/router/utils/strip-json-comments.js @@ -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)); +} diff --git a/peach/router/utils/uni-read-pages-v3.js b/peach/router/utils/uni-read-pages-v3.js new file mode 100644 index 0000000..303f10a --- /dev/null +++ b/peach/router/utils/uni-read-pages-v3.js @@ -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; diff --git a/peach/scss/_main.scss b/peach/scss/_main.scss new file mode 100644 index 0000000..0bf2e6d --- /dev/null +++ b/peach/scss/_main.scss @@ -0,0 +1,3 @@ +body { + color: var(--text-a); +} diff --git a/peach/scss/_mixins.scss b/peach/scss/_mixins.scss new file mode 100644 index 0000000..094f96c --- /dev/null +++ b/peach/scss/_mixins.scss @@ -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); +} diff --git a/peach/scss/_tools.scss b/peach/scss/_tools.scss new file mode 100644 index 0000000..d854a67 --- /dev/null +++ b/peach/scss/_tools.scss @@ -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; +} diff --git a/peach/scss/_var.scss b/peach/scss/_var.scss new file mode 100644 index 0000000..0f6451b --- /dev/null +++ b/peach/scss/_var.scss @@ -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 +); diff --git a/peach/scss/font/OPPOSANS-M-subfont.ttf b/peach/scss/font/OPPOSANS-M-subfont.ttf new file mode 100644 index 0000000..88ff835 Binary files /dev/null and b/peach/scss/font/OPPOSANS-M-subfont.ttf differ diff --git a/peach/scss/index.scss b/peach/scss/index.scss new file mode 100644 index 0000000..1726fbf --- /dev/null +++ b/peach/scss/index.scss @@ -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; +} diff --git a/peach/scss/style/_background.scss b/peach/scss/style/_background.scss new file mode 100644 index 0000000..c68b9a1 --- /dev/null +++ b/peach/scss/style/_background.scss @@ -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); + } + } +} diff --git a/peach/scss/style/_text.scss b/peach/scss/style/_text.scss new file mode 100644 index 0000000..f1c672b --- /dev/null +++ b/peach/scss/style/_text.scss @@ -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; + } +} diff --git a/peach/scss/theme/_dark.scss b/peach/scss/theme/_dark.scss new file mode 100644 index 0000000..fdaf143 --- /dev/null +++ b/peach/scss/theme/_dark.scss @@ -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; +} diff --git a/peach/scss/theme/_light.scss b/peach/scss/theme/_light.scss new file mode 100644 index 0000000..31c0d78 --- /dev/null +++ b/peach/scss/theme/_light.scss @@ -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; +} diff --git a/peach/scss/theme/_style.scss b/peach/scss/theme/_style.scss new file mode 100644 index 0000000..03f24b9 --- /dev/null +++ b/peach/scss/theme/_style.scss @@ -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; + } +} diff --git a/peach/scss/ui.scss b/peach/scss/ui.scss new file mode 100644 index 0000000..a497455 --- /dev/null +++ b/peach/scss/ui.scss @@ -0,0 +1,4 @@ +@import './theme/_style'; +@import './main'; + +@import './style/background'; diff --git a/peach/store/app.js b/peach/store/app.js new file mode 100644 index 0000000..e4eec54 --- /dev/null +++ b/peach/store/app.js @@ -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 diff --git a/peach/store/cart.js b/peach/store/cart.js new file mode 100644 index 0000000..3b2728d --- /dev/null +++ b/peach/store/cart.js @@ -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 diff --git a/peach/store/index.js b/peach/store/index.js new file mode 100644 index 0000000..e31b822 --- /dev/null +++ b/peach/store/index.js @@ -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]() +} diff --git a/peach/store/modal.js b/peach/store/modal.js new file mode 100644 index 0000000..dcd3e6c --- /dev/null +++ b/peach/store/modal.js @@ -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 diff --git a/peach/store/sys.js b/peach/store/sys.js new file mode 100644 index 0000000..9665dee --- /dev/null +++ b/peach/store/sys.js @@ -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 diff --git a/peach/store/user.js b/peach/store/user.js new file mode 100644 index 0000000..27034b8 --- /dev/null +++ b/peach/store/user.js @@ -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 diff --git a/peach/url/index.js b/peach/url/index.js new file mode 100644 index 0000000..8713545 --- /dev/null +++ b/peach/url/index.js @@ -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] +} diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000..b5771e2 Binary files /dev/null and b/static/logo.png differ diff --git a/uni.promisify.adaptor.js b/uni.promisify.adaptor.js new file mode 100644 index 0000000..47fbce1 --- /dev/null +++ b/uni.promisify.adaptor.js @@ -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])); + }); + }, +}); \ No newline at end of file diff --git a/uni.scss b/uni.scss new file mode 100644 index 0000000..87b3550 --- /dev/null +++ b/uni.scss @@ -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; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..c8a052e --- /dev/null +++ b/vite.config.js @@ -0,0 +1,8 @@ +import uni from '@dcloudio/vite-plugin-uni' + +// https://vitejs.dev/config/ +export default (command, mode) => { + return { + plugins: [uni()], + } +}