chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
<template>
</template>
<script>
</script>
<style>
</style>

View File

@@ -0,0 +1,97 @@
{
"id": "x-tools",
"displayName": "各端常用工具函数",
"version": "1.1.7",
"description": "各端常用工具函数",
"keywords": [
"x-tools"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0",
"uni-app": "^3.6.5",
"uni-app-x": ""
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "插件不采集任何数据",
"permissions": "无"
},
"npmurl": "",
"darkmode": "x",
"i18n": "x",
"widescreen": "x"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "√",
"vue3": "√"
},
"web": {
"safari": "√",
"chrome": "√"
},
"app": {
"vue": "√",
"nvue": "√",
"android": "√",
"ios": "√",
"harmony": "√"
},
"mp": {
"weixin": "√",
"alipay": "√",
"toutiao": "√",
"baidu": "√",
"kuaishou": "√",
"jd": "√",
"harmony": "√",
"qq": "√",
"lark": "√"
},
"quickapp": {
"huawei": "√",
"union": "√"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* 小程序工具
*/
import { getQueryParams } from './h5Utils.js'
// 跳转视频号直播
export const toChannelsLive = (finderUserName = '') => {
// #ifdef MP-WEIXIN
wx.getChannelsLiveInfo({
finderUserName,
success: info => {
const { feedId, nonceId } = info
wx.openChannelsLive({
finderUserName,
feedId,
nonceId,
fail: (err) => {
console.log(err);
uni.showToast({
title: err?.errMsg
})
}
})
},
fail: (err) => {
console.log(err);
uni.showToast({
title: err?.errMsg
})
}
})
// #endif
// #ifndef MP-WEIXIN
console.error('只支持在微信小程序中调用')
// #endif
}
// 获取小程序分享参数
export const getWxShareParams = (queryParams, title, imageUrl, path) => {
const { appName } = uni.getSystemInfoSync()
const pages = getCurrentPages()
const { route, options, $page } = pages[pages.length - 1]
const params = queryParams || options || getQueryParams($page?.fullPath)
title = title || appName
path = path || '/' + route
path += Object.keys(params).reduce((prev, curr) => curr ? `${prev}${curr}=${params[curr]}&` : prev, path.includes('?') ? '&' : '?').slice(0, -1)
console.log('getWxShareParams', title, path, imageUrl);
return {
title,
path,
imageUrl
}
}
// 获取场景参数
export const getSceneParams = () => {
const { query } = uni.getLaunchOptionsSync()
return getQueryParams(decodeURIComponent(query.scene))
}

View File

@@ -0,0 +1,57 @@
export class CodeUtil {
codeType = []
constructor(codeType) {
if (!Array.isArray(codeType)) {
console.error('codeType should be an array')
return
}
this.codeType = codeType
}
scan(options = {}) {
return uni.scanCode(options)
}
async getCodeInfo(options) {
const { scanType, result } = await this.scan(options)
return this.parseCode(result)
}
makeCode(type, content) {
const t = this.codeType.indexOf(type)
if (t == -1) {
console.error(`codeType ${type} is undefined`, this.codeType)
return null
}
return JSON.stringify({
t,
c: content
})
}
parseCode(codeInfo) {
try {
const info = JSON.parse(codeInfo)
const { t, c } = info
const type = this.codeType[t]
if (type) return { type, content: c }
return { type: 'unknown', content: info }
} catch (err) {
console.error('parseCode error', err)
return { type: 'error', content: codeInfo }
}
}
}
export const codeUtil = new CodeUtil([])

View File

@@ -0,0 +1,18 @@
export const commonProps = {
bgColor: {
type: String,
default: '#fff'
},
customStyle: {
type: Object,
default: () => ({})
}
}
export const str2px = str => {
if (!str) return 0
str = String(str)
if (str.endsWith('rpx')) return uni.upx2px(parseInt(str))
if (str.endsWith('px')) return parseInt(str)
return parseInt(str)
}

View File

@@ -0,0 +1,227 @@
/**
* H5工具
*/
// 线上调试
export const debug = async (tip = true) => {
const erudaList = [
'https://unpkg.com/eruda',
'https://cdn.jsdelivr.net/npm/eruda',
'/eruda.js',
]
let idx = 0
while (erudaList[idx]) {
try {
await loadScript(erudaList[idx])
eruda.init()
if (tip) alert('debug ready')
return Promise.resolve('加载成功')
} catch (e) {
idx++
}
}
return Promise.reject('加载失败')
}
// 获取查询参数
export const getQueryParams = (url) => {
url = url || window?.location?.href
const queryString = String(url).split('?').at(-1);
if (!queryString) {
return {};
}
return queryString.split('&').reduce((params, param) => {
const [key, value] = param.split('=');
if (key && value) params[key] = decodeURIComponent(value);
return params;
}, {});
}
// 判断浏览器
export const judgeBrowser = () => {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes('micromessenger')) return 'wx'
if (userAgent.includes('alipay')) return 'ali'
return 'unknown'
}
// 加载 js script
export const loadScript = (src) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = (error) => reject(error);
document.head.appendChild(script);
});
}
/**
* 微信公众号相关
*/
// 微信公众号授权获取 code
export const getWxAuthCode = (appId, state = '', scope = 'snsapi_base') => {
/**
* 以snsapi_base为scope发起的网页授权是用来获取进入页面的用户的openid的
* 并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
* 以snsapi_userinfo为scope发起的网页授权是用来获取用户的基本信息的。但这种授权需要用户手动同意
* 并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息。
*/
const redirect_uri = encodeURIComponent(window.location.href);
const authUrl = `http://open.weixin.qq.com/connect/oauth2/authorize
?appid=${appId}
&redirect_uri=${redirect_uri}
&response_type=code
&scope=${scope}
&state=${state}#wechat_redirect`;
window.location.href = authUrl
}
export const wxConfig = (params) => {
/**
* config信息验证后会执行ready方法所有接口调用都必须在config接口获得结果之后
* config是一个客户端的异步操作所以如果需要在页面加载时就调用相关接口
* 则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口
* 则可以直接调用不需要放在ready函数中。
*/
jWeixin.config({
debug: false,
appId: params.appId,
timestamp: params.timestamp,
nonceStr: params.noncestr,
signature: params.signature,
jsApiList: [
"updateTimelineShareData",
"updateAppMessageShareData",
"chooseWXPay",
]
});
}
// 微信公众号支付
export const wxPay = (params) => {
console.log('wxPay', params);
wxConfig(params)
return new Promise((resolve, reject) => {
jWeixin.ready(function() {
jWeixin.chooseWXPay({
timestamp: params.timeStamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType,
paySign: params.paySign,
success: function(res) {
console.log('支付成功:', res);
resolve(res)
},
cancel: function(res) {
console.log('用户取消支付:', res);
reject(res)
},
fail: function(err) {
console.error('支付失败:', err);
reject(err)
}
});
});
wx.error(function(res) {
reject(res)
})
})
};
// 微信公众号设置分享信息
export const wxShare = (params) => {
console.log('wxShare', params);
//配置校验成功后执行
jWeixin.ready(function() {
if (!params.link) {
let url = window.location.href;
let index = url.indexOf("?");
if (index != -1) {
if (url.indexOf("#") != -1 && url.indexOf("#") > index) {
url = url.substring(0, index) + url.substring(url.indexOf("#"));
} else {
url = url.substr(0, index);
}
}
params.link = url;
}
// 自定义“分享给朋友”及“分享到QQ”按钮的分享内容
jWeixin.updateAppMessageShareData({
title: params.title, // 分享标题
desc: params.desc, // 分享描述
link: params.link, // 分享链接该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: params.imgUrl, // 分享图标
success: function() {
console.log('updateAppMessageShareData success');
},
fail: err => {
console.log('updateAppMessageShareData fail', err);
}
});
// 自定义“分享到朋友圈”及“分享到QQ空间”按钮的分享内容
jWeixin.updateTimelineShareData({
title: params.title, // 分享标题
link: params.link, // 分享链接该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl: params.imgUrl, // 分享图标
success: function() {
console.log('updateTimelineShareData success');
},
fail: err => {
console.log('updateTimelineShareData fail', err);
}
});
console.log('wxShare', params);
});
}
/**
* 支付宝相关
*/
// 支付宝生活号授权获取 code
export const getAliAuthCode = (appId, scope = 'auth_base') => {
/**
* auth_base(静默授权):静默授权,用户无需点击确认授权,默认返回 auth_code该授权码不支持获取用户信息。
* auth_user(主动授权):首次授权需要用户手动点击同意,用户同意后,返回 auth_code
* 商家需要考虑用户拒绝授权的情况并进行相应容错。如果授权关系依旧存在,下次进入页面时也会静默授权。
*/
return new Promise((resolve, reject) => {
ap.getAuthCode({
appId,
scopes: [scope],
}, function(res) {
if (res.error) {
reject(res)
} else {
resolve(res)
}
});
})
}
// 支付宝生活号支付
export const aliPay = (tradeNO) => {
return new Promise((resolve, reject) => {
console.log('aliPay', tradeNO);
ap.tradePay({
tradeNO,
success: res => {
if (res.resultCode === '9000') {
resolve(res)
} else {
reject(res)
}
},
fail: err => {
reject(err)
},
});
})
}

