初始化仓库

This commit is contained in:
wangxiaowei
2025-12-04 17:44:28 +08:00
commit 0ab8464612
302 changed files with 52014 additions and 0 deletions

4
src/utils/coupon.ts Normal file
View File

@ -0,0 +1,4 @@
export const CouponType = {
Discount: 1, // 优惠券
GroupBuy: 2 // 团购券
}

211
src/utils/index.ts Normal file
View File

@ -0,0 +1,211 @@
import { pages, subPackages } from '@/pages.json'
import { tabbarList } from '@/tabbar/config'
import { isMpWeixin } from './platform'
export function getLastPage() {
// getCurrentPages() 至少有1个元素所以不再额外判断
// const lastPage = getCurrentPages().at(-1)
// 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts但依然报错】
const pages = getCurrentPages()
return pages[pages.length - 1]
}
/**
* 获取当前页面路由的 path 路径和 redirectPath 路径
* path 如 '/pages/login/index'
* redirectPath 如 '/pages/demo/base/route-interceptor'
*/
export function currRoute() {
const lastPage = getLastPage()
const currRoute = (lastPage as any).$page
// console.log('lastPage.$page:', currRoute)
// console.log('lastPage.$page.fullpath:', currRoute.fullPath)
// console.log('lastPage.$page.options:', currRoute.options)
// console.log('lastPage.options:', (lastPage as any).options)
// 经过多端测试,只有 fullPath 靠谱,其他都不靠谱
const { fullPath } = currRoute as { fullPath: string }
// console.log(fullPath)
// eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序)
// eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5)
return getUrlObj(fullPath)
}
function ensureDecodeURIComponent(url: string) {
if (url.startsWith('%')) {
return ensureDecodeURIComponent(decodeURIComponent(url))
}
return url
}
/**
* 解析 url 得到 path 和 query
* 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor
* 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}}
*/
export function getUrlObj(url: string) {
const [path, queryStr] = url.split('?')
// console.log(path, queryStr)
if (!queryStr) {
return {
path,
query: {},
}
}
const query: Record<string, string> = {}
queryStr.split('&').forEach((item) => {
const [key, value] = item.split('=')
// console.log(key, value)
query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下可以兼容h5和微信y
})
return { path, query }
}
/**
* 得到所有的需要登录的 pages包括主包和分包的
* 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用
* 如果没有传 key则表示所有的 pages如果传递了 key, 则表示通过 key 过滤
*/
export function getAllPages(key = 'needLogin') {
// 这里处理主包
const mainPages = pages
.filter(page => !key || page[key])
.map(page => ({
...page,
path: `/${page.path}`,
}))
// 这里处理分包
const subPages: any[] = []
subPackages.forEach((subPageObj) => {
// console.log(subPageObj)
const { root } = subPageObj
subPageObj.pages
.filter(page => !key || page[key])
.forEach((page: { path: string } & Record<string, any>) => {
subPages.push({
...page,
path: `/${root}/${page.path}`,
})
})
})
const result = [...mainPages, ...subPages]
// console.log(`getAllPages by ${key} result: `, result)
return result
}
export function isCurrentPageTabbar() {
const routeObj = currRoute()
return tabbarList.some(item => `/${item.pagePath}` === routeObj.path)
}
export function getCurrentPageI18nKey() {
const routeObj = currRoute()
const currPage = pages.find(page => `/${page.path}` === routeObj.path)
if (!currPage) {
console.warn('路由不正确')
return ''
}
console.log(currPage)
console.log(currPage.style.navigationBarTitleText)
return currPage.style.navigationBarTitleText
}
/**
* 得到所有的需要登录的 pages包括主包和分包的
* 只得到 path 数组
*/
export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
/**
* 得到所有的需要登录的 pages包括主包和分包的
* 只得到 path 数组
*/
export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
/**
* 根据微信小程序当前环境,判断应该获取的 baseUrl
*/
export function getEnvBaseUrl() {
// 请求基准地址
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = ''
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = ''
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = ''
// 微信小程序端环境区分
if (isMpWeixin) {
const {
miniProgram: { envVersion },
} = uni.getAccountInfoSync()
switch (envVersion) {
case 'develop':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl
break
case 'trial':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl
break
case 'release':
baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl
break
}
}
return baseUrl
}
/**
* 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL
*/
export function getEnvBaseUploadUrl() {
// 请求基准地址
let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL
const VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = ''
const VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = ''
const VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = ''
// 微信小程序端环境区分
if (isMpWeixin) {
const {
miniProgram: { envVersion },
} = uni.getAccountInfoSync()
switch (envVersion) {
case 'develop':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl
break
case 'trial':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl
break
case 'release':
baseUploadUrl = VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl
break
}
}
return baseUploadUrl
}
export function getNavBarHeight() {
const systemInfo = uni.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight; // 状态栏高度单位px
const titleBarHeight = 44; // 默认导航栏标题高度iOS/Android 一般为 44px
const navbarHeight = statusBarHeight + titleBarHeight; // 完整的导航栏高度
console.log("🚀 ~ getNavBarHeight ~ navbarHeight:", navbarHeight)
return navbarHeight
}
export function getCapsuleOffset() {
let rightPadding: string = '24px'
// #ifdef MP-WEIXIN
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
rightPadding = menuButtonInfo.width + 16 + 'px'
// #endif
return rightPadding
}

