跳转到内容

二次封装能力

使用文件系统

js
// file-storage.js
const isAlipay = uni.getSystemInfoSync().platform === 'alipay';

// 获取文件系统管理器
function getFileSystemManager() {
	return isAlipay ? my.getFileSystemManager() : uni.getFileSystemManager();
}

// 获取用户数据路径
function getUserDataPath() {
	return isAlipay ? my.env.USER_DATA_PATH : wx.env.USER_DATA_PATH;
}

// 写入文件
export function writeFile(fileName, data) {
	const fs = getFileSystemManager();
	const filePath = `${getUserDataPath()}/${fileName}`;
	const content = typeof data === 'string' ? data : JSON.stringify(data);

	return new Promise((resolve, reject) => {
		fs.writeFile({
			filePath,
			data: content,
			encoding: 'utf8',
			success: resolve,
			fail: reject,
		});
	});
}

// 读取文件
export function readFile(fileName) {
	const fs = getFileSystemManager();
	const filePath = `${getUserDataPath()}/${fileName}`;

	return new Promise((resolve, reject) => {
		fs.readFile({
			filePath,
			encoding: 'utf8',
			success(res) {
				try {
					resolve(JSON.parse(res.data));
				} catch (e) {
					resolve(res.data);
				}
			},
			fail: reject,
		});
	});
}

// 检查文件是否存在
export function access(fileName) {
	const fs = getFileSystemManager();
	const filePath = `${getUserDataPath()}/${fileName}`;

	return new Promise((resolve) => {
		fs.access({
			path: filePath,
			success: () => resolve(true),
			fail: () => resolve(false),
		});
	});
}
js
let res;
try {
	// 获取页面数据
	res = await this.$Req.getApp('/app/getPageInfo', params, undefined, undefined, 3000);
	uni.stopPullDownRefresh();
	if (uni.getStorageSync('homeStorage')) {
		console.error('模拟获取页面接口失败');
		throw new Error('获取页面接口失败');
	}
	// 对页面进行赋值
	if (res.success) {
		// 备份数据
		res.source = 'interface';
		await writeFile(this.fileName, res);
	}
} catch (err) {
	const isAccess = await access(this.fileName);
	if (isAccess) {
		res = await readFile(this.fileName);
		res.source = 'storage';
	}
} finally {
	console.log(res, 'pageData');

	if (res.success) {
		this.pageInfo = res.data.page;
		this.fileHost = res.data.fileHost;
		this.propList = res.data.propList;
		this.style = res.data.style;
	}
	await uni.vuex('vuexHomeSource', res.source);
}

获取扫码参数

js
import MztUtils from '@/utils/mzt-utils';

class HandleFunc {
	/**
	 * 获取微信、支付宝扫码参数并进行处理。
	 * 提取出 URL 后,会进一步解析出 query 参数对象,并通过回调返回。
	 *
	 * @param {Object} options - 小程序页面 onLoad(options) 中的参数对象。
	 * @param {Function} [callback] - 处理解析后参数的回调函数。
	 * @param {string} callback.url - 扫码跳转的完整 URL。
	 * @param {Object} callback.paramsObj - 从 URL 中解析出的参数对象(key-value)。
	 */
	static scanParams(options, callback) {
		if (MztUtils.isEmpty(options)) {
			return;
		}
		let url;
		let paramsObj;
		// #ifdef MP-ALIPAY
		if (options.query && MztUtils.isNotEmpty(options.query.qrCode)) {
			url = options.query.qrCode;
		}
		// #endif
		// #ifdef MP-WEIXIN
		if (MztUtils.isNotEmpty(options.q)) {
			url = decodeURIComponent(options.q);
		}
		// #endif
		if (MztUtils.isNotEmpty(url)) {
			paramsObj = MztUtils.getQueryParams(url);
			callback && callback(url, paramsObj);
		}
	}