View File

@@ -0,0 +1,321 @@
export * as sugar from './sugar'
export * as router from './router'
export * as h5Utils from './h5Utils'
export * as appletUtils from './appletUtils'
export * from './retryAsync'
import pagesJson from '@/pages.json'
export const isPromise = (obj) => {
return !!obj && (typeof obj === "object" || typeof obj === "function") && typeof obj.then === "function";
}
let promisifyFlag = false
export const promisify = () => {
if (promisifyFlag) return
promisifyFlag = true
// #ifdef VUE2
uni.addInterceptor({
returnValue(res) {
if (isPromise(res)) {
return new Promise((resolve, reject) => {
res.then((res) => {
if (Array.isArray(res)) {
if (res[0]) reject(res[0])
else resolve(res[1])
} else {
if (typeof res == 'object' && String(res.errMsg).includes(':ok')) {
resolve(res)
} else {
reject(res)
}
}
});
});
}
return res;
},
});
// #endif
}
// 获取类型
export const _typeof = val => Object.prototype.toString.call(val).slice(8, -1).toLowerCase()
// 休眠
export const sleep = ms => new Promise(r => setTimeout(r, ms))
// 获取当前页面信息
export const getCurrentPage = () => {
const pages = getCurrentPages()
return pages[pages.length - 1] || {}
}
// 获取当前页面 path
export const getCurrentPagePath = (fullPath = false) => {
const { route, $page } = getCurrentPage()
return fullPath ? $page.fullPath : '/' + route
}
// 判断当前页或指定 path 是否是 tabbar 页面
export const isTabBarPage = path => {
const { tabBar } = pagesJson
if (!tabBar) return false
path = path || getCurrentPagePath()
return !(!tabBar || !tabBar.list || !tabBar.list.length || tabBar.list.findIndex(i => '/' + i.pagePath == path) == -1)
}
// 获取应用运行页面信息
export const getPageInfo = () => {
// 获取 pages 并去重
const pages = getCurrentPages().reduce((p, c) => {
if (!p.find(f => f.route == c.route)) p.push(c)
return p
}, [])
// 启动页
const startPage = pagesJson.pages[0].path
// tabBar
const tabBarList = pagesJson.tabBar?.list || []
// 首页
const [page] = pages
return {
currentPage: pages[pages.length - 1] || {},
startPage,
isSharePage: pages.length == 1 && page.route != startPage && !tabBarList.some(i => i.pagePath == page.route),
pageStackLen: pages.length,
}
}
/**
* @desc 倒计时
* @func countDown
* @param {String | Number} endTime 结束时间
* @param {Function} callback 倒计时字符串更新回调函数 (info, isEnd) => {}
* @param {String} formatStr 倒计时字符串显示格式 d=天, h=时, m=分, s=秒, 默认(hh:mm:ss)
* @param {Number} refreshRate 更新频率 默认(100)
* @return {Number} 定时器 id 用于结束倒计时
*/
export const countDown = function(endTime, callback, formatStr = 'hh:mm:ss', refreshRate = 100) {
const targetTime = new Date(endTime).getTime();
let cacheStr
const intervalId = setInterval(function() {
const currentTime = new Date().getTime();
const distance = targetTime - currentTime;
const date = {
d: Math.floor(distance / (1000 * 60 * 60 * 24)),
h: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
m: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)),
s: Math.floor((distance % (1000 * 60)) / 1000)
}
if (!formatStr.includes('d')) {
date.h += date.d * 24;
date.d = 0;
}
if (!formatStr.includes('h')) {
date.m += date.h * 60;
date.h = 0;
}
if (!formatStr.includes('m')) {
date.s += date.m * 60;
date.m = 0;
}
let str = formatStr
Object.keys(date).forEach(key => {
if (date[key] < 0) date[key] = 0
if (str.includes(key)) {
if (str.includes(key + key)) {
str = str.replace(key + key, date[key].toString().padStart(2, 0))
} else {
str = str.replace(key, date[key])
}
}
})
if (cacheStr != str) {
cacheStr = str
if (date.d + date.h + date.m + date.s <= 0) {
clearInterval(intervalId);
callback && callback({ str, ...date }, true)
} else {
callback && callback({ str, ...date }, false)
}
}
}, refreshRate);
return intervalId
}
/**
* @desc 轮询函数(等待结果)
* @func pollingForResult
* @param {Function} fun 返回值为 Promise 的函数
* @param {Array} params fun函数调用的参数列表 ( 例: [param,param] )
* @param {Function} judgeFun 拿到 fun 函数返回结果 res, 然后调用 judgeFun(res), judgeFun函数应返回 success 或 fail , 没有返回值即忽略本次
* @param {Number} ms 最快多长时间调用一次 ( 默认 1000ms )
* @param {Number} timeout 超时时间 ( 默认 1min )
* @return {Promise} judgeFun 函数返回 success 或 fail 的调用结果
*/
export const pollingForResult = function(fun, params, judgeFun, ms = 1000, timeout = 1000 * 60) {
// console.time('polling-time')
console.log('polling-params', arguments);
let startTime = Date.now()
let prevCallTime = 0
if (typeof judgeFun != 'function') throw new Error('judgeFun 参数应为一个 function');
if (!(params instanceof Array)) params = [params]
if (ms <= 0) ms = 1
return new Promise((resolve, reject) => {
function fun_call() {
if ((prevCallTime + ms) > Date.now()) {
setTimeout(() => {
fun_call()
}, (prevCallTime + ms) - Date.now());
return
}
if ((timeout + startTime) < Date.now()) {
reject('Timeout')
return
}
prevCallTime = Date.now()
// console.time('fun-call-time')
fun(...params).then(res => {
console.log('fun-call-res', res);
// console.timeEnd('fun-call-time')
if (judgeFun(res) === 'success') {
resolve(res)
// console.timeEnd('polling-time')
} else if (judgeFun(res) === 'fail') {
reject(res)
// console.timeEnd('polling-time')
} else {
fun_call()
}
}).catch(err => {
console.log('fun-call-err', err);
// console.timeEnd('fun-call-time')
fun_call()
})
}
fun_call()
})
}
/**
* @desc 轮询函数(获取数据)
* @func pollingForData
* @param {Function} fun 返回值为 Promise 的函数
* @param {Array} params fun函数调用的参数列表 ( 例: [param,param] )
* @param {Function} callback 每次调用的结果 ( 例: (err, res, count) => {} )
* @param {Number} ms 最快多长时间调用一次 ( 默认 1000ms )
* @return {Function} 结束轮询函数
*/
export const pollingForData = function(fun, params, callback, ms = 1000) {
console.log('polling-params', arguments);
let prevCallTime = 0
let endFlag = false
let count = 0
if (typeof callback != 'function') throw new Error('callback 参数应为一个 function');
if (!(params instanceof Array)) params = [params]
if (ms <= 0) ms = 1
function fun_call() {
if (endFlag) return
if ((prevCallTime + ms) > Date.now()) {
setTimeout(() => {
fun_call()
}, (prevCallTime + ms) - Date.now());
return
}
prevCallTime = Date.now()
count++
fun(...params).then(res => {
callback && callback(null, res, count)
fun_call()
}).catch(err => {
callback && callback(err, null, count)
fun_call()
})
}
fun_call()
return () => {
console.log('polling-end');
endFlag = true
}
}
/**
* @desc 深拷贝
* @func deepClone
* @param {Object} target 被拷贝的对象
* @return {Object} 新对象
*/
export const deepClone = (target, map = new WeakMap()) => {
if (target === null || typeof target !== 'object') {
return target;
}
if (target instanceof Date) {
return new Date(target);
}
if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
}
if (target instanceof Map) {
const newMap = new Map();
target.forEach((value, key) => {
newMap.set(deepClone(key, map), deepClone(value, map));
});
return newMap;
}
if (target instanceof Set) {
const newSet = new Set();
target.forEach(value => {
newSet.add(deepClone(value, map));
});
return newSet;
}
if (typeof target === 'function') {
return target;
}
if (map.has(target)) {
return map.get(target);
}
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
const symbolKeys = Object.getOwnPropertySymbols(target);
if (symbolKeys.length > 0) {
symbolKeys.forEach(symKey => {
cloneTarget[symKey] = deepClone(target[symKey], map);
});
}
for (let key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}

