feat:工具类的迁移

feat:hooks 的迁移(字典、权限)
feat:store 的迁移(字典、用户信息)
This commit is contained in:
YunaiV
2025-12-12 19:15:49 +08:00
parent ae9451f415
commit 75cf29263b
18 changed files with 1057 additions and 84 deletions

3
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './constants/biz-infra-enum'
export * from './constants/biz-system-enum'
export * from './constants/dict-enum'

View File

@@ -0,0 +1,26 @@
/**
* 代码生成模板类型
*/
export const InfraCodegenTemplateTypeEnum = {
CRUD: 1, // 基础 CRUD
TREE: 2, // 树形 CRUD
SUB: 15, // 主子表 CRUD
}
/**
* 任务状态的枚举
*/
export const InfraJobStatusEnum = {
INIT: 0, // 初始化中
NORMAL: 1, // 运行中
STOP: 2, // 暂停运行
}
/**
* API 异常数据的处理状态
*/
export const InfraApiErrorLogProcessStatusEnum = {
INIT: 0, // 未处理
DONE: 1, // 已处理
IGNORE: 2, // 已忽略
}

View File

@@ -0,0 +1,59 @@
// ========== COMMON 模块 ==========
// 全局通用状态枚举
export const CommonStatusEnum = {
ENABLE: 0, // 开启
DISABLE: 1, // 禁用
}
// 全局用户类型枚举
export const UserTypeEnum = {
MEMBER: 1, // 会员
ADMIN: 2, // 管理员
}
// ========== SYSTEM 模块 ==========
/**
* 菜单的类型枚举
*/
export const SystemMenuTypeEnum = {
DIR: 1, // 目录
MENU: 2, // 菜单
BUTTON: 3, // 按钮
}
/**
* 角色的类型枚举
*/
export const SystemRoleTypeEnum = {
SYSTEM: 1, // 内置角色
CUSTOM: 2, // 自定义角色
}
/**
* 数据权限的范围枚举
*/
export const SystemDataScopeEnum = {
ALL: 1, // 全部数据权限
DEPT_CUSTOM: 2, // 指定部门数据权限
DEPT_ONLY: 3, // 部门数据权限
DEPT_AND_CHILD: 4, // 部门及以下数据权限
DEPT_SELF: 5, // 仅本人数据权限
}
/**
* 用户的社交平台的类型枚举
*/
export const SystemUserSocialTypeEnum = {
DINGTALK: {
title: '钉钉',
type: 20,
source: 'dingtalk',
img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png',
},
WECHAT_ENTERPRISE: {
title: '企业微信',
type: 30,
source: 'wechat_enterprise',
img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png',
},
}

View File