303
src/utils/order.ts Normal file
View File

@ -0,0 +1,303 @@
// 预约服务分类
export enum ReserveServiceCategory {
ReserveRoom = 'ReserveRoom', // 预约茶室
GroupBuying = 'GroupBuying', // 团购
}
/** 售后服务分类 **/
// 个人原因
enum PersonalReasonEnum {
OverBought = 1, // 买多了/买错了
NoLongerNeeded = 2 // 计划有变,暂时不需要的
}
// 商家原因
enum MerchantReasonEnum {
CannotContact = 3, // 电话联系不上商家
NotServing = 4, // 商家营业但不接待
Renovating = 5 // 商家停止装修
}
// 个人原因映射
export const PersonalReasonMap: { label: string, value: PersonalReasonEnum }[] = [
{ label: '买多了/买错了', value: PersonalReasonEnum.OverBought },
{ label: '计划有变,暂时不需要的', value: PersonalReasonEnum.NoLongerNeeded }
]
// 商家原因映射
export const MerchantReasonMap: { label: string, value: MerchantReasonEnum }[] = [
{ label: '电话联系不上商家', value: MerchantReasonEnum.CannotContact },
{ label: '商家营业但不接待', value: MerchantReasonEnum.NotServing },
{ label: '商家停止装修', value: MerchantReasonEnum.Renovating }
]
// 售后原因映射(用于提交时的参数转换)
export const ReasonMap: Record<number, string> = {
[PersonalReasonEnum.OverBought]: '买多了/买错了',
[PersonalReasonEnum.NoLongerNeeded]: '计划有变,暂时不需要的',
[MerchantReasonEnum.CannotContact]: '已退款',
[MerchantReasonEnum.NotServing]: '申请售后',
[MerchantReasonEnum.Renovating]: '申请售后中'
}
/** 结束 **/
// 订单来源
export enum OrderSource {
Combo = 'combo', // TODO 团购套餐用于替代下面的Direct和Franchise
DouYin = 'douyin', // 抖音团购
TeaRoom = 'teaRoom', // 茶室
TeaSpecialist = 'teaSpecialist' // 茶艺师
}
// 订单来源对应名称
export const OrderSourceText: Record<OrderSource, string> = {
[OrderSource.Combo]: 'combo',
[OrderSource.DouYin]: '抖音',
[OrderSource.TeaRoom]: '茶室',
[OrderSource.TeaSpecialist]: '茶艺师',
}
// 订单状态
export enum OrderStatus {
Consuming = 'consuming', // 消费中(服务中)
Reserved = 'reserved', // 预约单(已预约)
Serving = 'serving', // 服务中(茶艺师订单独有的)
Pending = 'pending', // 待付款
Confirm = 'confirm', // 待确认
Finished = 'finished', // 已完结
Cancelled = 'cancelled', // 已取消
ToUse = 'toUse', // 待使用
Used = 'used', // 已使用(交易完成)
Refunded = 'refunded', // 已退款
AfterSaleApply = 'afterSaleApply', // 申请售后
AfterSaleProcessing = 'afterSaleProcessing', // 申请售后中
}
// 订单状态对应名称
export const OrderStatusText: Record<OrderStatus, string> = {
[OrderStatus.Consuming]: '消费中',
[OrderStatus.Reserved]: '预约单',
[OrderStatus.Serving]: '服务中',
[OrderStatus.Pending]: '待付款',
[OrderStatus.Confirm]: '待确认',
[OrderStatus.Finished]: '已完结',
[OrderStatus.Cancelled]: '已取消',
[OrderStatus.ToUse]: '待使用',
[OrderStatus.Used]: '已使用',
[OrderStatus.Refunded]: '已退款',
[OrderStatus.AfterSaleApply]: '申请售后',
[OrderStatus.AfterSaleProcessing]: '申请售后中'
}
// 对应tabbar显示的标题
export const OrderStatusTitle: Record<OrderSource, Record<OrderStatus, string>> = {
[OrderSource.TeaRoom]: {
[OrderStatus.Consuming]: '消费中',
[OrderStatus.Reserved]: '预约单',
[OrderStatus.Serving]: '服务中',
[OrderStatus.Pending]: '等待付款',
[OrderStatus.Confirm]: '待确认',
[OrderStatus.Finished]: '交易完成',
[OrderStatus.Cancelled]: '订单取消',
[OrderStatus.ToUse]: '待使用',
[OrderStatus.Used]: '交易完成',
[OrderStatus.Refunded]: '售后完成',
[OrderStatus.AfterSaleApply]: '申请售后',
[OrderStatus.AfterSaleProcessing]: '申请售后中'
},
[OrderSource.TeaSpecialist]: {
[OrderStatus.Consuming]: '消费中',
[OrderStatus.Reserved]: '已预约',
[OrderStatus.Serving]: '服务中',
[OrderStatus.Pending]: '待付款',
[OrderStatus.Confirm]: '订单待确认',
[OrderStatus.Finished]: '已完结',
[OrderStatus.Cancelled]: '订单取消',
[OrderStatus.ToUse]: '待使用',
[OrderStatus.Used]: '交易完成',
[OrderStatus.Refunded]: '售后完成',
[OrderStatus.AfterSaleApply]: '申请售后',
[OrderStatus.AfterSaleProcessing]: '申请售后中'
},
[OrderSource.Combo]: {
[OrderStatus.Consuming]: '消费中',
[OrderStatus.Reserved]: '预约单',
[OrderStatus.Serving]: '服务中',
[OrderStatus.Pending]: '待付款',
[OrderStatus.Confirm]: '待确认',
[OrderStatus.Finished]: '已完结',
[OrderStatus.Cancelled]: '已取消',
[OrderStatus.ToUse]: '待使用',
[OrderStatus.Used]: '交易完成',
[OrderStatus.Refunded]: '售后完成',
[OrderStatus.AfterSaleApply]: '申请售后',
[OrderStatus.AfterSaleProcessing]: '申请售后中'
},
[OrderSource.DouYin]: {
[OrderStatus.Consuming]: '消费中',
[OrderStatus.Reserved]: '预约单',
[OrderStatus.Serving]: '服务中',
[OrderStatus.Pending]: '待付款',
[OrderStatus.Confirm]: '待确认',
[OrderStatus.Finished]: '已完结',
[OrderStatus.Cancelled]: '已取消',
[OrderStatus.ToUse]: '待使用',
[OrderStatus.Used]: '已使用', // DouYin专属
[OrderStatus.Refunded]: '售后完成',
[OrderStatus.AfterSaleApply]: '申请售后',
[OrderStatus.AfterSaleProcessing]: '申请售后中'
}
}
// 茶艺师订单状态数字(根据UI图还缺已退款、待接单、售后中、售后完成)
export enum TeaSpecialistOrderStatus {
Pending = 0, // 待付款(未支付)
Pay = 1, // 预约单、已预约(已支付)
Cancelled = 2, // 已取消(订单取消)
Confirm = 3, // 待确认(已接单)
Serving = 4, // 服务中
Finished = 5, // 已完成
}
// 茶艺师订单状态文本
export enum TeaSpecialistOrderStatusText {
All = 'all', // 全部
Pending = 'pending', // 待付款
Pay = 'pay', // 已支付(预约单、已预约)
Cancelled = 'cancelled', // 已取消
Confirm = 'confirm', // 待确认
Serving = 'serving', // 服务中
Finished = 'finished', // 已完成
}
// 状态内容映射
export const TeaSpecialistOrderStatusTextValue: Record<TeaSpecialistOrderStatus, any> = {
[TeaSpecialistOrderStatus.Pending]: {
title: '等待付款'
},
[TeaSpecialistOrderStatus.Pay]: {
title: '已预约'
},
[TeaSpecialistOrderStatus.Cancelled]: {
title: '订单取消'
},
[TeaSpecialistOrderStatus.Confirm]: {
title: '订单待确认'
},
[TeaSpecialistOrderStatus.Serving]: {
title: '服务中'
},
[TeaSpecialistOrderStatus.Finished]: {
title: '交易完成'
},
}
// 茶艺师订单状态文本对应值
export const TeaSpecialistOrderStatusValue: Record<TeaSpecialistOrderStatusText, string | number> = {
[TeaSpecialistOrderStatusText.All]: '',
[TeaSpecialistOrderStatusText.Pending]: 0,
[TeaSpecialistOrderStatusText.Pay]: 1,
[TeaSpecialistOrderStatusText.Cancelled]: 2,
[TeaSpecialistOrderStatusText.Confirm]: 3,
[TeaSpecialistOrderStatusText.Serving]: 4,
[TeaSpecialistOrderStatusText.Finished]: 5,
}
// 下单类型
export const OrderType = {
TeaRoomOrder: 'teaRoomOrder',
}
// 包间订单状态数字(根据UI图还缺已退款、待接单、售后中、售后完成)
export enum TeaRoomOrderStatus {
Pending = 0, // 待付款
Pay = 1, // 已支付(预约单)
Consumption = 2, // 消费中
Finished = 3, // 完成
Cancelled = 4, // 订单取消
}
// 包间订单状态文本
export enum TeaRoomOrderStatusText {
All = 'all', // 全部
Pending = 'pending', // 待付款
Pay = 'pay', // 已支付(预约单)
Consumption = 'consumption', // 消费中
Finished = 'finished', // 已完成
Cancelled = 'cancelled', // 已取消
}
// 状态内容映射
export const TeaRoomOrderStatusTextValue: Record<TeaRoomOrderStatus, any> = {
[TeaRoomOrderStatus.Pending]: {
title: '等待付款'
},
[TeaRoomOrderStatus.Pay]: {
title: '已预约'
},
[TeaRoomOrderStatus.Consumption]: {
title: '消费中'
},
[TeaRoomOrderStatus.Finished]: {
title: '交易完成'
},
[TeaRoomOrderStatus.Cancelled]: {
title: '订单取消'
},
}
// 包间订单状态文本对应值
export const TeaRoomOrderStatusValue: Record<TeaRoomOrderStatusText, string | number> = {
[TeaRoomOrderStatusText.All]: '',
[TeaRoomOrderStatusText.Pending]: 0,
[TeaRoomOrderStatusText.Pay]: 1,
[TeaRoomOrderStatusText.Consumption]: 2,
[TeaRoomOrderStatusText.Finished]: 3,
[TeaRoomOrderStatusText.Cancelled]: 4,
}
// 包间订单状态数字(根据UI图还缺已退款、待接单、售后中、售后完成)
export enum TeaRoomPackageOrderStatus {
Pending = 0, // 待付款
ToUse = 1, // 待使用
Used = 2, // 已使用
Refunded = 3, // 已退款
}
// 套餐订单状态文本
export enum TeaRoomPackageOrderStatusText {
All = 'all', // 全部
Pending = 'pending', // 待付款
ToUse = 'toUse', // 待使用
Used = 'used', // 已使用
Refunded = 'refunded', // 已退款
}
// 套餐订单状态文本对应值
export const TeaRoomPackageOrderStatusValue: Record<TeaRoomPackageOrderStatusText, string | number> = {
[TeaRoomPackageOrderStatusText.All]: '',
[TeaRoomPackageOrderStatusText.Pending]: 0,
[TeaRoomPackageOrderStatusText.ToUse]: 1,
[TeaRoomPackageOrderStatusText.Used]: 2,
[TeaRoomPackageOrderStatusText.Refunded]: 3,
}
// 状态内容映射
export const TeaRoomPackageOrderStatusTextValue: Record<TeaRoomPackageOrderStatus, any> = {
[TeaRoomPackageOrderStatus.Pending]: {
title: '待付款'
},
[TeaRoomPackageOrderStatus.ToUse]: {
title: '待使用'
},
[TeaRoomPackageOrderStatus.Used]: {
title: '交易完成'
},
[TeaRoomPackageOrderStatus.Refunded]: {
title: '售后完成'
},
}