View File

@@ -0,0 +1,112 @@
/**
* 异步操作重试工具函数
* @param {Function} asyncFn - 需要重试的异步函数
* @param {Object} options - 重试配置选项
* @param {number} options.maxRetries - 最大重试次数 (默认: 3)
* @param {number} options.delay - 重试延迟时间(ms) (默认: 1000)
* @param {string} options.backoffStrategy - 退避策略 ('fixed' | 'exponential' | 'linear') (默认: 'fixed')
* @param {number} options.backoffFactor - 退避因子 (默认: 2)
* @param {Function} options.shouldRetry - 自定义重试条件函数 (默认: 所有错误都重试)
* @param {Function} options.onRetry - 重试回调函数
* @param {number} options.timeout - 单次操作超时时间(ms)
* @returns {Promise} 返回异步操作的结果
*/
export async function retryAsync(asyncFn, options = {}) {
const {
maxRetries = 3,
delay = 1000,
backoffStrategy = 'fixed',
backoffFactor = 2,
shouldRetry = () => true,
onRetry = () => {},
timeout
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// 如果设置了超时时间,包装函数添加超时控制
const result = timeout ?
await withTimeout(asyncFn, timeout) :
await asyncFn();
return result;
} catch (error) {
lastError = error;
// 如果是最后一次尝试,直接抛出错误
if (attempt === maxRetries) {
throw error;
}
// 检查是否应该重试
if (!shouldRetry(error, attempt + 1)) {
throw error;
}
// 执行重试回调
onRetry(error, attempt + 1, maxRetries);
// 计算延迟时间并等待
const waitTime = calculateDelay(delay, attempt, backoffStrategy, backoffFactor);
await sleep(waitTime);
}
}
throw lastError;
}
/**
* 计算重试延迟时间
*/
function calculateDelay(baseDelay, attempt, strategy, factor) {
switch (strategy) {
case 'exponential':
return baseDelay * Math.pow(factor, attempt);
case 'linear':
return baseDelay * (attempt + 1);
case 'fixed':
default:
return baseDelay;
}
}
/**
* 延迟函数
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 为异步函数添加超时控制
*/
function withTimeout(asyncFn, timeoutMs) {
return new Promise((resolve, reject) => {
let isResolved = false;
const timer = setTimeout(() => {
if (!isResolved) {
isResolved = true;
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
}
}, timeoutMs);
asyncFn()
.then(result => {
if (!isResolved) {
isResolved = true;
clearTimeout(timer);
resolve(result);
}
})
.catch(error => {
if (!isResolved) {
isResolved = true;
clearTimeout(timer);
reject(error);
}
});
});
}

