feat: add vue3(element-plus)

This commit is contained in:
xingyu
2022-07-18 19:06:37 +08:00
parent c6b58dca52
commit 80a3ae8d74
423 changed files with 41039 additions and 0 deletions

View File

@ -0,0 +1,29 @@
import { useCache } from '@/hooks/web/useCache'
import { TokenType } from '@/api/login/types'
const { wsCache } = useCache()
const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'
// 获取token
export function getAccessToken() {
// 此处与TokenKey相同此写法解决初始化时Cookies中不存在TokenKey报错
return wsCache.get('ACCESS_TOKEN')
}
// 刷新token
export function getRefreshToken() {
return wsCache.get(RefreshTokenKey)
}
// 设置token
export function setToken(token: TokenType) {
wsCache.set(RefreshTokenKey, token.refreshToken, { exp: token.expiresTime })
wsCache.set(AccessTokenKey, token.accessToken)
}
// 删除token
export function removeToken() {
wsCache.delete(AccessTokenKey)
wsCache.delete(RefreshTokenKey)
}

View File

@ -0,0 +1,130 @@
import { required as requiredRule } from '@/utils/formRules'
import dayjs from 'dayjs'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n() // 国际化
export class FormSchemaBuilder {
static input(label: string, field: string, required: Boolean = false): FormSchema {
return {
label,
field,
component: 'Input',
formItemProps: {
rules: required ? [requiredRule] : []
}
}
}
static inputNumber(
label: string,
field: string,
value: number,
required: Boolean = false
): FormSchema {
return {
label,
field,
value,
component: 'InputNumber',
formItemProps: {
rules: required ? [requiredRule] : []
}
}
}
static radioButton(
label: string,
field: string,
value: number,
options: ComponentOptions[],
required: Boolean = false
): FormSchema {
return {
label,
field,
component: 'RadioButton',
value,
formItemProps: {
rules: required ? [requiredRule] : []
},
componentProps: {
options
}
}
}
static select(
label: string,
field: string,
value: number | null,
options: ComponentOptions[],
required: Boolean = false
): FormSchema {
return {
label,
field,
component: 'Select',
value,
formItemProps: {
rules: required ? [requiredRule] : []
},
componentProps: {
options
}
}
}
static textarea(
label: string,
field: string,
rows: number,
span: number,
required: Boolean = false
): FormSchema {
return {
label,
field,
component: 'Input',
componentProps: {
type: 'textarea',
rows: rows
},
formItemProps: {
rules: required ? [requiredRule] : []
},
colProps: {
span: span
}
}
}
}
export class TableColumnBuilder {
static column(label: string, field: string): TableColumn {
return {
label,
field
}
}
static date(label: string, field: string, template?: string): TableColumn {
return {
label,
field,
formatter: (_: Recordable, __: TableColumn, cellValue: string) => {
return dayjs(cellValue).format(template || 'YYYY-MM-DD HH:mm:ss')
}
}
}
static action(width: number): TableColumn {
return {
label: t('table.action'),
field: 'action',
width: width + 'px'
}
}
}
export class ComponentOptionsBuilder {
static option(label: string, value: number): ComponentOptions {
return {
label,
value
}
}
}

View File

@ -0,0 +1,153 @@
/**
* 判断是否 十六进制颜色值.
* 输入形式可为 #fff000 #f00
*
* @param String color 十六进制颜色值
* @return Boolean
*/
export const isHexColor = (color: string) => {
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/
return reg.test(color)
}
/**
* RGB 颜色值转换为 十六进制颜色值.
* r, g, 和 b 需要在 [0, 255] 范围内
*
* @return String 类似#ff00ff
* @param r
* @param g
* @param b
*/
export const rgbToHex = (r: number, g: number, b: number) => {
// tslint:disable-next-line:no-bitwise
const hex = ((r << 16) | (g << 8) | b).toString(16)
return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex
}
/**
* Transform a HEX color to its RGB representation
* @param {string} hex The color to transform
* @returns The RGB representation of the passed color
*/
export const hexToRGB = (hex: string, opacity?: number) => {
let sHex = hex.toLowerCase()
if (isHexColor(hex)) {
if (sHex.length === 4) {
let sColorNew = '#'
for (let i = 1; i < 4; i += 1) {
sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1))
}
sHex = sColorNew
}
const sColorChange: number[] = []
for (let i = 1; i < 7; i += 2) {
sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2)))
}
return opacity
? 'RGBA(' + sColorChange.join(',') + ',' + opacity + ')'
: 'RGB(' + sColorChange.join(',') + ')'
}
return sHex
}
export const colorIsDark = (color: string) => {
if (!isHexColor(color)) return
const [r, g, b] = hexToRGB(color)
.replace(/(?:\(|\)|rgb|RGB)*/g, '')
.split(',')
.map((item) => Number(item))
return r * 0.299 + g * 0.578 + b * 0.114 < 192
}
/**
* Darkens a HEX color given the passed percentage
* @param {string} color The color to process
* @param {number} amount The amount to change the color by
* @returns {string} The HEX representation of the processed color
*/
export const darken = (color: string, amount: number) => {
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
amount = Math.trunc((255 * amount) / 100)
return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight(
color.substring(2, 4),
amount
)}${subtractLight(color.substring(4, 6), amount)}`
}
/**
* Lightens a 6 char HEX color according to the passed percentage
* @param {string} color The color to change
* @param {number} amount The amount to change the color by
* @returns {string} The processed color represented as HEX
*/
export const lighten = (color: string, amount: number) => {
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color
amount = Math.trunc((255 * amount) / 100)
return `#${addLight(color.substring(0, 2), amount)}${addLight(
color.substring(2, 4),
amount
)}${addLight(color.substring(4, 6), amount)}`
}
/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */
/**
* Sums the passed percentage to the R, G or B of a HEX color
* @param {string} color The color to change
* @param {number} amount The amount to change the color by
* @returns {string} The processed part of the color
*/
const addLight = (color: string, amount: number) => {
const cc = parseInt(color, 16) + amount
const c = cc > 255 ? 255 : cc
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
}
/**
* Calculates luminance of an rgb color
* @param {number} r red
* @param {number} g green
* @param {number} b blue
*/
const luminanace = (r: number, g: number, b: number) => {
const a = [r, g, b].map((v) => {
v /= 255
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
})
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722
}
/**
* Calculates contrast between two rgb colors
* @param {string} rgb1 rgb color 1
* @param {string} rgb2 rgb color 2
*/
const contrast = (rgb1: string[], rgb2: number[]) => {
return (
(luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) /
(luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05)
)
}
/**
* Determines what the best text color is (black or white) based con the contrast with the background
* @param hexColor - Last selected color by the user
*/
export const calculateBestTextColor = (hexColor: string) => {
const rgbColor = hexToRGB(hexColor.substring(1))
const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0])
return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'
}
/**
* Subtracts the indicated percentage to the R, G or B of a HEX color
* @param {string} color The color to change
* @param {number} amount The amount to change the color by
* @returns {string} The processed part of the color
*/
const subtractLight = (color: string, amount: number) => {
const cc = parseInt(color, 16) - amount
const c = cc < 0 ? 0 : cc
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`
}