	/**
	 * 获取全局扫码参数
	 */
	static getLaunchOptions() {
		let options;
		try {
			// #ifdef MP-ALIPAY
			options = my.getLaunchOptionsSync();
			// #endif
			// #ifdef MP-WEIXIN
			options = wx.getLaunchOptionsSync()?.query;
			// #endif
			return options;
		} catch (e) {
			console.log('获取扫码参数错误');
			return null;
		}
	}
}

export default HandleFunc;
js
onLoad();
{
	// 获取全局扫码参数
	let scan_options = HandleFunc.getLaunchOptions();
	console.log(scan_options, 'scan_options');
	try {
		HandleFunc.scanParams(scan_options, (url, obj) => {
			console.log(url, obj);
			this.qrCodeId = obj.qrCodeId || '';
		});
	} catch (e) {
		uni.$u.toast('获取扫码数据失败');
	}
}

判断是否开启相机权限

js
/**
 * 判断是否开启系统摄像头权限
 * @returns {Promise<unknown>}
 */
export function hasCameraAccess() {
	return new Promise((resolve, reject) => {
		// #ifdef MP-ALIPAY
		my.getSystemInfo({
			success: (res) => {
				resolve(res.cameraAuthorized);
			},
		});
		// #endif
		// #ifdef MP-WEIXIN
		uni.authorize({
			scope: 'scope.camera',
			success: () => {
				resolve(true);
			},
			fail: () => {
				resolve(false);
			},
		});
		// #endif
	});
}

/**
 * 展示系统设置弹窗
 */
export function showCameraAuthGuide() {
	uniApi.showCustomModal(
		{
			msg: '检测到您未开启相机权限,该服务需通过人脸核验确保账户安全。请开启相机使用权限',
			confirmText: '去设置',
		},
		() => {
			// #ifdef MP-WEIXIN
			uni.openSetting({
				success(settingRes) {
					if (settingRes.authSetting['scope.camera']) {
						console.log('用户已授权摄像头权限');
					} else {
						console.log('用户未授权摄像头权限');
					}
				},
			});
			// #endif
			// #ifdef MP-ALIPAY
			my.showAuthGuide({
				authType: 'CAMERA',
				success: (res) => {
					console.log('用户已授权摄像头权限');
				},
				fail: (error) => {
					console.log('用户未授权摄像头权限');
				},
			});
			// #endif
		}
	);
}
js
import {hasCameraAccess} from "@/utils/camera_permission.js";
// 判断是否有相机权限
const isCamerAccess = await hasCameraAccess();
if (!isCamerAccess))
{
  // 开启提醒
  showCameraAuthGuide();
}

工具方法

js
let uniApi = {
	/**
	 * 通用对话框
	 * @param options
	 * @param confirmback
	 * @param cancelCallback
	 */
	showCustomModal(options, confirmback, cancelCallback = null) {
		uni.showModal({
			title: options.title || '温馨提示',
			content: options.msg || '',
			confirmText: options.confirmText || '确认',
			cancelText: options.cancelText || '取消',
			showCancel: options.showCancel !== undefined ? options.showCancel : true,
			success: (res) => {
				if (res.confirm) {
					confirmback && confirmback();
				} else if (res.cancel) {
					cancelCallback && cancelCallback();
				}
			},
		});
	},

	// 通用跳转
	router(url, type = 'navigateTo') {
		uni[type]({ url });
	},

	/**
	 * 获取胶囊信息
	 * @returns {Object}
	 */
	getMenuButtonInfo() {
		const menuButtonInfo = uni.getMenuButtonBoundingClientRect();
		return {
			width: menuButtonInfo.width,
			top: menuButtonInfo.top,
			height: menuButtonInfo.height,
		};
	},

	/**
	 * 获取小程序当前版本号
	 * @returns {Promise<unknown>}
	 */
	getMiniVersion() {
		return new Promise(async (resolve, reject) => {
			let version = '';
			const systemInfo = await getSystemInfo();
			version = systemInfo.appVersion;
			const accountInfo = uni.getAccountInfoSync();
			if (accountInfo.miniProgram.version) {
				version = accountInfo.miniProgram.version;
			}
			resolve(version);
		});
	},

	/**
	 * 获取当前平台的版本
	 * @returns {Promise<unknown>}
	 */
	async getEnvVersion() {
		return new Promise(async (resolve, reject) => {
			let dict = {
				release: '正式版',
				trial: '体验版',
				develop: '开发版',
			};
			try {
				// #ifdef MP-WEIXIN
				resolve({
					v: __wxConfig.envVersion,
					d: dict[__wxConfig.envVersion],
					s: 'wechat',
				});
				// #endif
				// #ifdef MP-ALIPAY
				const res = await my.getRunScene();
				resolve({
					v: res.envVersion,
					d: dict[res.envVersion],
					s: 'alipay',
				});
				// #endif
			} catch (error) {
				reject(error);
			}
		});
	},
};