View File

@@ -0,0 +1,91 @@
import { deepClone } from './index.js'
const delayCall = (fun, timeout) => {
if (timeout) setTimeout(fun, timeout)
else fun()
}
export const navBackAndEvent = (eventChannel, backData, timeout = 0, delta = 1) => {
delayCall(() => {
eventChannel.emit('backData', backData)
uni.navigateBack({
delta
})
}, timeout)
}
export const navToAndEvent = (url, data, acceptCallback, timeout = 0) => {
return new Promise(resolve => {
delayCall(() => {
uni.navigateTo({
url,
events: {
backData: data => {
acceptCallback && acceptCallback(data)
resolve(data)
}
},
success: ({ eventChannel }) => {
if (eventChannel && eventChannel.emit) {
eventChannel.emit('navData', deepClone(data))
}
},
fail: console.log,
})
}, timeout)
})
}
export const navTo = (url, options, timeout = 0) => {
delayCall(() => {
uni.navigateTo({
url,
fail: console.log,
...options
})
}, timeout)
}
export const redTo = (url, timeout = 0) => {
delayCall(() => {
uni.redirectTo({
url,
fail: console.log
})
}, timeout)
}
export const clearTo = (url, timeout = 0) => {
delayCall(() => {
uni.reLaunch({
url,
fail: console.log
})
}, timeout)
}
export const tabTo = (url, timeout = 0) => {
delayCall(() => {
uni.switchTab({
url,
fail: console.log
})
}, timeout)
}
export const shTab = (url, timeout = 0) => {
delayCall(() => {
uni.switchTab({
url,
fail: console.log
})
}, timeout)
}
export const navBack = (timeout = 0, delta = 1) => {
delayCall(() => {
uni.navigateBack({
delta
})
}, timeout)
}