View File

@ -0,0 +1,218 @@
/**
* Created by 芋道源码
*
* 枚举类
*/
// 全局通用状态枚举
export const CommonStatusEnum = {
ENABLE: 0, // 开启
DISABLE: 1 // 禁用
}
/**
* 菜单的类型枚举
*/
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 InfraCodegenTemplateTypeEnum = {
CRUD: 1, // 基础 CRUD
TREE: 2, // 树形 CRUD
SUB: 3 // 主子表 CRUD
}
/**
* 任务状态的枚举
*/
export const InfraJobStatusEnum = {
INIT: 0, // 初始化中
NORMAL: 1, // 运行中
STOP: 2 // 暂停运行
}
/**
* API 异常数据的处理状态
*/
export const InfraApiErrorLogProcessStatusEnum = {
INIT: 0, // 未处理
DONE: 1, // 已处理
IGNORE: 2 // 已忽略
}
/**
* 用户的社交平台的类型枚举
*/
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'
}
}
/**
* 支付渠道枚举
*/
export const PayChannelEnum = {
WX_PUB: {
code: 'wx_pub',
name: '微信 JSAPI 支付'
},
WX_LITE: {
code: 'wx_lite',
name: '微信小程序支付'
},
WX_APP: {
code: 'wx_app',
name: '微信 APP 支付'
},
ALIPAY_PC: {
code: 'alipay_pc',
name: '支付宝 PC 网站支付'
},
ALIPAY_WAP: {
code: 'alipay_wap',
name: '支付宝 WAP 网站支付'
},
ALIPAY_APP: {
code: 'alipay_app',
name: '支付宝 APP 支付'
},
ALIPAY_QR: {
code: 'alipay_qr',
name: '支付宝扫码支付'
}
}
/**
* 支付类型枚举
*/
export const PayType = {
WECHAT: 'WECHAT',
ALIPAY: 'ALIPAY'
}
/**
* 支付订单状态枚举
*/
export const PayOrderStatusEnum = {
WAITING: {
status: 0,
name: '未支付'
},
SUCCESS: {
status: 10,
name: '已支付'
},
CLOSED: {
status: 20,
name: '未支付'
}
}
/**
* 支付订单回调状态枚举
*/
export const PayOrderNotifyStatusEnum = {
NO: {
status: 0,
name: '未通知'
},
SUCCESS: {
status: 10,
name: '通知成功'
},
FAILURE: {
status: 20,
name: '通知失败'
}
}
/**
* 支付订单退款状态枚举
*/
export const PayOrderRefundStatusEnum = {
NO: {
status: 0,
name: '未退款'
},
SOME: {
status: 10,
name: '部分退款'
},
ALL: {
status: 20,
name: '全部退款'
}
}
/**
* 支付退款订单状态枚举
*/
export const PayRefundStatusEnum = {
CREATE: {
status: 0,
name: '退款订单生成'
},
SUCCESS: {
status: 1,
name: '退款成功'
},
FAILURE: {
status: 2,
name: '退款失败'
},
PROCESSING_NOTIFY: {
status: 3,
name: '退款中,渠道通知结果'
},
PROCESSING_QUERY: {
status: 4,
name: '退款中,系统查询结果'
},
UNKNOWN_RETRY: {
status: 5,
name: '状态未知,请重试'
},
UNKNOWN_QUERY: {
status: 6,
name: '状态未知,系统查询结果'
},
CLOSE: {
status: 99,
name: '退款关闭'
}
}

View File