49
src/utils/pay.ts Normal file
View File

@ -0,0 +1,49 @@
interface PayMethod {
id: number
name: string
icon: string
balance: number
value: number,
type: string
}
// 支付方式分类
export enum PayCategory {
PlatformBalance = 'PlatformBalance', // 平台余额
StoreBalance = 'StoreBalance', // 门店余额
WeChatPay = 'WeChatPay', // 微信支付
}
export enum PayValue {
PlatformBalance = 1, // 平台余额
WeChatPay = 2, // 微信支付
StoreBalance = 3, // 门店余额
}
// 支付方式列表
export const PayList: PayMethod[] = [
{
id: 1,
name: '平台余额',
icon: 'icon/icon_platform_balance.png',
balance: 0,
value: PayValue.PlatformBalance,
type: PayCategory.PlatformBalance
},
{
id: 2,
name: '门店余额',
icon: 'icon/icon_store_balance.png',
balance: 0,
value: PayValue.StoreBalance,
type: PayCategory.StoreBalance
},
{
id: 3,
name: '微信支付',
icon: 'icon/icon_weichat.png',
balance: 0,
value: PayValue.WeChatPay,
type: PayCategory.WeChatPay
}
]

26
src/utils/platform.ts Normal file
View File

@ -0,0 +1,26 @@
/*
* @Author: 菲鸽
* @Date: 2024-03-28 19:13:55
* @Last Modified by: 菲鸽
* @Last Modified time: 2024-03-28 19:24:55
*/
export const platform = __UNI_PLATFORM__
export const isH5 = __UNI_PLATFORM__ === 'h5'
export const isApp = __UNI_PLATFORM__ === 'app'
export const isMp = __UNI_PLATFORM__.startsWith('mp-')
export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin')
export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
export const isHarmony = __UNI_PLATFORM__.startsWith('app-harmony')
const PLATFORM = {
platform,
isH5,
isApp,
isMp,
isMpWeixin,
isMpAplipay,
isMpToutiao,
isHarmony,
}
export default PLATFORM