export default uniApi;

选择系统相册并上传

js
/**
 * 选择系统相册
 * @param {Function} callback
 * @param {Object} params
 */
function selectSystemPhoto(callback, params) {
	uni.chooseImage({
		...params,
		count: params?.count ?? 1,
		sizeType: params?.sizeType ?? ['original', 'compressed'],
		sourceType: params?.sourceType ?? ['album'],
		success: function (res) {
			callback && callback(res);
		},
	});
}
vue
<template>
	<div></div>
</template>
<script>
export default {
	methods: {
		uploadAvatar() {
			selectSystemPhoto(async (res) => {
				console.log(res, 'res');
				this.avatarFilePath = res.tempFilePaths[0]; // 用于图片临时回显
				let uploadRes = await uploadPhoto(this.avatarFilePath); // 上传
				if (uploadRes.success) {
					this.$toast.success('上传成功');
				}
			});
		},
	},
};
</script>
js
export async function uploadPhoto(filePath) {
	uni.showLoading({
		title: '加载中',
	});
	const url = `${BASE_URL}/sys-file/upload`; // 后端实际接口
	return new Promise((resolve, reject) => {
		try {
			uni.uploadFile({
				url,
				filePath: filePath,
				name: 'file', // 后端接收file的字段
				header: request.getHeaders(), // 请求头 token等等
				success: async (uploadFileRes) => {
					const { statusCode, data } = uploadFileRes;
					if (statusCode !== 200) {
						handleStatus(statusCode);
						reject(Result.fail('上传失败'));
					}
					resolve(Result.ok(JSON.parse(data).data));
				},
			});
		} catch (e) {
			reject(Result.fail('上传失败'));
		} finally {
			uni.hideLoading();
		}
	});
}

在线图片转本地缓存图片

js
/**
 * 图片缓存
 * @param url - https 地址
 * @param cache - 缓存对象
 * @param options - 配置项
 * @return {Promise<Result>}
 */
function getCachedImage(url, cache, options = {}) {
	const { forceUpdate = false } = options;

	return new Promise((resolve, reject) => {
		try {
			// 缓存存在且 URL 一致,且非强制更新
			if (cache && cache.url === url && !forceUpdate) {
				console.log('使用缓存图片');
				return resolve(Result.ok(cache));
			}

			// 下载图片
			uni.downloadFile({
				url,
				success: (res) => {
					if (res.statusCode !== 200) {
						return reject(Result.fail('图片下载失败'));
					}

					const fs = uni.getFileSystemManager();
					// 使用唯一文件名,避免删除旧文件
					const savePath = `${wx.env.USER_DATA_PATH}/${Date.now()}_${Math.random().toString(36).slice(2)}.png`;

					saveFile(res.tempFilePath, savePath, url, resolve, reject);
				},
				fail: (err) => {
					console.error('下载头像失败', err);
					reject(Result.fail('下载失败'));
				},
			});
		} catch (e) {
			reject(Result.fail(e.message || '获取缓存失败'));
		}
	});
}