View File

@@ -0,0 +1,213 @@
export const log = console.log
import { t } from '@/utils/i18n'
export const upx2px = val => uni.upx2px(parseInt(val))
export const toast = (title, options = { duration: 2000, icon: 'none' }) => uni.showToast({ title, fail: console.log, ...options })
export const makePhoneCall = phoneNumber => uni.makePhoneCall({ phoneNumber, fail: console.log });
export const previewImage = (urls, current, options) => uni.previewImage({ urls, current, fail: console.log, ...options });
export const copy = (str, options) => uni.setClipboardData({ data: String(str), fail: console.log, ...options })
export const openMap = (lng, lat, name, address, options) => uni.openLocation({ longitude: parseFloat(lng), latitude: parseFloat(lat), name, address, fail: console.log, ...options })
export const setStorage = (key, value) => uni.setStorageSync(key, value)
// 判断空值
export const isEmpty = value => {
if (value === null || value === undefined || value === '') {
return true;
}
if (Array.isArray(value)) {
return value.length === 0;
}
if (typeof value === 'object') {
return Object.keys(value).length === 0;
}
return false;
}
// 返回对象属性为空的 property path
const getEmptyValuePropertyPath = (obj, excludedKeys = []) => {
for (let key in obj) {
if (obj.hasOwnProperty(key) && !excludedKeys.includes(key)) {
let value = obj[key];
if (Array.isArray(value)) {
// 判断是否为数组对象
for (let item of value) {
const childrenKey = getEmptyValuePropertyPath(item, excludedKeys)
if (item instanceof Object && childrenKey) {
return key + '.' + childrenKey;
}
}
if (value.length === 0) {
return key;
}
} else if (value instanceof Object) {
const childrenKey = getEmptyValuePropertyPath(value, excludedKeys)
if (childrenKey) {
return key + '.' + childrenKey;
}
} else if (isEmpty(value)) {
return key;
}
}
}
return null;
}
// 有无空字段
export const hasEmptyField = (obj, excludedKeys = []) => getEmptyValuePropertyPath(obj, excludedKeys)
// 跳转企业微信客服
export const toCustomerService = (corpId, url) => {
// #ifdef MP-WEIXIN
wx.openCustomerServiceChat({
extInfo: {
url
},
corpId,
success(res) {},
fail: (err) => {
console.log(err);
uni.showToast({
title: err?.errMsg
})
}
})
// #endif
// #ifdef APP
plus.share.getServices(services => {
const sweixin = services.find(i => i.id === 'weixin')
if (sweixin) {
sweixin.openCustomerServiceChat({
corpid: corpId,
url,
}, res => {
console.log("success", JSON.stringify(res))
}, err => {
console.log("error", JSON.stringify(err))
})
} else {
plus.nativeUI.alert(t('Mobile.ThirdParty.XTools.wechatUnsupported'))
}
}, function() {
uni.showToast({
title: t('Mobile.ThirdParty.XTools.getServiceFailed'),
icon: 'error'
})
})
// #endif
// #ifdef H5
window.location.href = url
// #endif
}
// 保存图片
export const saveImage = async (url, tips = true) => {
try {
console.log('saveImage', url);
// #ifdef H5
const link = document.createElement('a');
link.href = url;
link.download = url.substring(url.lastIndexOf('/') + 1);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// #endif
// #ifndef H5
if (url.startsWith('http')) {
const { statusCode, tempFilePath: filePath } = await uni.downloadFile({
url
})
if (statusCode == 200) {
await uni.saveImageToPhotosAlbum({
filePath
})
} else {
throw Error('图片下载失败')
}
} else {
await uni.saveImageToPhotosAlbum({
filePath: url
})
}
// #endif
tips && uni.showToast({
title: t('Mobile.ThirdParty.XTools.saveSuccess'),
icon: 'success'
})
} catch (e) {
console.log(e);
tips && uni.showToast({
title: t('Mobile.ThirdParty.XTools.saveFailed'),
icon: 'error'
})
}
}
// 获取页面通信管道(支持 vue2 需绑定this
export const pageEvent = function() {
// #ifdef VUE2
return this.getOpenerEventChannel()
// #endif
// #ifdef VUE3
console.error('vue3 不支持这种使用方式')
// #endif
}
// 对象转查询字符串
export const object2queryStr = (obj) => {
if (!obj) return obj
return Object.keys(obj).reduce((val, key) => {
return `${val}${key}=${obj[key]}&`
}, '?').slice(0, -1)
}
// html 转纯文本
export const html2text = (html) => {
return String(html).replace(/<[^>]+>|&[^;]+;/g, '').replace(/\s+/g, ' ').trim();
}
/**
* @Func dataMask
* @Desc 数据脱敏
* @param {String} str 字符串
* @param {Number} head 头部保留位数
* @param {Number} tail 尾部保留位数
* @return {String} 脱敏后的数据
*/
export const dataMask = (str, head, tail) => {
if (!str) return str
const arr = String(str).split('')
tail = arr.length - tail
if (head < 0) head = 0
if (head > tail) return str
for (let i = head; i < tail; i++) {
arr[i] = '*'
}
return arr.join('')
}
// 查询元素信息
export const queryElementRect = function(selector) {
if (Array.isArray(selector)) return Promise.all(selector.map(i => queryElementRect.call(this, i)))
return new Promise(resolve => {
uni.createSelectorQuery()
.in(this).select(selector)
.boundingClientRect(resolve)
.exec();
})
}