@@ -0,0 +1,60 @@
/** ========== COMMON - 通用模块 ========== */
const COMMON_DICT = {
USER_TYPE: 'user_type',
COMMON_STATUS: 'common_status',
TERMINAL: 'terminal', // 终端
DATE_INTERVAL: 'date_interval', // 数据间隔
} as const
/** ========== SYSTEM - 系统模块 ========== */
const SYSTEM_DICT = {
SYSTEM_USER_SEX: 'system_user_sex',
SYSTEM_MENU_TYPE: 'system_menu_type',
SYSTEM_ROLE_TYPE: 'system_role_type',
SYSTEM_DATA_SCOPE: 'system_data_scope',
SYSTEM_NOTICE_TYPE: 'system_notice_type',
SYSTEM_LOGIN_TYPE: 'system_login_type',
SYSTEM_LOGIN_RESULT: 'system_login_result',
SYSTEM_SMS_CHANNEL_CODE: 'system_sms_channel_code',
SYSTEM_SMS_TEMPLATE_TYPE: 'system_sms_template_type',
SYSTEM_SMS_SEND_STATUS: 'system_sms_send_status',
SYSTEM_SMS_RECEIVE_STATUS: 'system_sms_receive_status',
SYSTEM_OAUTH2_GRANT_TYPE: 'system_oauth2_grant_type',
SYSTEM_MAIL_SEND_STATUS: 'system_mail_send_status',
SYSTEM_NOTIFY_TEMPLATE_TYPE: 'system_notify_template_type',
SYSTEM_SOCIAL_TYPE: 'system_social_type',
} as const
/** ========== INFRA - 基础设施模块 ========== */
const INFRA_DICT = {
INFRA_BOOLEAN_STRING: 'infra_boolean_string',
INFRA_JOB_STATUS: 'infra_job_status',
INFRA_JOB_LOG_STATUS: 'infra_job_log_status',
INFRA_API_ERROR_LOG_PROCESS_STATUS: 'infra_api_error_log_process_status',
INFRA_CONFIG_TYPE: 'infra_config_type',
INFRA_CODEGEN_TEMPLATE_TYPE: 'infra_codegen_template_type',
INFRA_CODEGEN_FRONT_TYPE: 'infra_codegen_front_type',
INFRA_CODEGEN_SCENE: 'infra_codegen_scene',
INFRA_FILE_STORAGE: 'infra_file_storage',
INFRA_OPERATE_TYPE: 'infra_operate_type',
} as const
/** ========== BPM - 工作流模块 ========== */
const BPM_DICT = {
BPM_MODEL_FORM_TYPE: 'bpm_model_form_type', // BPM 模型表单类型
BPM_MODEL_TYPE: 'bpm_model_type', // BPM 模型类型
BPM_OA_LEAVE_TYPE: 'bpm_oa_leave_type', // BPM OA 请假类型
BPM_PROCESS_INSTANCE_STATUS: 'bpm_process_instance_status', // BPM 流程实例状态
BPM_PROCESS_LISTENER_TYPE: 'bpm_process_listener_type', // BPM 流程监听器类型
BPM_PROCESS_LISTENER_VALUE_TYPE: 'bpm_process_listener_value_type', // BPM 流程监听器值类型
BPM_TASK_CANDIDATE_STRATEGY: 'bpm_task_candidate_strategy', // BPM 任务候选人策略
BPM_TASK_STATUS: 'bpm_task_status', // BPM 任务状态
} as const
/** 字典类型枚举 - 统一导出 */
export const DICT_TYPE = {
...BPM_DICT,
...INFRA_DICT,
...SYSTEM_DICT,
...COMMON_DICT,
} as const

85
src/utils/date.ts Normal file
View File

@@ -0,0 +1,85 @@
import dayjs from 'dayjs'
type FormatDate = Date | dayjs.Dayjs | number | string
type Format
= | 'HH'
| 'HH:mm'
| 'HH:mm:ss'
| 'YYYY'
| 'YYYY-MM'
| 'YYYY-MM-DD'
| 'YYYY-MM-DD HH'
| 'YYYY-MM-DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
| (string & {})
/** 格式化日期 */
export function formatDate(time?: FormatDate, format: Format = 'YYYY-MM-DD') {
if (!time) {
return ''
}
try {
const date = dayjs.isDayjs(time) ? time : dayjs(time)
if (!date.isValid()) {
throw new Error('Invalid date')
}
return date.format(format)
} catch (error) {
console.error(`Error formatting date: ${error}`)
return String(time ?? '')
}
}
/** 格式化日期时间 */
export function formatDateTime(time?: FormatDate) {
return formatDate(time, 'YYYY-MM-DD HH:mm:ss')
}
/** 计算开始结束时间 */
export function formatDateRange(dateRange?: [any, any]) {
if (!dateRange || !dateRange[0] || !dateRange[1]) {
return undefined
}
const startDate = new Date(dateRange[0])
startDate.setHours(0, 0, 0, 0)
const endDate = new Date(dateRange[1])
endDate.setHours(23, 59, 59, 999)
return [formatDateTime(startDate), formatDateTime(endDate)]
}
/** 格式化过去时间3分钟前、2小时前、1天前 */
export function formatPast(time?: FormatDate): string {
if (!time) {
return ''
}
const now = Date.now()
const date = dayjs.isDayjs(time) ? time : dayjs(time)
if (!date.isValid()) {
return ''
}
const diff = now - date.valueOf()
const seconds = Math.floor(diff / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)
const days = Math.floor(hours / 24)
const months = Math.floor(days / 30)
const years = Math.floor(days / 365)
if (years > 0) {
return `${years}年前`
}
if (months > 0) {
return `${months}个月前`
}
if (days > 0) {
return `${days}天前`
}
if (hours > 0) {
return `${hours}小时前`
}
if (minutes > 0) {
return `${minutes}分钟前`
}
return '刚刚'
}