5
src/utils/tea.ts Normal file
View File

@ -0,0 +1,5 @@
export const StoreType = {
Direct: 1, // 直营
Franchise: 2, // 加盟
DouYin: 3 // 抖音
}

View File

@ -0,0 +1,26 @@
// 茶艺师等级枚举
export enum TeaSpecialistLevel {
Gold = '金牌茶艺师',
Senior = '高级茶艺师',
Intermediate = '中级茶艺师',
Junior = '初级茶艺师',
Enthusiast = '茶艺爱好者'
}
// 订单来源对应名称
export const TeaSpecialistLevelValue = {
['金牌茶艺师']: 'gold',
['高级茶艺师']: 'senior',
['中级茶艺师']: 'intermediate',
['初级茶艺师']: 'junior',
['茶艺爱好者']: 'enthusiast',
}
// 茶艺师对象结构
export const TeaSpecialistLevels = [
{ id: 1, value: 'gold', label: TeaSpecialistLevel.Gold},
{ id: 2, value: 'senior', label: TeaSpecialistLevel.Senior },
{ id: 3, value: 'intermediate', label: TeaSpecialistLevel.Intermediate },
{ id: 4, value: 'junior', label: TeaSpecialistLevel.Junior },
{ id: 5, value: 'enthusiast', label: TeaSpecialistLevel.Enthusiast }
];