@ -0,0 +1,107 @@
/**
* 数据字典工具类
*/
import { useDictStoreWithOut } from '@/store/modules/dict'
const dictStore = useDictStoreWithOut()
/**
* 获取 dictType 对应的数据字典数组
*
* @param dictType 数据类型
* @returns {*|Array} 数据字典数组
*/
export interface DictDataType {
dictType: string
label: string
value: string | number
colorType: ElementPlusInfoType | '' | 'default' | 'primary'
cssClass: string
}
export const getDictOptions = (dictType: string) => {
const dictOptions: DictDataType[] = []
dictStore.getDictMap.forEach((dict: DictDataType) => {
if (dict.dictType + '' === dictType) {
dictOptions.push(dict)
}
})
return dictOptions
}
// TODO @芋艿:暂时提供这个方法,主要考虑 ElSelect。下拉如果可以解就可以不用这个方法
export const getIntDictOptions = (dictType: string) => {
const dictOptions: DictDataType[] = []
dictStore.getDictMap.forEach((dict: DictDataType) => {
if (dict.dictType + '' === dictType) {
dictOptions.push({
...dict,
value: parseInt(dict.value + '')
})
}
})
return dictOptions
}
export const getDictObj = (dictType: string, value: string) => {
const dictOptions: DictDataType[] = getDictOptions(dictType)
dictOptions.forEach((dict: DictDataType) => {
if (dict.value === value) {
return dict
}
})
}
export enum DICT_TYPE {
USER_TYPE = 'user_type',
COMMON_STATUS = 'common_status',
SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id',
// ========== SYSTEM 模块 ==========
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_OPERATE_TYPE = 'system_operate_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_ERROR_CODE_TYPE = 'system_error_code_type',
SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
// ========== INFRA 模块 ==========
INFRA_BOOLEAN_STRING = 'infra_boolean_string',
INFRA_REDIS_TIMEOUT_TYPE = 'infra_redis_timeout_type',
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_SCENE = 'infra_codegen_scene',
INFRA_FILE_STORAGE = 'infra_file_storage',
// ========== BPM 模块 ==========
BPM_MODEL_CATEGORY = 'bpm_model_category',
BPM_MODEL_FORM_TYPE = 'bpm_model_form_type',
BPM_TASK_ASSIGN_RULE_TYPE = 'bpm_task_assign_rule_type',
BPM_PROCESS_INSTANCE_STATUS = 'bpm_process_instance_status',
BPM_PROCESS_INSTANCE_RESULT = 'bpm_process_instance_result',
BPM_TASK_ASSIGN_SCRIPT = 'bpm_task_assign_script',
BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
// ========== PAY 模块 ==========
PAY_CHANNEL_WECHAT_VERSION = 'pay_channel_wechat_version', // 微信渠道版本
PAY_CHANNEL_ALIPAY_SIGN_TYPE = 'pay_channel_alipay_sign_type', // 支付渠道支付宝算法类型
PAY_CHANNEL_ALIPAY_MODE = 'pay_channel_alipay_mode', // 支付宝公钥类型
PAY_CHANNEL_ALIPAY_SERVER_TYPE = 'pay_channel_alipay_server_type', // 支付宝网关地址
PAY_CHANNEL_CODE_TYPE = 'pay_channel_code_type', // 支付渠道编码类型
PAY_ORDER_NOTIFY_STATUS = 'pay_order_notify_status', // 商户支付订单回调状态
PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
PAY_ORDER_REFUND_STATUS = 'pay_order_refund_status', // 商户支付订单退款状态
PAY_REFUND_ORDER_STATUS = 'pay_refund_order_status', // 退款订单状态
PAY_REFUND_ORDER_TYPE = 'pay_refund_order_type' // 退款订单类别
}

View File

@ -0,0 +1,289 @@
import { isServer } from './is'
const ieVersion = isServer ? 0 : Number((document as any).documentMode)
const SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g
const MOZ_HACK_REGEXP = /^moz([A-Z])/
export interface ViewportOffsetResult {
left: number
top: number
right: number
bottom: number
rightIncludeBody: number
bottomIncludeBody: number
}
/* istanbul ignore next */
const trim = function (string: string) {
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
}
/* istanbul ignore next */
const camelCase = function (name: string) {
return name
.replace(SPECIAL_CHARS_REGEXP, function (_, __, letter, offset) {
return offset ? letter.toUpperCase() : letter
})
.replace(MOZ_HACK_REGEXP, 'Moz$1')
}
/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
if (!el || !cls) return false
if (cls.indexOf(' ') !== -1) {
throw new Error('className should not contain space.')
}
if (el.classList) {
return el.classList.contains(cls)
} else {
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1
}
}
/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
if (!el) return
let curClass = el.className
const classes = (cls || '').split(' ')
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) continue
if (el.classList) {
el.classList.add(clsName)
} else if (!hasClass(el, clsName)) {
curClass += ' ' + clsName
}
}
if (!el.classList) {
el.className = curClass
}
}
/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
if (!el || !cls) return
const classes = cls.split(' ')
let curClass = ' ' + el.className + ' '
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i]
if (!clsName) continue
if (el.classList) {
el.classList.remove(clsName)
} else if (hasClass(el, clsName)) {
curClass = curClass.replace(' ' + clsName + ' ', ' ')
}
}
if (!el.classList) {
el.className = trim(curClass)
}
}
export function getBoundingClientRect(element: Element): DOMRect | number {
if (!element || !element.getBoundingClientRect) {
return 0
}
return element.getBoundingClientRect()
}
/**
* 获取当前元素的left、top偏移
* left元素最左侧距离文档左侧的距离
* top:元素最顶端距离文档顶端的距离
* right:元素最右侧距离文档右侧的距离
* bottom元素最底端距离文档底端的距离
* rightIncludeBody元素最左侧距离文档右侧的距离
* bottomIncludeBody元素最底端距离文档最底部的距离
*
* @description:
*/
export function getViewportOffset(element: Element): ViewportOffsetResult {
const doc = document.documentElement
const docScrollLeft = doc.scrollLeft
const docScrollTop = doc.scrollTop
const docClientLeft = doc.clientLeft
const docClientTop = doc.clientTop
const pageXOffset = window.pageXOffset
const pageYOffset = window.pageYOffset
const box = getBoundingClientRect(element)
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0)
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0)
const offsetLeft = retLeft + pageXOffset
const offsetTop = rectTop + pageYOffset
const left = offsetLeft - scrollLeft
const top = offsetTop - scrollTop
const clientWidth = window.document.documentElement.clientWidth
const clientHeight = window.document.documentElement.clientHeight
return {
left: left,
top: top,
right: clientWidth - rectWidth - left,
bottom: clientHeight - rectHeight - top,
rightIncludeBody: clientWidth - left,
bottomIncludeBody: clientHeight - top
}
}
/* istanbul ignore next */
export const on = function (
element: HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject
): void {
if (element && event && handler) {
element.addEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const off = function (
element: HTMLElement | Document | Window,
event: string,
handler: any
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false)
}
}
/* istanbul ignore next */
export const once = function (el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) {
if (fn) {
// @ts-ignore
fn.apply(this, args)
}
off(el, event, listener)
}
on(el, event, listener)
}
/* istanbul ignore next */
export const getStyle =
ieVersion < 9
? function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'styleFloat'
}
try {
switch (styleName) {
case 'opacity':
try {
return element.filters.item('alpha').opacity / 100
} catch (e) {
return 1.0
}
default:
return element.style[styleName] || element.currentStyle
? element.currentStyle[styleName]
: null
}
} catch (e) {
return element.style[styleName]
}
}
: function (element: Element | any, styleName: string) {
if (isServer) return
if (!element || !styleName) return null
styleName = camelCase(styleName)
if (styleName === 'float') {
styleName = 'cssFloat'
}
try {
const computed = (document as any).defaultView.getComputedStyle(element, '')
return element.style[styleName] || computed ? computed[styleName] : null
} catch (e) {
return element.style[styleName]
}
}
/* istanbul ignore next */
export function setStyle(element: Element | any, styleName: any, value: any) {
if (!element || !styleName) return
if (typeof styleName === 'object') {
for (const prop in styleName) {
if (Object.prototype.hasOwnProperty.call(styleName, prop)) {
setStyle(element, prop, styleName[prop])
}
}
} else {
styleName = camelCase(styleName)
if (styleName === 'opacity' && ieVersion < 9) {
element.style.filter = isNaN(value) ? '' : 'alpha(opacity=' + value * 100 + ')'
} else {
element.style[styleName] = value
}
}
}
/* istanbul ignore next */
export const isScroll = (el: Element, vertical: any) => {
if (isServer) return
const determinedDirection = vertical !== null || vertical !== undefined
const overflow = determinedDirection
? vertical
? getStyle(el, 'overflow-y')
: getStyle(el, 'overflow-x')
: getStyle(el, 'overflow')
return overflow.match(/(scroll|auto)/)
}
/* istanbul ignore next */
export const getScrollContainer = (el: Element, vertical?: any) => {
if (isServer) return
let parent: any = el
while (parent) {
if ([window, document, document.documentElement].includes(parent)) {
return window
}
if (isScroll(parent, vertical)) {
return parent
}
parent = parent.parentNode
}
return parent
}
/* istanbul ignore next */
export const isInContainer = (el: Element, container: any) => {
if (isServer || !el || !container) return false
const elRect = el.getBoundingClientRect()
let containerRect
if ([window, document, document.documentElement, null, undefined].includes(container)) {
containerRect = {
top: 0,
right: window.innerWidth,
bottom: window.innerHeight,
left: 0
}
} else {
containerRect = container.getBoundingClientRect()
}
return (
elRect.top < containerRect.bottom &&
elRect.bottom > containerRect.top &&
elRect.right > containerRect.left &&
elRect.left < containerRect.right
)
}