110
src/utils/download.ts Normal file
View File

@@ -0,0 +1,110 @@
/**
* 下载工具类 - 支持多端H5、小程序、APP
*/
import { isH5, isMpWeixin } from '@uni-helper/uni-env'
/** 保存图片到相册 */
export async function saveImageToAlbum(url: string, fileName?: string): Promise<void> {
if (isH5) {
await downloadFileH5(url, fileName)
return
}
// 小程序和 APP 端保存图片到相册
return new Promise((resolve, reject) => {
// 如果是网络图片,先下载
if (url.startsWith('http')) {
uni.downloadFile({
url,
success: (downloadResult) => {
if (downloadResult.statusCode === 200) {
saveToAlbum(downloadResult.tempFilePath, resolve, reject)
} else {
uni.showToast({ icon: 'none', title: '下载失败' })
reject(new Error('Download failed'))
}
},
fail: (err) => {
uni.showToast({ icon: 'none', title: '下载失败' })
reject(err)
},
})
} else {
// 本地图片直接保存
saveToAlbum(url, resolve, reject)
}
})
}
/** 保存图片到相册(内部方法) */
function saveToAlbum(
filePath: string,
resolve: () => void,
reject: (err: unknown) => void,
): void {
uni.saveImageToPhotosAlbum({
filePath,
success: () => {
uni.showToast({
icon: 'success',
title: '已保存到相册',
})
resolve()
},
fail: (err) => {
// 微信小程序需要授权
if (isMpWeixin && err.errMsg?.includes('auth deny')) {
uni.showModal({
title: '提示',
content: '需要您授权保存相册权限',
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.writePhotosAlbum']) {
// 重新尝试保存
saveToAlbum(filePath, resolve, reject)
}
else {
reject(new Error('User denied'))
}
},
})
}
else {
reject(new Error('User cancelled'))
}
},
})
} else {
uni.showToast({
icon: 'none',
title: '保存失败',
})
reject(err)
}
},
})
}
/** H5 端下载文件 */
async function downloadFileH5(url: string, fileName?: string): Promise<void> {
const link = document.createElement('a')
link.href = url
link.download = fileName || resolveFileName(url)
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/** 从 URL 中解析文件名 */
function resolveFileName(url: string): string {
const defaultName = 'downloaded_file'
try {
const pathname = new URL(url).pathname
return pathname.slice(pathname.lastIndexOf('/') + 1) || defaultName
} catch {
return url.slice(url.lastIndexOf('/') + 1) || defaultName
}
}

View File

@@ -1,6 +1,8 @@
import type { PageMetaDatum, SubPackages } from '@uni-helper/vite-plugin-uni-pages'
import { isMpWeixin } from '@uni-helper/uni-env'
import { pages, subPackages } from '@/pages.json'
import { tabbarList } from '@/tabbar/config'
import { isPageTabbar } from '@/tabbar/store'
export type PageInstance = Page.PageInstance<AnyObject, object> & { $page: Page.PageInstance<AnyObject, object> & { fullPath: string } }
@@ -121,9 +123,10 @@ export function getEnvBaseUrl() {
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
// TODO @芋艿:这个后续也要调整。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://localhost:48080/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'http://localhost:48080/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://localhost:48080/admin-api'
// 微信小程序端环境区分
if (isMpWeixin) {
@@ -147,6 +150,20 @@ export function getEnvBaseUrl() {
return baseUrl
}
/**
* 根据环境变量,获取基础路径的根路径,比如 http://localhost:48080
*
* add by 芋艿:用户类似 websocket 这种需要根路径的场景
*
* @return 根路径
*/
export function getEnvBaseUrlRoot() {
const baseUrl = getEnvBaseUrl()
// 提取根路径
const urlObj = new URL(baseUrl)
return urlObj.origin
}
/**
* 是否是双token模式
*/
@@ -157,3 +174,22 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
* 通常为 /pages/index/index
*/
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
// TODO @芋艿:这里要不要换成 HOME_PAGE
/**
* 登录成功后跳转
* @param redirectUrl 重定向地址为空则跳转到默认首页tabbar 第一个页面)
*/
export function redirectAfterLogin(redirectUrl?: string) {
let path = redirectUrl || tabbarList[0].pagePath
if (!path.startsWith('/')) {
path = `/${path}`
}
const { path: _path } = parseUrlToObj(path)
if (isPageTabbar(_path)) {
uni.switchTab({ url: path })
}
else {
uni.navigateBack()
}
}

View File

@@ -1,3 +1,4 @@
import { LOGIN_PAGE } from '@/router/config'
import { getLastPage } from '@/utils'
import { debounce } from '@/utils/debounce'
@@ -14,9 +15,6 @@ interface ToLoginPageOptions {
queryString?: string
}
// TODO: 自己增加登录页
const LOGIN_PAGE = '/pages/login/index'
/**
* 跳转到登录页, 带防抖处理
*

101
src/utils/tree.ts Normal file
View File

@@ -0,0 +1,101 @@
/**
* 树形结构工具函数
*/
interface TreeNode {
id?: number
parentId?: number
children?: TreeNode[]
[key: string]: any
}
/**
* 构造树型结构数据
* @param data 数据源
* @param id id 字段,默认 'id'
* @param parentId 父节点字段,默认 'parentId'
* @param children 孩子节点字段,默认 'children'
*/
export function handleTree<T extends TreeNode>(
data: T[],
id = 'id',
parentId = 'parentId',
children = 'children',
): T[] {
if (!Array.isArray(data)) {
console.warn('data must be an array')
return []
}
const nodeMap: Record<number, T> = {}
const childrenListMap: Record<number, T[]> = {}
const tree: T[] = []
// 构建节点映射和子节点列表
for (const node of data) {
const nodeId = node[id] as number
const nodeParentId = node[parentId] as number
nodeMap[nodeId] = { ...node, [children]: [] } as T
if (!childrenListMap[nodeParentId]) {
childrenListMap[nodeParentId] = []
}
childrenListMap[nodeParentId].push(nodeMap[nodeId])
}
// 构建树形结构
for (const node of data) {
const nodeParentId = node[parentId] as number
// 父节点不存在于 nodeMap 中,说明是根节点
if (!nodeMap[nodeParentId]) {
tree.push(nodeMap[node[id] as number])
}
}
// 递归设置子节点
function setChildren(node: T) {
const nodeId = node[id] as number
const nodeChildren = childrenListMap[nodeId]
if (nodeChildren && nodeChildren.length > 0) {
;(node as any)[children] = nodeChildren
for (const child of nodeChildren) {
setChildren(child)
}
}
}
for (const node of tree) {
setChildren(node)
}
return tree
}
/**
* 在树中查找节点的子节点列表
* @param tree 树形数据
* @param parentId 父节点 ID
* @param id id 字段,默认 'id'
* @param children 孩子节点字段,默认 'children'
*/
export function findChildren<T extends TreeNode>(
tree: T[],
parentId: number,
id = 'id',
children = 'children',
): T[] {
for (const node of tree) {
if (node[id] === parentId) {
return (node[children] as T[]) || []
}
const nodeChildren = node[children] as T[] | undefined
if (nodeChildren && nodeChildren.length > 0) {
const found = findChildren(nodeChildren, parentId, id, children)
if (found.length > 0) {
return found
}
}
}
return []
}

View File

@@ -1,45 +1,128 @@
/**
* 文件上传钩子函数使用示例
* @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),
* },
* )
* 文件上传工具
*
* 支持两种上传模式:
* - server: 后端上传(默认)
* - client: 前端直连上传(仅支持 S3 服务)
*
* 通过环境变量 VITE_UPLOAD_TYPE 配置
*/
/**
* 上传文件的URL配置
*/
export const uploadFileUrl = {
/** 用户头像上传地址 */
USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
import * as FileApi from '@/api/infra/file'
/** 上传类型 */
const UPLOAD_TYPE = {
/** 客户端直接上传只支持S3服务 */
CLIENT: 'client',
/** 客户端发送到后端上传 */
SERVER: 'server',
}
/**
* 通用文件上传函数(支持直接传入文件路径)
* @param url 上传地址
* @param filePath 本地文件路径
* @param formData 额外表单数据
* @param options 上传选项
* 读取文件二进制内容
* @param uniFile 文件对象
*/
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,
)
async function readFile(uniFile: { path: string, arrayBuffer?: () => Promise<ArrayBuffer> }): Promise<ArrayBuffer | string> {
// 微信小程序
if (uni.getFileSystemManager) {
const fs = uni.getFileSystemManager()
return fs.readFileSync(uniFile.path) as ArrayBuffer
}
// H5 等
if (uniFile.arrayBuffer) {
return uniFile.arrayBuffer()
}
throw new Error('不支持的文件读取方式')
}
/**
* 创建文件记录(异步)
* @param presignedInfo 预签名信息
* @param file 文件信息
*/
function createFileRecord(presignedInfo: FileApi.FilePresignedUrlRespVO, file: { name: string, type?: string, size?: number }) {
const fileVo: FileApi.FileCreateReqVO = {
configId: presignedInfo.configId,
url: presignedInfo.url,
path: presignedInfo.path,
name: file.name,
type: file.type,
size: file.size,
}
FileApi.createFile(fileVo).catch((err) => {
console.error('创建文件记录失败:', err, fileVo)
})
}
/**
* 从文件路径上传文件(纯文件上传)
* @param filePath 文件路径
* @param directory 目录(可选)
* @returns 文件访问 URL
*/
export async function uploadFileFromPath(filePath: string, directory?: string, fileType?: string): Promise<string> {
const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath
const uploadType = import.meta.env.VITE_UPLOAD_TYPE || UPLOAD_TYPE.SERVER
// 根据文件后缀推断 MIME 类型
const mimeType = fileType || getMimeType(fileName)
// 情况一:前端直连上传
if (uploadType === UPLOAD_TYPE.CLIENT) {
// 1.1 获取文件预签名地址
const presignedInfo = await FileApi.getFilePresignedUrl(fileName, directory)
// 1.2 获取二进制文件对象
const fileBuffer = await readFile({ path: filePath })
// 返回上传的 Promise
return new Promise((resolve, reject) => {
// 1.3 上传到 S3
uni.request({
url: presignedInfo.uploadUrl,
method: 'PUT',
header: {
'Content-Type': mimeType,
},
data: fileBuffer,
success: () => {
// 1.4. 记录文件信息到后端(异步)
createFileRecord(presignedInfo, { name: fileName, type: mimeType })
// 1.5 返回文件访问 URL
resolve(presignedInfo.url)
},
fail: (err) => {
console.error('上传到S3失败:', err, presignedInfo)
reject(err)
},
})
})
} else {
// 情况二:后端上传
return FileApi.uploadFile(filePath, directory)
}
}
/** 根据文件名获取 MIME 类型 */
function getMimeType(fileName: string): string {
const ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase()
const mimeTypes: Record<string, string> = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
bmp: 'image/bmp',
svg: 'image/svg+xml',
mp4: 'video/mp4',
mov: 'video/quicktime',
avi: 'video/x-msvideo',
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}
return mimeTypes[ext] || 'application/octet-stream'
}
export interface UploadOptions {
@@ -62,7 +145,7 @@ export interface UploadOptions {
}
/**
* 文件上传钩子函数
* 文件上传钩子函数(带 formData
* @template T 上传成功后返回的数据类型
* @param url 上传地址
* @param formData 额外的表单数据
@@ -249,7 +332,7 @@ interface UploadFileOptions<T> {
}
/**
* 执行文件上传
* 执行文件上传(带 formData
* @template T 上传成功后返回的数据类型
* @param options 上传选项
*/
@@ -288,8 +371,7 @@ function uploadFile<T>({
// 上传成功
data.value = _data as T
onSuccess?.(_data)
}
catch (err) {
} catch (err) {
// 响应解析错误
console.error('解析上传响应失败:', err)
error.value = true
@@ -314,8 +396,7 @@ function uploadFile<T>({
progress.value = res.progress
onProgress?.(res.progress)
})
}
catch (err) {
} catch (err) {
// 创建上传任务失败
console.error('创建上传任务失败:', err)
error.value = true

43
src/utils/url.ts Normal file
View File

@@ -0,0 +1,43 @@
/**
* 解析 URL 查询参数
* @param url URL 字符串
* @returns { path: 路径, query: 参数对象 }
*/
export function parseUrl(url: string): { path: string, query: Record<string, string> } {
const [path, queryString] = url.split('?')
const query: Record<string, string> = {}
if (queryString) {
queryString.split('&').forEach((param) => {
const [key, value] = param.split('=')
if (key) {
query[key] = decodeURIComponent(value || '')
}
})
}
return { path, query }
}
/**
* 设置 tabBar 页面跳转参数(通过 globalData 传递)
* @param params 参数对象
*/
export function setTabParams(params: Record<string, string>) {
const app = getApp()
if (app) {
app.globalData = app.globalData || {}
app.globalData.tabParams = params
}
}
/**
* 获取并清除 tabBar 页面跳转参数
* @returns 参数对象,如果没有则返回 undefined
*/
export function getAndClearTabParams(): Record<string, string> | undefined {
const app = getApp()
const tabParams = app?.globalData?.tabParams
if (tabParams) {
delete app.globalData.tabParams
}
return tabParams
}

41
src/utils/validator.ts Normal file
View File

@@ -0,0 +1,41 @@
/** 手机号正则表达式(中国) */
const MOBILE_REGEX = /^1[3-9]\d{9}$/
/** 邮箱正则表达式 */
const EMAIL_REGEX = /^[\w-]+(?:\.[\w-]+)*@[\w-]+(?:\.[\w-]+)+$/
/**
* 判断字符串是否为空白null、undefined、空字符串或仅包含空白字符
*
* @param value 值
* @returns 是否为空白
*/
export function isBlank(value?: null | string): boolean {
return !value || value.trim().length === 0
}
/**
* 验证是否为手机号码(中国)
*
* @param value 值
* @returns 是否为手机号码(中国)
*/
export function isMobile(value?: null | string): boolean {
if (!value) {
return false
}
return MOBILE_REGEX.test(value)
}
/**
* 验证是否为邮箱
*
* @param value 值
* @returns 是否为邮箱
*/
export function isEmail(value?: null | string): boolean {
if (!value) {
return false
}
return EMAIL_REGEX.test(value)
}