287
src/utils/test.ts Normal file
View File

@ -0,0 +1,287 @@
/**
* 验证电子邮箱格式
*/
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([3589]\d|4[5-9]|6[1-2,4-7]|7[0-8])\d{8}$/.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 {
email,
mobile,
url,
date,
dateISO,
number,
digits,
idCard,
carNo,
amount,
chinese,
letter,
enOrNum,
contains,
range,
rangeLength,
empty,
jsonString,
landline,
object,
array,
code,
func,
promise,
video,
image,
regExp,
string
}

77
src/utils/toast.ts Normal file
View File

@ -0,0 +1,77 @@
/**
* toast 弹窗组件
* 支持 success/error/warning/info 四种状态
* 可配置 duration, position 等参数
*/
type ToastType = 'success' | 'error' | 'warning' | 'info'
interface ToastOptions {
type?: ToastType
duration?: number
position?: 'top' | 'middle' | 'bottom'
icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
message: string
/**
* 是否显示透明蒙层,防止触摸穿透
* @default true
*/
mask?: boolean
}
/**
* 显示 toast
*/
export function showToast(message: string): void
export function showToast(options: ToastOptions): void
export function showToast(options: ToastOptions | string): void {
const defaultOptions: ToastOptions = {
type: 'info',
duration: 2000,
position: 'middle',
message: '',
mask: true,
}
const mergedOptions
= typeof options === 'string'
? { ...defaultOptions, message: options }
: { ...defaultOptions, ...options }
// 映射position到uniapp支持的格式
const positionMap: Record<ToastOptions['position'], 'top' | 'bottom' | 'center'> = {
top: 'top',
middle: 'center',
bottom: 'bottom',
}
// 映射图标类型
const iconMap: Record<
ToastType,
'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
> = {
success: 'success',
error: 'error',
warning: 'fail',
info: 'none',
}
// 调用uni.showToast显示提示
uni.showToast({
title: mergedOptions.message,
duration: mergedOptions.duration,
position: positionMap[mergedOptions.position],
icon: mergedOptions.icon || iconMap[mergedOptions.type],
mask: mergedOptions.mask,
})
}
type _ToastOptions = Omit<ToastOptions, 'type' | 'message'>
export const toast = {
success: (message: string, options?: _ToastOptions) =>
showToast({ ...options, type: 'success', message }),
error: (message: string, options?: _ToastOptions) =>
showToast({ ...options, type: 'error', message }),
warning: (message: string, options?: _ToastOptions) =>
showToast({ ...options, type: 'warning', message }),
info: (message: string, options?: _ToastOptions) =>
showToast({ ...options, type: 'info', message }),
}