View File

@ -0,0 +1,38 @@
const download0 = (data: any, fileName: string, mineType: string) => {
// 创建 blob
const blob = new Blob([data], { type: mineType })
// 创建 href 超链接,点击进行下载
window.URL = window.URL || window.webkitURL
const href = URL.createObjectURL(blob)
const downA = document.createElement('a')
downA.href = href
downA.download = fileName
downA.click()
// 销毁超连接
window.URL.revokeObjectURL(href)
}
const download = {
// 下载 Excel 方法
excel: (data: any, fileName: string) => {
download0(data, fileName, 'application/vnd.ms-excel')
},
// 下载 Word 方法
word: (data: any, fileName: string) => {
download0(data, fileName, 'application/msword')
},
// 下载 Zip 方法
zip: (data: any, fileName: string) => {
download0(data, fileName, 'application/zip')
},
// 下载 Html 方法
html: (data: any, fileName: string) => {
download0(data, fileName, 'text/html')
},
// 下载 Markdown 方法
markdown: (data: any, fileName: string) => {
download0(data, fileName, 'text/markdown')
}
}
export default download

View File

@ -0,0 +1,157 @@
export const openWindow = (
url: string,
opt?: {
target?: '_self' | '_blank' | string
noopener?: boolean
noreferrer?: boolean
}
) => {
const { target = '__blank', noopener = true, noreferrer = true } = opt || {}
const feature: string[] = []
noopener && feature.push('noopener=yes')
noreferrer && feature.push('noreferrer=yes')
window.open(url, target, feature.join(','))
}
/**
* @description: base64 to blob
*/
export const dataURLtoBlob = (base64Buf: string): Blob => {
const arr = base64Buf.split(',')
const typeItem = arr[0]
const mime = typeItem.match(/:(.*?);/)![1]
const bstr = window.atob(arr[1])
let n = bstr.length
const u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], { type: mime })
}
/**
* img url to base64
* @param url
*/
export const urlToBase64 = (url: string, mineType?: string): Promise<string> => {
return new Promise((resolve, reject) => {
let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>
const ctx = canvas!.getContext('2d')
const img = new Image()
img.crossOrigin = ''
img.onload = function () {
if (!canvas || !ctx) {
return reject()
}
canvas.height = img.height
canvas.width = img.width
ctx.drawImage(img, 0, 0)
const dataURL = canvas.toDataURL(mineType || 'image/png')
canvas = null
resolve(dataURL)
}
img.src = url
})
}
/**
* Download online pictures
* @param url
* @param filename
* @param mime
* @param bom
*/
export const downloadByOnlineUrl = (
url: string,
filename: string,
mime?: string,
bom?: BlobPart
) => {
urlToBase64(url).then((base64) => {
downloadByBase64(base64, filename, mime, bom)
})
}
/**
* Download pictures based on base64
* @param buf
* @param filename
* @param mime
* @param bom
*/
export const downloadByBase64 = (buf: string, filename: string, mime?: string, bom?: BlobPart) => {
const base64Buf = dataURLtoBlob(buf)
downloadByData(base64Buf, filename, mime, bom)
}
/**
* Download according to the background interface file stream
* @param {*} data
* @param {*} filename
* @param {*} mime
* @param {*} bom
*/
export const downloadByData = (data: BlobPart, filename: string, mime?: string, bom?: BlobPart) => {
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data]
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' })
const blobURL = window.URL.createObjectURL(blob)
const tempLink = document.createElement('a')
tempLink.style.display = 'none'
tempLink.href = blobURL
tempLink.setAttribute('download', filename)
if (typeof tempLink.download === 'undefined') {
tempLink.setAttribute('target', '_blank')
}
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
window.URL.revokeObjectURL(blobURL)
}
/**
* Download file according to file address
* @param {*} sUrl
*/
export const downloadByUrl = ({
url,
target = '_blank',
fileName
}: {
url: string
target?: '_self' | '_blank'
fileName?: string
}): boolean => {
const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1
const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1
if (/(iP)/g.test(window.navigator.userAgent)) {
console.error('Your browser does not support download!')
return false
}
if (isChrome || isSafari) {
const link = document.createElement('a')
link.href = url
link.target = target
if (link.download !== undefined) {
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length)
}
if (document.createEvent) {
const e = document.createEvent('MouseEvents')
e.initEvent('click', true, true)
link.dispatchEvent(e)
return true
}
}
if (url.indexOf('?') === -1) {
url += '?download'
}
openWindow(url, { target })
return true
}