// 保存文件封装
function saveFile(tempFilePath, savePath, url, resolve, reject) {
	const fs = uni.getFileSystemManager();
	fs.saveFile({
		tempFilePath,
		filePath: savePath,
		success: (saveRes) => {
			// 缓存对象的数据结构(path:本地缓存地址,url:真实图片地址)
			const cacheData = { path: saveRes.savedFilePath, url };
			resolve(Result.ok(cacheData));
		},
		fail: (err) => {
			console.error('保存头像失败', err);
			reject(Result.fail('保存头像失败'));
		},
	});
}
js
import Request from '@/api/request';
import { handleErrorCode } from '@/utils/common';
import { isKit } from 'y-js-kit';
import { getCachedImage } from '@/utils/uni-api';

export default {
	namespaced: true,
	state: {
		avatar_cache: null,
		login_info: null,
		user_info: null,
	},
	mutations: {
		setLoginInfo(state, login_info) {
			state.login_info = login_info;
		},

		setUserInfo(state, user_info) {
			state.user_info = user_info;
		},

		setAvatarCache(state, cache) {
			state.avatar_cache = cache;
		},
	},
	actions: {
		async updateUserInfo({ commit }, fieldsObj) {
			try {
				let res = await Request.post('/api_user/updateInfo', fieldsObj);
				console.log(res, 'res');
				if (res.ok) {
					uni.$u.toast('修改成功!');
				}
			} catch (e) {
				uni.$u.toast('修改用户信息失败,请稍后再试~');
			}
		},

		setLoginInfo({ commit }, login_info) {
			commit('setLoginInfo', login_info);
		},

		async setUserInfo({ commit, getters, dispatch }) {
			if (!getters.isLogin) {
				return;
			}
			let user_info = null;
			try {
				let res = await Request.get('/api_user/info', null);
				if (res.ok) {
					user_info = res.data;
					const avatar = user_info?.appUser.avatar;
					if (isKit.isNotEmpty(avatar)) {
						// 缓存头像
						await dispatch('setAvatarCache', avatar);
					}
				}
			} catch (e) {
				console.log(e);
				handleErrorCode({ code: 0, msg: e.msg || '未知错误' }, false);
			} finally {
				commit('setUserInfo', user_info);
			}
		},

		async setAvatarCache({ commit, getters }, url) {
			if (!url) return null;

			try {
				const res = await getCachedImage(url, getters.avatarCache);
				if (res.success && res.data) {
					const cacheData = res.data;
					commit('setAvatarCache', cacheData);

					return cacheData;
				}
			} catch (e) {
				console.error('缓存头像失败', e);
			}

			return null;
		},

		logout({ commit }) {
			commit('setLoginInfo', null);
			commit('setUserInfo', null);
			commit('setAvatarCache', null);
		},
	},
	getters: {
		avatarCache: (state) => state.avatar_cache,
		isLogin: (state) => !!state.login_info,
		userInfo: (state) => state.user_info?.appUser || {},
		accessToken: (state) => state.login_info?.access_token || '',
	},
};
js
function avatarImage() {
	let url = '';
	if (isKit.isNotEmpty(this.avatarCache)) {
		url = this.avatarCache.path;
	} else {
		url = '/static/mine/avatar.png';
	}
	return url;
}

获取系统键盘高度

js
/**
 * 获取系统键盘高度
 */
function getKeyBoardHeight(changeCallBack) {
	let safeBottom = 0;
	let systemInfoRes = uni.getSystemInfoSync();
	if (isNotEmpty(systemInfoRes) && isNotEmpty(systemInfoRes.safeAreaInsets) && isNotEmpty(systemInfoRes.safeAreaInsets.bottom)) {
		safeBottom = systemInfoRes.safeAreaInsets.bottom;
	}

	uni.onKeyboardHeightChange((e) => changeCallBack({ ...e, keyboardHeight: e.height + safeBottom }));

	return () => {
		uni.offKeyboardHeightChange();
	};
}