138
src/utils/tools.ts Normal file
View File

@ -0,0 +1,138 @@
import Decimal from 'decimal.js'
import { allowedNodeEnvironmentFlags } from 'process'
import { toast } from './toast'
/**
* 页面跳转方法
* @param url 跳转地址
* @param time 延迟时间(毫秒)默认0
*/
export const router = {
//跳转至非table页面
navigateTo: (url: string, time = 0) => {
setTimeout(function() {
uni.navigateTo({
url
})
}, time);
},
//跳转至 table
switchTab: (url: string, time = 0) => {
setTimeout(function() {
uni.switchTab({
url
})
}, time);
},
//返回上页面
navigateBack: (delta: number = 1, time = 0) => {
setTimeout(function() {
uni.navigateBack({
delta
})
}, time);
},
//关闭所有页面,打开到应用内的某个页面
reLaunch: (url: string, time = 0) => {
setTimeout(function() {
uni.reLaunch({
url
})
}, time);
},
//关闭当前页面跳转至非table页面
redirectTo: (url: string, time = 0) => {
setTimeout(function() {
uni.redirectTo({
url
})
}, time);
},
}
/**
* 乘法,避免浮点数精度问题,不进行四舍五入,直接截断
* @param num1 乘数1
* @param num2 乘数2
* @returns 乘积
*/
export function toTimes(num1: number, num2: number) {
const value1 = new Decimal(num1)
const value2 = new Decimal(num2)
const result = value1.times(value2).toDecimalPlaces(2, Decimal.ROUND_DOWN)
return result.toString()
}
/**
* 加法,避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的加数
* @returns 求和结果字符串保留2位小数向下截断
*/
/**
* 加法,支持对象参数(如 {0: '128', 1: '128'}),避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的加数或对象
* @returns 求和结果字符串保留2位小数向下截断
*/
export function toPlus(...args: any[]): string {
// 支持 toPlus({0: '128', 1: '128'}) 或 toPlus('128', '128')
let arr: any[] = []
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
// 传入的是对象,取所有值
arr = Object.values(args[0])
} else {
arr = args
}
let sum = new Decimal(0)
for (const num of arr) {
// 自动转为数字
const n = Number(num)
if (!isNaN(n)) {
sum = sum.plus(new Decimal(n))
}
}
// 截断2位小数不四舍五入
const result = sum.toDecimalPlaces(2, Decimal.ROUND_DOWN)
return result.toString()
}
/**
* 减法,支持对象参数(如 {0: '128', 1: '28'}),避免浮点数精度问题,不进行四舍五入,直接截断
* @param args 任意数量的被减数和减数或对象
* @returns 结果字符串保留2位小数向下截断
*/
export function toMinus(...args: any[]): string {
// 支持 toMinus({0: '128', 1: '28'}) 或 toMinus('128', '28')
let arr: any[] = []
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null) {
arr = Object.values(args[0])
} else {
arr = args
}
if (arr.length === 0) return '0.00'
let result = new Decimal(Number(arr[0]) || 0)
for (let i = 1; i < arr.length; i++) {
const n = Number(arr[i])
if (!isNaN(n)) {
result = result.minus(new Decimal(n))
}
}
return result.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString()
}
/**
* 复制内容到剪贴板
* @param data 复制的内容
*/
export function copy(data: any) {
uni.setClipboardData({
data: data,
success: () => {
toast.info('已复制到剪贴板')
}
})
}