View File

@ -0,0 +1,9 @@
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
// 必填项
export const required = {
required: true,
message: t('common.required')
}

View File

@ -0,0 +1,149 @@
/**
* 时间日期转换
* @param date 当前时间new Date() 格式
* @param format 需要转换的时间格式字符串
* @description format 字符串随意,如 `YYYY-mm、YYYY-mm-dd`
* @description format 季度:"YYYY-mm-dd HH:MM:SS QQQQ"
* @description format 星期:"YYYY-mm-dd HH:MM:SS WWW"
* @description format 几周:"YYYY-mm-dd HH:MM:SS ZZZ"
* @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
* @returns 返回拼接后的时间字符串
*/
export function formatDate(date: Date, format: string): string {
const we = date.getDay() // 星期
const z = getWeek(date) // 周
const qut = Math.floor((date.getMonth() + 3) / 3).toString() // 季度
const opt: { [key: string]: string } = {
'Y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月(月份从0开始要+1)
'd+': date.getDate().toString(), // 日
'H+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
'S+': date.getSeconds().toString(), // 秒
'q+': qut // 季度
}
// 中文数字 (星期)
const week: { [key: string]: string } = {
'0': '日',
'1': '一',
'2': '二',
'3': '三',
'4': '四',
'5': '五',
'6': '六'
}
// 中文数字(季度)
const quarter: { [key: string]: string } = {
'1': '一',
'2': '二',
'3': '三',
'4': '四'
}
if (/(W+)/.test(format))
format = format.replace(
RegExp.$1,
RegExp.$1.length > 1 ? (RegExp.$1.length > 2 ? '星期' + week[we] : '周' + week[we]) : week[we]
)
if (/(Q+)/.test(format))
format = format.replace(
RegExp.$1,
RegExp.$1.length == 4 ? '第' + quarter[qut] + '季度' : quarter[qut]
)
if (/(Z+)/.test(format))
format = format.replace(RegExp.$1, RegExp.$1.length == 3 ? '第' + z + '周' : z + '')
for (const k in opt) {
const r = new RegExp('(' + k + ')').exec(format)
// 若输入的长度不为1则前面补零
if (r)
format = format.replace(
r[1],
RegExp.$1.length == 1 ? opt[k] : opt[k].padStart(RegExp.$1.length, '0')
)
}
return format
}
/**
* 获取当前日期是第几周
* @param dateTime 当前传入的日期值
* @returns 返回第几周数字值
*/
export function getWeek(dateTime: Date): number {
const temptTime = new Date(dateTime.getTime())
// 周几
const weekday = temptTime.getDay() || 7
// 周1+5天=周六
temptTime.setDate(temptTime.getDate() - weekday + 1 + 5)
let firstDay = new Date(temptTime.getFullYear(), 0, 1)
const dayOfWeek = firstDay.getDay()
let spendDay = 1
if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1
firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay)
const d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000)
const result = Math.ceil(d / 7)
return result
}
/**
* 将时间转换为 `几秒前`、`几分钟前`、`几小时前`、`几天前`
* @param param 当前时间new Date() 格式或者字符串时间格式
* @param format 需要转换的时间格式字符串
* @description param 10秒 10 * 1000
* @description param 1分 60 * 1000
* @description param 1小时 60 * 60 * 1000
* @description param 24小时60 * 60 * 24 * 1000
* @description param 3天 60 * 60* 24 * 1000 * 3
* @returns 返回拼接后的时间字符串
*/
export function formatPast(param: string | Date, format = 'YYYY-mm-dd HH:MM:SS'): string {
// 传入格式处理、存储转换值
let t: any, s: number
// 获取js 时间戳
let time: number = new Date().getTime()
// 是否是对象
typeof param === 'string' || 'object' ? (t = new Date(param).getTime()) : (t = param)
// 当前时间戳 - 传入时间戳
time = Number.parseInt(`${time - t}`)
if (time < 10000) {
// 10秒内
return '刚刚'
} else if (time < 60000 && time >= 10000) {
// 超过10秒少于1分钟内
s = Math.floor(time / 1000)
return `${s}秒前`
} else if (time < 3600000 && time >= 60000) {
// 超过1分钟少于1小时
s = Math.floor(time / 60000)
return `${s}分钟前`
} else if (time < 86400000 && time >= 3600000) {
// 超过1小时少于24小时
s = Math.floor(time / 3600000)
return `${s}小时前`
} else if (time < 259200000 && time >= 86400000) {
// 超过1天少于3天内
s = Math.floor(time / 86400000)
return `${s}天前`
} else {
// 超过3天
const date = typeof param === 'string' || 'object' ? new Date(param) : param
return formatDate(date, format)
}
}
/**
* 时间问候语
* @param param 当前时间new Date() 格式
* @description param 调用 `formatAxis(new Date())` 输出 `上午好`
* @returns 返回拼接后的时间字符串
*/
export function formatAxis(param: Date): string {
const hour: number = new Date(param).getHours()
if (hour < 6) return '凌晨好'
else if (hour < 9) return '早上好'
else if (hour < 12) return '上午好'
else if (hour < 14) return '中午好'
else if (hour < 17) return '下午好'
else if (hour < 19) return '傍晚好'
else if (hour < 22) return '晚上好'
else return '夜里好'
}