图片百分比宽度计算相应高度

js
/**
 * 计算正方形元素高度
 * @param {number} columns 一行几个元素
 * @param {number} margin 元素间距(rpx 或 px,根据实际单位)
 * @param {number} containerWidth 容器宽度,可选,不传则自动取屏幕宽度
 * @returns {string} 高度,单位与 containerWidth 保持一致
 */
function calcSquareItemHeight(columns = 3, margin = 15, containerWidth) {
	if (!containerWidth) {
		// 获取屏幕宽度(px)
		containerWidth = uni.getSystemInfoSync().screenWidth;
	}

	// 总间距 = 左右边距 + 元素间距
	const totalMargin = (columns - 1) * margin;
	const width = (containerWidth - totalMargin) / columns;

	return `${width}px`; // 返回字符串可直接绑定 style
}

封装uni.$xxx

js
import Vue from "vue";
import mztRequest from "@/http/mzt-request";
import MztUtils from "@/utils/mzt-utils";

class PageDialog {
  constructor() {
    this.state = Vue.observable({
      visible: false,
      list: [],
      data: {}
    });
  }

  async checkShow(pageId) {
    this.pageId = pageId;
    // 初始化弹窗数组
    this.state.list = await this.getList();
    // 获取弹窗数据
    this.state.data = this.isIncluded();
    if (MztUtils.isNotEmpty(this.state.data)) {
      this.setStatus(true);
    } else {
      this.setStatus(false);
    }
  }

  isIncluded() {
    if (this.state.list.length === 0) {
      return null;
    }

    return (
      this.state.list.find(item => {
        const ids = Array.isArray(item.layoutId)
          ? item.layoutId
          : String(item.layoutId || "").split(",");

        return ids.includes(this.pageId);
      }) || null
    );
  }

  async getList() {
    if (this.state.list.length > 0) {
      return this.state.list;
    }
    try {
      let res = await mztRequest.postApp("dialog/list");
      if (!res.success) {
        throw new Error('"请求错误"');
      }
      return res.data;
    } catch (e) {
      console.error("[弹窗error]" + e.message);
      return [];
    }
  }

  setStatus(status) {
    this.state.visible = status;
  }

  limitRules() {}
}
export const pageDialog = new PageDialog();

export default {
  install(Vue) {
    Vue.prototype.$dialog = pageDialog;

    if (!uni.$dialog) {
      uni.$dialog = pageDialog;
    }
  }
};
js
import Vue from "vue";
import PageDialog from "@/components/PageDialog/page-dialog";

Vue.use(PageDialog);
vue
await uni.$dialog.checkShow(this.pageInfo.id);
vue
<template>
  <u-popup
    :show="state.visible"
    :round="10"
    mode="center"
    :closeOnClickOverlay="true"
    @close=""
    customStyle="background-color:transparent;height:auto"
    :closeable="false"
  >
    <view class="image-dialog">
      <Image mode="aspectFit" :src="getImageUrl" />
      <u-icon
        name="close-circle"
        size="40"
        color="#dcdfe6"
        custom-style="margin-top:15px"
        @click="closeDialog"
      ></u-icon>
    </view>
  </u-popup>
</template>

<script>
export default {
  name: "PageDialog",
  data() {
    return {
      state: uni.$dialog.state
    };
  },
  computed: {
    getImageUrl() {
      const url = this.state.data.fileUrl;
      return url ? url.replace(/http:\/\//, "https://") : url;
    }
  },
  methods: {
    closeDialog() {
      uni.$dialog.setStatus(false);
    }
  }
};
</script>

<style scoped lang="scss">
.image-dialog {
  display: flex;
  flex-direction: column;
  align-items: center;

  Image {
    height: 200px;
  }
}
</style>

Will Try My Best.