324
src/utils/uploadFile.ts Normal file
View File

@ -0,0 +1,324 @@
import { toast } from './toast'
/**
* 文件上传钩子函数使用示例
* @example
* const { loading, error, data, progress, run } = useUpload<IUploadResult>(
* uploadUrl,
* {},
* {
* maxSize: 5, // 最大5MB
* sourceType: ['album'], // 仅支持从相册选择
* onProgress: (p) => console.log(`上传进度:${p}%`),
* onSuccess: (res) => console.log('上传成功', res),
* onError: (err) => console.error('上传失败', err),
* },
* )
*/
/**
* 上传文件的URL配置
*/
export const uploadFileUrl = {
/** 用户头像上传地址 */
USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
}
/**
* 通用文件上传函数(支持直接传入文件路径)
* @param url 上传地址
* @param filePath 本地文件路径
* @param formData 额外表单数据
* @param options 上传选项
*/
export function useFileUpload<T = string>(url: string, filePath: string, formData: Record<string, any> = {}, options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {}) {
return useUpload<T>(
url,
formData,
{
...options,
sourceType: ['album'],
sizeType: ['original'],
},
filePath,
)
}
export interface UploadOptions {
/** 最大可选择的图片数量默认为1 */
count?: number
/** 所选的图片的尺寸original-原图compressed-压缩图 */
sizeType?: Array<'original' | 'compressed'>
/** 选择图片的来源album-相册camera-相机 */
sourceType?: Array<'album' | 'camera'>
/** 文件大小限制单位MB */
maxSize?: number //
/** 上传进度回调函数 */
onProgress?: (progress: number) => void
/** 上传成功回调函数 */
onSuccess?: (res: Record<string, any>) => void
/** 上传失败回调函数 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调函数(无论成功失败) */
onComplete?: () => void
}
/**
* 文件上传钩子函数
* @template T 上传成功后返回的数据类型
* @param url 上传地址
* @param formData 额外的表单数据
* @param options 上传选项
* @returns 上传状态和控制对象
*/
export function useUpload<T = string>(url: string, formData: Record<string, any> = {}, options: UploadOptions = {},
/** 直接传入文件路径,跳过选择器 */
directFilePath?: string) {
/** 上传中状态 */
const loading = ref(false)
/** 上传错误状态 */
const error = ref(false)
/** 上传成功后的响应数据 */
const data = ref<T>()
/** 上传进度0-100 */
const progress = ref(0)
/** 解构上传选项,设置默认值 */
const {
/** 最大可选择的图片数量 */
count = 1,
/** 所选的图片的尺寸 */
sizeType = ['original', 'compressed'],
/** 选择图片的来源 */
sourceType = ['album', 'camera'],
/** 文件大小限制MB */
maxSize = 10,
/** 进度回调 */
onProgress,
/** 成功回调 */
onSuccess,
/** 失败回调 */
onError,
/** 完成回调 */
onComplete,
} = options
/**
* 检查文件大小是否超过限制
* @param size 文件大小(字节)
* @returns 是否通过检查
*/
const checkFileSize = (size: number) => {
const sizeInMB = size / 1024 / 1024
if (sizeInMB > maxSize) {
toast.warning(`文件大小不能超过${maxSize}MB`)
return false
}
return true
}
/**
* 触发文件选择和上传
* 根据平台使用不同的选择器:
* - 微信小程序使用 chooseMedia
* - 其他平台使用 chooseImage
*/
const run = () => {
if (directFilePath) {
// 直接使用传入的文件路径
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: directFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
return
}
// #ifdef MP-WEIXIN
// 微信小程序环境下使用 chooseMedia API
uni.chooseMedia({
count,
mediaType: ['image'], // 仅支持图片类型
sourceType,
success: (res) => {
const file = res.tempFiles[0]
// 检查文件大小是否符合限制
if (!checkFileSize(file.size))
return
// 开始上传
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: file.tempFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
},
fail: (err) => {
console.error('选择媒体文件失败:', err)
error.value = true
onError?.(err)
},
})
// #endif
// #ifndef MP-WEIXIN
// 非微信小程序环境下使用 chooseImage API
uni.chooseImage({
count,
sizeType,
sourceType,
success: (res) => {
console.log('选择图片成功:', res)
// 开始上传
loading.value = true
progress.value = 0
uploadFile<T>({
url,
tempFilePath: res.tempFilePaths[0],
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
})
},
fail: (err) => {
console.error('选择图片失败:', err)
error.value = true
onError?.(err)
},
})
// #endif
}
return { loading, error, data, progress, run }
}
/**
* 文件上传选项接口
* @template T 上传成功后返回的数据类型
*/
interface UploadFileOptions<T> {
/** 上传地址 */
url: string
/** 临时文件路径 */
tempFilePath: string
/** 额外的表单数据 */
formData: Record<string, any>
/** 上传成功后的响应数据 */
data: Ref<T | undefined>
/** 上传错误状态 */
error: Ref<boolean>
/** 上传中状态 */
loading: Ref<boolean>
/** 上传进度0-100 */
progress: Ref<number>
/** 上传进度回调 */
onProgress?: (progress: number) => void
/** 上传成功回调 */
onSuccess?: (res: Record<string, any>) => void
/** 上传失败回调 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调 */
onComplete?: () => void
}
/**
* 执行文件上传
* @template T 上传成功后返回的数据类型
* @param options 上传选项
*/
function uploadFile<T>({
url,
tempFilePath,
formData,
data,
error,
loading,
progress,
onProgress,
onSuccess,
onError,
onComplete,
}: UploadFileOptions<T>) {
try {
// 创建上传任务
const uploadTask = uni.uploadFile({
url,
filePath: tempFilePath,
name: 'file', // 文件对应的 key
formData,
header: {
// H5环境下不需要手动设置Content-Type让浏览器自动处理multipart格式
// #ifndef H5
'Content-Type': 'multipart/form-data',
// #endif
},
// 确保文件名称合法
success: (uploadFileRes) => {
console.log('上传文件成功:', uploadFileRes)
try {
// 解析响应数据
const { data: _data } = JSON.parse(uploadFileRes.data)
// 上传成功
data.value = _data as T
onSuccess?.(_data)
}
catch (err) {
// 响应解析错误
console.error('解析上传响应失败:', err)
error.value = true
onError?.(new Error('上传响应解析失败'))
}
},
fail: (err) => {
// 上传请求失败
console.error('上传文件失败:', err)
error.value = true
onError?.(err)
},
complete: () => {
// 无论成功失败都执行
loading.value = false
onComplete?.()
},
})
// 监听上传进度
uploadTask.onProgressUpdate((res) => {
progress.value = res.progress
onProgress?.(res.progress)
})
}
catch (err) {
// 创建上传任务失败
console.error('创建上传任务失败:', err)
error.value = true
loading.value = false
onError?.(new Error('创建上传任务失败'))
}
}