View File

@ -0,0 +1,110 @@
// import type { Plugin } from 'vue'
/**
*
* @param component 需要注册的组件
* @param alias 组件别名
* @returns any
*/
export const withInstall = <T>(component: T, alias?: string) => {
const comp = component as any
comp.install = (app: any) => {
app.component(comp.name || comp.displayName, component)
if (alias) {
app.config.globalProperties[alias] = component
}
}
return component as T & Plugin
}
/**
* @param str 需要转下划线的驼峰字符串
* @returns 字符串下划线
*/
export const humpToUnderline = (str: string): string => {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}
/**
* @param str 需要转驼峰的下划线字符串
* @returns 字符串驼峰
*/
export const underlineToHump = (str: string): string => {
if (!str) return ''
return str.replace(/\-(\w)/g, (_, letter: string) => {
return letter.toUpperCase()
})
}
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}
/**
* 查找数组对象的某个下标
* @param {Array} ary 查找的数组
* @param {Functon} fn 判断的方法
*/
// eslint-disable-next-line
export const findIndex = <T = Recordable>(ary: Array<T>, fn: Fn): number => {
if (ary.findIndex) {
return ary.findIndex(fn)
}
let index = -1
ary.some((item: T, i: number, ary: Array<T>) => {
const ret: T = fn(item, i, ary)
if (ret) {
index = i
return ret
}
})
return index
}
export const trim = (str: string) => {
return str.replace(/(^\s*)|(\s*$)/g, '')
}
/**
* @param {Date | number | string} time 需要转换的时间
* @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
*/
export function formatTime(time: Date | number | string, fmt: string) {
if (!time) return ''
else {
const date = new Date(time)
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds()
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(
RegExp.$1,
RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length)
)
}
}
return fmt
}
}
/**
* 生成随机字符串
*/
export function toAnyString() {
const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
const r: number = (Math.random() * 16) | 0
const v: number = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString()
})
return str
}

View File

@ -0,0 +1,105 @@
// copy to vben-admin
const toString = Object.prototype.toString
export const is = (val: unknown, type: string) => {
return toString.call(val) === `[object ${type}]`
}
export const isDef = <T = unknown>(val?: T): val is T => {
return typeof val !== 'undefined'
}
export const isUnDef = <T = unknown>(val?: T): val is T => {
return !isDef(val)
}
export const isObject = (val: any): val is Record<any, any> => {
return val !== null && is(val, 'Object')
}
export const isEmpty = <T = unknown>(val: T): val is T => {
if (isArray(val) || isString(val)) {
return val.length === 0
}
if (val instanceof Map || val instanceof Set) {
return val.size === 0
}
if (isObject(val)) {
return Object.keys(val).length === 0
}
return false
}
export const isDate = (val: unknown): val is Date => {
return is(val, 'Date')
}
export const isNull = (val: unknown): val is null => {
return val === null
}
export const isNullAndUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) && isNull(val)
}
export const isNullOrUnDef = (val: unknown): val is null | undefined => {
return isUnDef(val) || isNull(val)
}
export const isNumber = (val: unknown): val is number => {
return is(val, 'Number')
}
export const isPromise = <T = any>(val: unknown): val is Promise<T> => {
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch)
}
export const isString = (val: unknown): val is string => {
return is(val, 'String')
}
export const isFunction = (val: unknown): val is Function => {
return typeof val === 'function'
}
export const isBoolean = (val: unknown): val is boolean => {
return is(val, 'Boolean')
}
export const isRegExp = (val: unknown): val is RegExp => {
return is(val, 'RegExp')
}
export const isArray = (val: any): val is Array<any> => {
return val && Array.isArray(val)
}
export const isWindow = (val: any): val is Window => {
return typeof window !== 'undefined' && is(val, 'Window')
}
export const isElement = (val: unknown): val is Element => {
return isObject(val) && !!val.tagName
}
export const isMap = (val: unknown): val is Map<any, any> => {
return is(val, 'Map')
}
export const isServer = typeof window === 'undefined'
export const isClient = !isServer
export const isUrl = (path: string): boolean => {
const reg =
/(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/
return reg.test(path)
}
export const isDark = (): boolean => {
return window.matchMedia('(prefers-color-scheme: dark)').matches
}

View File

@ -0,0 +1,29 @@
import { createTypes, VueTypesInterface, VueTypeValidableDef } from 'vue-types'
import { CSSProperties } from 'vue'
// 自定义扩展vue-types
type PropTypes = VueTypesInterface & {
readonly style: VueTypeValidableDef<CSSProperties>
}
const propTypes = createTypes({
func: undefined,
bool: undefined,
string: undefined,
number: undefined,
object: undefined,
integer: undefined
}) as PropTypes
// 需要自定义扩展的类型
// see: https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
propTypes.extend([
{
name: 'style',
getter: true,
type: [String, Object],
default: undefined
}
])
export { propTypes }

View File

@ -0,0 +1,197 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import type { Router, RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'
import { isUrl } from '@/utils/is'
import { omit, cloneDeep } from 'lodash-es'
const modules = import.meta.glob('../views/**/*.{vue,tsx}')
/* Layout */
export const Layout = () => import('@/layout/Layout.vue')
export const getParentLayout = () => {
return () =>
new Promise((resolve) => {
resolve({
name: 'ParentLayout'
})
})
}
// 按照路由中meta下的rank等级升序来排序路由
export function ascending(arr: any[]) {
arr.forEach((v) => {
if (v?.meta?.rank === null) v.meta.rank = undefined
if (v?.meta?.rank === 0) {
if (v.name !== 'home' && v.path !== '/') {
console.warn('rank only the home page can be 0')
}
}
})
return arr.sort((a: { meta: { rank: number } }, b: { meta: { rank: number } }) => {
return a?.meta?.rank - b?.meta?.rank
})
}
export const getRawRoute = (route: RouteLocationNormalized): RouteLocationNormalized => {
if (!route) return route
const { matched, ...opt } = route
return {
...opt,
matched: (matched
? matched.map((item) => ({
meta: item.meta,
name: item.name,
path: item.path
}))
: undefined) as RouteRecordNormalized[]
}
}
// 后端控制路由生成
export const generateRoutes = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
const res: AppRouteRecordRaw[] = []
const modulesRoutesKeys = Object.keys(modules)
for (const route of routes) {
const meta = {
title: route.name,
icon: route.icon,
hidden: !route.visible,
noCache: !route.keepAlive
}
// 路由地址转首字母大写驼峰作为路由名称适配keepAlive
let data: AppRouteRecordRaw = {
path: route.path,
name: toCamelCase(route.path, true),
redirect: route.redirect,
meta: meta
}
// 目录
if (route.children) {
data.component = Layout
data.redirect = getRedirect(route.path, route.children)
// 外链
} else if (isUrl(route.path)) {
data = {
path: '/external-link',
component: Layout,
meta: {
name: route.name
},
children: [data]
} as AppRouteRecordRaw
// 菜单
} else {
// 对后端传component组件路径和不传做兼容如果后端传component组件路径那么path可以随便写如果不传component组件路径会根path保持一致
const index = route?.component
? modulesRoutesKeys.findIndex((ev) => ev.includes(route.component))
: modulesRoutesKeys.findIndex((ev) => ev.includes(route.path))
data.component = modules[modulesRoutesKeys[index]]
}
if (route.children) {
data.children = generateRoutes(route.children)
}
res.push(data)
}
return res
}
export const getRedirect = (parentPath: string, children: Array<Object>) => {
if (!children || children.length == 0) {
return parentPath
}
const path = generateRoutePath(parentPath, children[0]?.path)
// 递归子节点
return getRedirect(path, children[0]?.children)
}
const generateRoutePath = (parentPath: string, path: string) => {
if (parentPath.endsWith('/')) {
parentPath = parentPath.slice(0, -1) // 移除默认的 /
}
if (!path.startsWith('/')) {
path = '/' + path
}
return parentPath + path
}
export const pathResolve = (parentPath: string, path: string) => {
if (isUrl(path)) return path
const childPath = path.startsWith('/') || !path ? path : `/${path}`
return `${parentPath}${childPath}`.replace(/\/\//g, '/')
}
// 路由降级
export const flatMultiLevelRoutes = (routes: AppRouteRecordRaw[]) => {
const modules: AppRouteRecordRaw[] = cloneDeep(routes)
for (let index = 0; index < modules.length; index++) {
const route = modules[index]
if (!isMultipleRoute(route)) {
continue
}
promoteRouteLevel(route)
}
return modules
}
// 层级是否大于2
const isMultipleRoute = (route: AppRouteRecordRaw) => {
if (!route || !Reflect.has(route, 'children') || !route.children?.length) {
return false
}
const children = route.children
let flag = false
for (let index = 0; index < children.length; index++) {
const child = children[index]
if (child.children?.length) {
flag = true
break
}
}
return flag
}
// 生成二级路由
const promoteRouteLevel = (route: AppRouteRecordRaw) => {
let router: Router | null = createRouter({
routes: [route as RouteRecordRaw],
history: createWebHashHistory()
})
const routes = router.getRoutes()
addToChildren(routes, route.children || [], route)
router = null
route.children = route.children?.map((item) => omit(item, 'children'))
}
// 添加所有子菜单
const addToChildren = (
routes: RouteRecordNormalized[],
children: AppRouteRecordRaw[],
routeModule: AppRouteRecordRaw
) => {
for (let index = 0; index < children.length; index++) {
const child = children[index]
const route = routes.find((item) => item.name === child.name)
if (!route) {
continue
}
routeModule.children = routeModule.children || []
if (!routeModule.children.find((item) => item.name === route.name)) {
routeModule.children?.push(route as unknown as AppRouteRecordRaw)
}
if (child.children?.length) {
addToChildren(routes, child.children, routeModule)
}
}
}
function toCamelCase(str: string, upperCaseFirst: boolean) {
str = (str || '').toLowerCase().replace(/-(.)/g, function (group1: string) {
return group1.toUpperCase()
})
if (upperCaseFirst && str) {
str = str.charAt(0).toUpperCase() + str.slice(1)
}
return str
}

View File

@ -0,0 +1,292 @@
interface TreeHelperConfig {
id: string
children: string
pid: string
}
const DEFAULT_CONFIG: TreeHelperConfig = {
id: 'id',
children: 'children',
pid: 'pid'
}
const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
// tree from list
export const listToTree = <T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] => {
const conf = getConfig(config) as TreeHelperConfig
const nodeMap = new Map()
const result: T[] = []
const { id, children, pid } = conf
for (const node of list) {
node[children] = node[children] || []
nodeMap.set(node[id], node)
}
for (const node of list) {
const parent = nodeMap.get(node[pid])
;(parent ? parent.children : result).push(node)
}
return result
}
export const treeToList = <T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T => {
config = getConfig(config)
const { children } = config
const result: any = [...tree]
for (let i = 0; i < result.length; i++) {
if (!result[i][children!]) continue
result.splice(i + 1, 0, ...result[i][children!])
}
return result
}
export const findNode = <T = any>(
tree: any,
func: Fn,
config: Partial<TreeHelperConfig> = {}
): T | null => {
config = getConfig(config)
const { children } = config
const list = [...tree]
for (const node of list) {
if (func(node)) return node
node[children!] && list.push(...node[children!])
}
return null
}
export const findNodeAll = <T = any>(
tree: any,
func: Fn,
config: Partial<TreeHelperConfig> = {}
): T[] => {
config = getConfig(config)
const { children } = config
const list = [...tree]
const result: T[] = []
for (const node of list) {
func(node) && result.push(node)
node[children!] && list.push(...node[children!])
}
return result
}
export const findPath = <T = any>(
tree: any,
func: Fn,
config: Partial<TreeHelperConfig> = {}
): T | T[] | null => {
config = getConfig(config)
const path: T[] = []
const list = [...tree]
const visitedSet = new Set()
const { children } = config
while (list.length) {
const node = list[0]
if (visitedSet.has(node)) {
path.pop()
list.shift()
} else {
visitedSet.add(node)
node[children!] && list.unshift(...node[children!])
path.push(node)
if (func(node)) {
return path
}
}
}
return null
}
export const findPathAll = (tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) => {
config = getConfig(config)
const path: any[] = []
const list = [...tree]
const result: any[] = []
const visitedSet = new Set(),
{ children } = config
while (list.length) {
const node = list[0]
if (visitedSet.has(node)) {
path.pop()
list.shift()
} else {
visitedSet.add(node)
node[children!] && list.unshift(...node[children!])
path.push(node)
func(node) && result.push([...path])
}
}
return result
}
export const filter = <T = any>(
tree: T[],
func: (n: T) => boolean,
config: Partial<TreeHelperConfig> = {}
): T[] => {
config = getConfig(config)
const children = config.children as string
function listFilter(list: T[]) {
return list
.map((node: any) => ({ ...node }))
.filter((node) => {
node[children] = node[children] && listFilter(node[children])
return func(node) || (node[children] && node[children].length)
})
}
return listFilter(tree)
}
export const forEach = <T = any>(
tree: T[],
func: (n: T) => any,
config: Partial<TreeHelperConfig> = {}
): void => {
config = getConfig(config)
const list: any[] = [...tree]
const { children } = config
for (let i = 0; i < list.length; i++) {
// func 返回true就终止遍历避免大量节点场景下无意义循环引起浏览器卡顿
if (func(list[i])) {
return
}
children && list[i][children] && list.splice(i + 1, 0, ...list[i][children])
}
}
/**
* @description: Extract tree specified structure
*/
export const treeMap = <T = any>(
treeData: T[],
opt: { children?: string; conversion: Fn }
): T[] => {
return treeData.map((item) => treeMapEach(item, opt))
}
/**
* @description: Extract tree specified structure
*/
export const treeMapEach = (
data: any,
{ children = 'children', conversion }: { children?: string; conversion: Fn }
) => {
const haveChildren = Array.isArray(data[children]) && data[children].length > 0
const conversionData = conversion(data) || {}
if (haveChildren) {
return {
...conversionData,
[children]: data[children].map((i: number) =>
treeMapEach(i, {
children,
conversion
})
)
}
} else {
return {
...conversionData
}
}
}
/**
* 递归遍历树结构
* @param treeDatas 树
* @param callBack 回调
* @param parentNode 父节点
*/
export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => {
treeDatas.forEach((element) => {
const newNode = callBack(element, parentNode) || element
if (element.children) {
eachTree(element.children, callBack, newNode)
}
})
}
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
*/
export const handleTree = (data, id?: string, parentId?: string, children?: string) => {
const config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
}
const childrenListMap = {}
const nodeIds = {}
const tree = []
for (const d of data) {
const parentId = d[config.parentId]
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = []
}
nodeIds[d[config.id]] = d
childrenListMap[parentId].push(d)
}
for (const d of data) {
const parentId = d[config.parentId]
if (nodeIds[parentId] == null) {
tree.push(d)
}
}
for (const t of tree) {
adaptToChildrenList(t)
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]]
}
if (o[config.childrenList]) {
for (const c of o[config.childrenList]) {
adaptToChildrenList(c)
}
}
}
return tree
}
/**
* 构造树型结构数据
* @param {*} data 数据源
* @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children'
* @param {*} rootId 根Id 默认 0
*/
export const handleTree2 = (data, id, parentId, children, rootId) => {
id = id || 'id'
parentId = parentId || 'parentId'
children = children || 'children'
rootId =
rootId ||
Math.min(
...data.map((item) => {
return item[parentId]
})
) ||
0
//对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data))
//循环所有项
const treeData = cloneData.filter((father) => {
const branchArr = cloneData.filter((child) => {
//返回每一项的子级数组
return father[id] === child[parentId]
})
branchArr.length > 0 ? (father.children = branchArr) : ''
//返回第一层
return father[parentId] === rootId
})
return treeData !== '' ? treeData : data
}

View File

@ -0,0 +1,16 @@
import { Slots } from 'vue'
import { isFunction } from '@/utils/is'
export const getSlot = (slots: Slots, slot = 'default', data?: Recordable) => {
// Reflect.has 判断一个对象是否存在某个属性
if (!slots || !Reflect.has(slots, slot)) {
return null
}
if (!isFunction(slots[slot])) {
console.error(`${slot} is not a function!`)
return null
}
const slotFn = slots[slot]
if (!slotFn) return null
return slotFn(data)
}