chore: initial commit
This commit is contained in:
44
frontend/packages/lib-shared/method/auth.ts
Normal file
44
frontend/packages/lib-shared/method/auth.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
const SESSION_ID = 'sessionId';
|
||||
const CSRF_TOKEN = 'csrfToken';
|
||||
const LOGIN_TYPE = 'loginType';
|
||||
|
||||
// 获取token
|
||||
const getToken = () => {
|
||||
return { [SESSION_ID]: localStorage.getItem(SESSION_ID), [CSRF_TOKEN]: localStorage.getItem(CSRF_TOKEN) || '' };
|
||||
};
|
||||
|
||||
const setToken = (sessionId: string, csrfToken: string) => {
|
||||
localStorage.setItem(SESSION_ID, sessionId);
|
||||
localStorage.setItem(CSRF_TOKEN, csrfToken);
|
||||
};
|
||||
|
||||
const setLoginType = (loginType: string) => {
|
||||
localStorage.setItem(LOGIN_TYPE, loginType);
|
||||
};
|
||||
|
||||
const getLoginType = () => {
|
||||
return localStorage.getItem(LOGIN_TYPE);
|
||||
};
|
||||
|
||||
const clearToken = () => {
|
||||
localStorage.removeItem(SESSION_ID);
|
||||
localStorage.removeItem(CSRF_TOKEN);
|
||||
};
|
||||
|
||||
const hasToken = () => {
|
||||
return !!localStorage.getItem(SESSION_ID) && !!localStorage.getItem(CSRF_TOKEN);
|
||||
};
|
||||
|
||||
const setLoginExpires = () => {
|
||||
localStorage.setItem('loginExpires', Date.now().toString());
|
||||
};
|
||||
|
||||
const isLoginExpires = () => {
|
||||
const lastLoginTime = Number(localStorage.getItem('loginExpires'));
|
||||
const now = Date.now();
|
||||
const diff = now - lastLoginTime;
|
||||
const thirtyDay = 24 * 60 * 60 * 1000 * 30;
|
||||
return diff > thirtyDay;
|
||||
};
|
||||
|
||||
export { clearToken, getLoginType, getToken, hasToken, isLoginExpires, setLoginExpires, setLoginType, setToken };
|
||||
195
frontend/packages/lib-shared/method/dom.ts
Normal file
195
frontend/packages/lib-shared/method/dom.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 滚动到指定元素
|
||||
*/
|
||||
export interface ScrollToViewOptions {
|
||||
behavior?: 'auto' | 'smooth';
|
||||
block?: 'start' | 'center' | 'end' | 'nearest';
|
||||
inline?: 'start' | 'center' | 'end' | 'nearest';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定元素滚动至视图区域内
|
||||
* @param targetRef 目标 ref 或 DOM
|
||||
* @param options 滚动配置
|
||||
*/
|
||||
export function scrollIntoView(targetRef: HTMLElement | Element | null, options: ScrollToViewOptions = {}) {
|
||||
const scrollOptions: ScrollToViewOptions = {
|
||||
behavior: options.behavior || 'smooth',
|
||||
block: options.block || 'start',
|
||||
inline: options.inline || 'nearest',
|
||||
};
|
||||
|
||||
targetRef?.scrollIntoView(scrollOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无操作函数
|
||||
*/
|
||||
export const NOOP = () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否为服务端渲染
|
||||
*/
|
||||
export const isServerRendering = (() => {
|
||||
try {
|
||||
return !(typeof window !== 'undefined' && document !== undefined);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* 监听事件
|
||||
*/
|
||||
export const on = (() => {
|
||||
if (isServerRendering) {
|
||||
return NOOP;
|
||||
}
|
||||
return <K extends keyof HTMLElementEventMap>(
|
||||
element: HTMLElement | Window,
|
||||
event: K,
|
||||
handler: (ev: HTMLElementEventMap[K]) => void,
|
||||
options: boolean | AddEventListenerOptions = false
|
||||
) => {
|
||||
element.addEventListener(event, handler as EventListenerOrEventListenerObject, options);
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* 移除监听事件
|
||||
*/
|
||||
export const off = (() => {
|
||||
if (isServerRendering) {
|
||||
return NOOP;
|
||||
}
|
||||
return <K extends keyof HTMLElementEventMap>(
|
||||
element: HTMLElement | Window,
|
||||
type: K,
|
||||
handler: (ev: HTMLElementEventMap[K]) => void,
|
||||
options: boolean | EventListenerOptions = false
|
||||
) => {
|
||||
element.removeEventListener(type, handler as EventListenerOrEventListenerObject, options);
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* 获取元素宽度
|
||||
* @param el 当前元素
|
||||
* @returns number
|
||||
*/
|
||||
export function getNodeWidth(el: HTMLElement) {
|
||||
return el && +el.getBoundingClientRect().width.toFixed(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素样式
|
||||
* @param element 当前元素
|
||||
* @param prop 样式属性
|
||||
* @returns string
|
||||
*/
|
||||
export function getStyle(element: HTMLElement | null, prop: string | null) {
|
||||
if (!element || !prop) return null;
|
||||
let styleName = prop as keyof CSSStyleDeclaration;
|
||||
if (styleName === 'float') {
|
||||
styleName = 'cssFloat';
|
||||
}
|
||||
try {
|
||||
if (document.defaultView) {
|
||||
const computed = document.defaultView.getComputedStyle(element, '');
|
||||
return element.style[styleName] || computed ? computed[styleName] : '';
|
||||
}
|
||||
} catch (e) {
|
||||
return element.style[styleName];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 获取当前展示的最上层的浮层(弹窗、抽屉等)
|
||||
* @param selector 浮层选择器
|
||||
*/
|
||||
export function getMaxZIndexLayer(selector: string): HTMLElement | null {
|
||||
const layers = document.querySelectorAll<HTMLElement>(selector);
|
||||
|
||||
let maxZIndex = 0;
|
||||
let maxZIndexDrawer: HTMLElement | null = null;
|
||||
|
||||
layers.forEach((layer) => {
|
||||
const zIndex = parseInt(window.getComputedStyle(layer).zIndex, 10);
|
||||
if (!Number.isNaN(zIndex) && zIndex > maxZIndex) {
|
||||
maxZIndex = zIndex;
|
||||
maxZIndexDrawer = layer;
|
||||
}
|
||||
});
|
||||
|
||||
return maxZIndexDrawer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并样式
|
||||
* @param element 当前元素
|
||||
* @param stylesToAdd 要添加的样式
|
||||
*/
|
||||
export function mergeStyles(element: HTMLElement | Element | null, stylesToAdd: string): void {
|
||||
if (element) {
|
||||
const originalStyles = element.getAttribute('style') || '';
|
||||
const mergedStyles: Record<string, string> = {};
|
||||
const originalStylePairs = originalStyles.split(';').filter((style) => style.trim() !== '');
|
||||
|
||||
// 解析原有的 style 属性
|
||||
originalStylePairs.forEach((pair) => {
|
||||
const [key, value] = pair.split(':').map((item) => item.trim());
|
||||
mergedStyles[key] = value;
|
||||
});
|
||||
|
||||
// 解析要添加的样式属性
|
||||
const stylesToAddPairs = stylesToAdd.split(';').filter((style) => style.trim() !== '');
|
||||
stylesToAddPairs.forEach((pair) => {
|
||||
const [key, value] = pair.split(':').map((item) => item.trim());
|
||||
mergedStyles[key] = value;
|
||||
});
|
||||
|
||||
// 构造新的 style 属性字符串
|
||||
const mergedStyleString = Object.entries(mergedStyles)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(';');
|
||||
|
||||
// 设置新的 style 属性值
|
||||
element.setAttribute('style', mergedStyleString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除样式
|
||||
* @param element 当前元素
|
||||
* @param stylesToRemove 要移除的样式
|
||||
*/
|
||||
export function removeStyles(element: HTMLElement | Element | null, stylesToRemove: string): void {
|
||||
if (element) {
|
||||
const originalStyles = element.getAttribute('style') || '';
|
||||
const updatedStyles: Record<string, string> = {};
|
||||
const originalStylePairs = originalStyles.split(';').filter((style) => style.trim() !== '');
|
||||
|
||||
// 解析原有的 style 属性
|
||||
originalStylePairs.forEach((pair) => {
|
||||
const [key, value] = pair.split(':').map((item) => item.trim());
|
||||
updatedStyles[key] = value;
|
||||
});
|
||||
|
||||
// 移除指定的样式属性
|
||||
const stylesToRemovePairs = stylesToRemove.split(';').filter((style) => style.trim() !== '');
|
||||
stylesToRemovePairs.forEach((pair) => {
|
||||
const [key] = pair.split(':').map((item) => item.trim());
|
||||
delete updatedStyles[key];
|
||||
});
|
||||
|
||||
// 构造新的 style 属性字符串
|
||||
const updatedStyleString = Object.entries(updatedStyles)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(';');
|
||||
|
||||
// 设置新的 style 属性值
|
||||
element.setAttribute('style', updatedStyleString);
|
||||
}
|
||||
}
|
||||
28
frontend/packages/lib-shared/method/equal.ts
Normal file
28
frontend/packages/lib-shared/method/equal.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { sortBy } from 'lodash-es';
|
||||
|
||||
/**
|
||||
* 比较两个一维数组对象是否相等,不考虑顺序,
|
||||
* @param arr1 数组1
|
||||
* @param arr2 数组2
|
||||
* @returns boolean
|
||||
*/
|
||||
export function isArraysEqualWithOrder<T>(arr1: T[], arr2: T[]): boolean {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
const sortArr1 = sortBy(arr1, 'dataIndex');
|
||||
const sortArr2 = sortBy(arr2, 'dataIndex');
|
||||
for (let i = 0; i < sortArr1.length; i++) {
|
||||
const obj1 = sortArr1[i];
|
||||
const obj2 = sortArr2[i];
|
||||
|
||||
// 逐一比较对象
|
||||
if (JSON.stringify(obj1) !== JSON.stringify(obj2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {};
|
||||
219
frontend/packages/lib-shared/method/exportPdf.ts
Normal file
219
frontend/packages/lib-shared/method/exportPdf.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
import { Canvg } from 'canvg';
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
import JSPDF from 'jspdf';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const A4_WIDTH = 595;
|
||||
const A4_HEIGHT = 842;
|
||||
const HEADER_HEIGHT = 16;
|
||||
const FOOTER_HEIGHT = 24;
|
||||
const PAGE_HEIGHT = A4_HEIGHT - FOOTER_HEIGHT - HEADER_HEIGHT;
|
||||
const PDF_WIDTH = A4_WIDTH - 32; // 左右分别 16px 间距
|
||||
const CONTAINER_WIDTH = 1190;
|
||||
export const SCALE_RATIO = window.devicePixelRatio * 1.5;
|
||||
// 实际每页高度 = PDF页面高度/页面容器宽度与 pdf 宽度的比例(这里比例*SCALE_RATIO 是因为html2canvas截图时生成的是 SCALE_RATIO 倍的清晰度)
|
||||
export const IMAGE_HEIGHT = Math.ceil(PAGE_HEIGHT * (CONTAINER_WIDTH / PDF_WIDTH) * SCALE_RATIO);
|
||||
export const MAX_CANVAS_HEIGHT = IMAGE_HEIGHT * 20; // 一次截图最大高度是 20 页整(过长会无法截完整,出现空白)
|
||||
|
||||
/**
|
||||
* 替换svg为base64
|
||||
*/
|
||||
async function inlineSvgUseElements(container: HTMLElement) {
|
||||
const useElements = container.querySelectorAll('use');
|
||||
useElements.forEach((useElement) => {
|
||||
const href = useElement.getAttribute('xlink:href') || useElement.getAttribute('href');
|
||||
if (href) {
|
||||
const symbolId = href.substring(1);
|
||||
const symbol = document.getElementById(symbolId);
|
||||
if (symbol) {
|
||||
const svgElement = useElement.closest('svg');
|
||||
if (svgElement) {
|
||||
svgElement.innerHTML = symbol.innerHTML;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将svg转换为base64
|
||||
*/
|
||||
async function convertSvgToBase64(svgElement: SVGSVGElement) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement);
|
||||
if (ctx) {
|
||||
const v = Canvg.fromString(ctx, svgString);
|
||||
canvas.width = svgElement.clientWidth;
|
||||
canvas.height = svgElement.clientHeight;
|
||||
await v.render();
|
||||
}
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换svg为base64
|
||||
*/
|
||||
export async function replaceSvgWithBase64(container: HTMLElement) {
|
||||
await inlineSvgUseElements(container);
|
||||
const svgElements = container.querySelectorAll('.c-icon');
|
||||
svgElements.forEach(async (svgElement) => {
|
||||
const img = new Image();
|
||||
img.src = await convertSvgToBase64(svgElement as SVGSVGElement);
|
||||
img.width = svgElement.clientWidth;
|
||||
img.height = svgElement.clientHeight;
|
||||
img.style.marginRight = '8px';
|
||||
svgElement.parentNode?.replaceChild(img, svgElement);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理 DOM 元素的分页,防止内容被截断
|
||||
* @param containerId 容器 ID
|
||||
* @param pageHeight PDF 一页的有效高度 (对应你的 PAGE_HEIGHT 或 IMAGE_HEIGHT / SCALE_RATIO)
|
||||
*/
|
||||
function handlePageBreak(containerId: string, pageHeight: number) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
|
||||
// 获取所有直接子元素,这里假设子元素是不可分割的块(比如一行表格、一个段落)
|
||||
// 根据实际情况,你可能需要更精确的选择器,比如 '.table-row', 'p', 'img'
|
||||
const children = Array.from(container.children) as HTMLElement[];
|
||||
|
||||
let currentPageHeight = 0;
|
||||
const nodesToMove: { node: HTMLElement; spacerHeight: number }[] = [];
|
||||
|
||||
children.forEach((child) => {
|
||||
const childHeight = child.offsetHeight;
|
||||
// 如果元素本身就比一页还高,那没法处理,只能让它截断
|
||||
if (childHeight > pageHeight) {
|
||||
currentPageHeight += childHeight;
|
||||
// 重置当前页高度计数,近似处理
|
||||
currentPageHeight = currentPageHeight % pageHeight;
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断加上当前元素后是否超出一页
|
||||
if (currentPageHeight + childHeight > pageHeight) {
|
||||
// 计算需要插入的空白高度,把当前元素挤到下一页开头
|
||||
const spacerHeight = pageHeight - currentPageHeight;
|
||||
nodesToMove.push({ node: child, spacerHeight });
|
||||
// 当前元素被挤到下一页了,所以新的当前页高度就是它自己的高度
|
||||
currentPageHeight = childHeight;
|
||||
} else {
|
||||
// 没超出一页,累加高度
|
||||
currentPageHeight += childHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// 统一插入空白占位符
|
||||
// 需要倒序插入,否则会影响后续元素的 offsetTop 计算(虽然这里用的是累加高度,倒序更安全)
|
||||
for (let i = nodesToMove.length - 1; i >= 0; i--) {
|
||||
const { node, spacerHeight } = nodesToMove[i];
|
||||
const spacer = document.createElement('div');
|
||||
spacer.style.height = `${spacerHeight}px`;
|
||||
spacer.style.width = '100%';
|
||||
// 标记一下,方便导出后移除
|
||||
spacer.className = 'pdf-page-break-spacer';
|
||||
spacer.style.backgroundColor = 'transparent'; // 确保透明
|
||||
container.insertBefore(spacer, node);
|
||||
}
|
||||
|
||||
return () => {
|
||||
// 返回一个清理函数,在导出完成后移除这些占位符,恢复网页原样
|
||||
const spacers = container.querySelectorAll('.pdf-page-break-spacer');
|
||||
spacers.forEach(spacer => spacer.remove());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 导出PDF
|
||||
* @param name 文件名
|
||||
* @param contentId 内容DOM id
|
||||
* @description 通过html2canvas生成图片,再通过jsPDF生成pdf
|
||||
* (使用html2canvas截图时,因为插件有截图极限,超出极限部分会出现截图失败,所以这里设置了MAX_CANVAS_HEIGHT截图高度,然后根据这个截图高度分页截图,然后根据每个截图裁剪每页 pdf 的图片并添加到 pdf 内)
|
||||
*/
|
||||
export default async function exportPDF(name: string, contentId: string, doneCallback?: () => void) {
|
||||
const element = document.getElementById(contentId);
|
||||
if (element) {
|
||||
await replaceSvgWithBase64(element);
|
||||
const totalHeight = element.scrollHeight;
|
||||
// jsPDFs实例
|
||||
const pdf = new JSPDF({
|
||||
unit: 'pt',
|
||||
format: 'a4',
|
||||
orientation: 'p',
|
||||
});
|
||||
pdf.setFontSize(10);
|
||||
// 计算pdf总页数
|
||||
let totalPages = 0;
|
||||
let position = 0; // 当前截图位置
|
||||
let pageIndex = 1;
|
||||
let loopTimes = 0;
|
||||
const screenshotList: HTMLCanvasElement[] = [];
|
||||
// 创建图片裁剪画布
|
||||
const cropCanvas = document.createElement('canvas');
|
||||
cropCanvas.width = CONTAINER_WIDTH * SCALE_RATIO; // 因为截图时放大了 SCALE_RATIO 倍,所以这里也要放大
|
||||
cropCanvas.height = IMAGE_HEIGHT;
|
||||
const tempContext = cropCanvas.getContext('2d', { willReadFrequently: true });
|
||||
// 这里是大的分页,也就是截图画布的分页
|
||||
while (position < totalHeight) {
|
||||
// 截图高度
|
||||
const screenshotHeight = Math.min(MAX_CANVAS_HEIGHT, totalHeight - position);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const canvas = await html2canvas(element, {
|
||||
x: 0,
|
||||
y: position,
|
||||
width: CONTAINER_WIDTH,
|
||||
height: screenshotHeight,
|
||||
backgroundColor: '#f9f9fe',
|
||||
scale: SCALE_RATIO, // 缩放增加清晰度
|
||||
});
|
||||
screenshotList.push(canvas);
|
||||
position += screenshotHeight;
|
||||
totalPages += Math.ceil(canvas.height / IMAGE_HEIGHT);
|
||||
loopTimes++;
|
||||
}
|
||||
totalPages -= loopTimes - 1; // 减去多余的页数
|
||||
// 生成 PDF
|
||||
screenshotList.forEach((_canvas) => {
|
||||
const canvasWidth = _canvas.width;
|
||||
const canvasHeight = _canvas.height;
|
||||
const pages = Math.ceil(canvasHeight / IMAGE_HEIGHT);
|
||||
for (let i = 1; i <= pages; i++) {
|
||||
// 这里是小的分页,是 pdf 的每一页
|
||||
const pagePosition = (i - 1) * IMAGE_HEIGHT;
|
||||
if (tempContext) {
|
||||
if (pageIndex === totalPages) {
|
||||
// 填充背景颜色为白色
|
||||
tempContext.fillStyle = '#ffffff';
|
||||
tempContext.fillRect(0, 0, cropCanvas.width, cropCanvas.height);
|
||||
}
|
||||
// 将大分页的画布图片裁剪成pdf 页面内容大小,并渲染到临时画布上
|
||||
tempContext.drawImage(_canvas, 0, -pagePosition, canvasWidth, canvasHeight);
|
||||
const tempCanvasData = cropCanvas.toDataURL('image/jpeg', 1);
|
||||
// 将临时画布图片渲染到 pdf 上
|
||||
pdf.addImage(tempCanvasData, 'PNG', 16, 16, PDF_WIDTH, PAGE_HEIGHT);
|
||||
}
|
||||
cropCanvas.remove();
|
||||
pdf.text(
|
||||
`${pageIndex} / ${totalPages}`,
|
||||
pdf.internal.pageSize.width / 2 - 10,
|
||||
pdf.internal.pageSize.height - 4
|
||||
);
|
||||
if (i < pages) {
|
||||
pdf.addPage();
|
||||
pageIndex++;
|
||||
}
|
||||
}
|
||||
_canvas.remove();
|
||||
});
|
||||
pdf.save(`${name}.pdf`);
|
||||
nextTick(() => {
|
||||
doneCallback?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
566
frontend/packages/lib-shared/method/formCreate.ts
Normal file
566
frontend/packages/lib-shared/method/formCreate.ts
Normal file
@@ -0,0 +1,566 @@
|
||||
import type { CommonList, ModuleField } from '../models/common';
|
||||
import { FieldTypeEnum } from '../enums/formDesignEnum';
|
||||
import type { FormCreateField, FormDetail } from '@cordys/web/src/components/business/crm-form-create/types';
|
||||
import { formatTimeValue, getCityPath, getIndustryPath } from './index';
|
||||
import { useI18n } from '../hooks/useI18n';
|
||||
|
||||
export const linkAllAcceptTypes = [FieldTypeEnum.INPUT, FieldTypeEnum.TEXTAREA];
|
||||
export const dataSourceTypes = [FieldTypeEnum.DATA_SOURCE, FieldTypeEnum.DATA_SOURCE_MULTIPLE];
|
||||
export const hiddenTypes = [
|
||||
FieldTypeEnum.DIVIDER,
|
||||
FieldTypeEnum.PICTURE,
|
||||
FieldTypeEnum.ATTACHMENT,
|
||||
FieldTypeEnum.LINK,
|
||||
FieldTypeEnum.SUB_PRICE,
|
||||
FieldTypeEnum.SUB_PRODUCT,
|
||||
];
|
||||
export const needSameTypes = [
|
||||
FieldTypeEnum.PHONE,
|
||||
FieldTypeEnum.LOCATION,
|
||||
FieldTypeEnum.DATE_TIME,
|
||||
FieldTypeEnum.INPUT_NUMBER,
|
||||
FieldTypeEnum.INDUSTRY,
|
||||
FieldTypeEnum.SUB_PRICE,
|
||||
FieldTypeEnum.SUB_PRODUCT,
|
||||
];
|
||||
export const multipleTypes = [FieldTypeEnum.CHECKBOX, FieldTypeEnum.SELECT_MULTIPLE, FieldTypeEnum.INPUT_MULTIPLE];
|
||||
export const memberTypes = [FieldTypeEnum.MEMBER, FieldTypeEnum.MEMBER_MULTIPLE];
|
||||
export const departmentTypes = [FieldTypeEnum.DEPARTMENT, FieldTypeEnum.DEPARTMENT_MULTIPLE];
|
||||
export const singleTypes = [FieldTypeEnum.RADIO, FieldTypeEnum.SELECT];
|
||||
export const specialBusinessKeyMap: Record<string, string> = {
|
||||
customerId: 'customerName',
|
||||
contactId: 'contactName',
|
||||
clueId: 'clueName',
|
||||
businessId: 'businessName',
|
||||
contractId: 'contractName',
|
||||
owner: 'ownerName',
|
||||
opportunityId: 'opportunityName',
|
||||
paymentPlanId: 'paymentPlanName',
|
||||
businessTitleId: 'businessTitleName',
|
||||
};
|
||||
|
||||
export function getRuleType(item: FormCreateField) {
|
||||
if (
|
||||
item.type === FieldTypeEnum.SELECT_MULTIPLE ||
|
||||
item.type === FieldTypeEnum.CHECKBOX ||
|
||||
item.type === FieldTypeEnum.INPUT_MULTIPLE ||
|
||||
item.type === FieldTypeEnum.MEMBER_MULTIPLE ||
|
||||
item.type === FieldTypeEnum.DEPARTMENT_MULTIPLE ||
|
||||
item.type === FieldTypeEnum.DATA_SOURCE ||
|
||||
item.type === FieldTypeEnum.DATA_SOURCE_MULTIPLE ||
|
||||
item.type === FieldTypeEnum.PICTURE ||
|
||||
item.type === FieldTypeEnum.ATTACHMENT
|
||||
) {
|
||||
return 'array';
|
||||
}
|
||||
if (item.type === FieldTypeEnum.DATE_TIME) {
|
||||
return 'date';
|
||||
}
|
||||
if ([FieldTypeEnum.INPUT_NUMBER, FieldTypeEnum.FORMULA].includes(item.type)) {
|
||||
return 'number';
|
||||
}
|
||||
return 'string';
|
||||
}
|
||||
|
||||
export function getNormalFieldValue(item: FormCreateField, value: any) {
|
||||
if (item.type === FieldTypeEnum.DATA_SOURCE && !value) {
|
||||
return '';
|
||||
}
|
||||
if (
|
||||
[
|
||||
FieldTypeEnum.SELECT_MULTIPLE,
|
||||
FieldTypeEnum.MEMBER_MULTIPLE,
|
||||
FieldTypeEnum.DEPARTMENT_MULTIPLE,
|
||||
FieldTypeEnum.DATA_SOURCE_MULTIPLE,
|
||||
FieldTypeEnum.INPUT_MULTIPLE,
|
||||
].includes(item.type) &&
|
||||
!value
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
if (item.type === FieldTypeEnum.INPUT_MULTIPLE && !value) {
|
||||
return [];
|
||||
}
|
||||
if (item.multiple && !value) {
|
||||
return [];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param value 数字
|
||||
* @param item
|
||||
*/
|
||||
export function formatNumberValue(value: string | number, item: FormCreateField) {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (item.numberFormat === 'percent') {
|
||||
return item.precision ? `${Number(value).toFixed(item.precision)}%` : `${value}%`;
|
||||
}
|
||||
if (item.showThousandsSeparator) {
|
||||
return (item.precision ? Number(Number(value).toFixed(item.precision)) : Number(value)).toLocaleString('en-US');
|
||||
}
|
||||
return item.precision ? Number(value).toFixed(item.precision) : value.toString();
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化数字显示为字符串
|
||||
* @param value 数字
|
||||
* @param item
|
||||
*/
|
||||
export function formatNumberValueToString(value: number, item: FormCreateField) {
|
||||
if (value !== undefined && value !== null) {
|
||||
if (item.numberFormat === 'percent') {
|
||||
return item.precision ? `${Number(value).toFixed(item.precision)}%` : `${value}%`;
|
||||
}
|
||||
if (item.showThousandsSeparator) {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
return item.precision
|
||||
? `${value.toLocaleString('en-US').split('.')[0]}.${value.toFixed?.(item.precision).split('.')[1]}`
|
||||
: value.toLocaleString('en-US');
|
||||
}
|
||||
return item.precision ? Number(value).toFixed(item.precision) : value.toString();
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
export function initFieldValue(field: FormCreateField, value: string | number | (string | number)[]) {
|
||||
if (
|
||||
[FieldTypeEnum.DATA_SOURCE, FieldTypeEnum.DATA_SOURCE_MULTIPLE].includes(field.type) &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
return value ? [value] : [];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getFieldItemId(field: FormCreateField) {
|
||||
if (field.resourceFieldId) {
|
||||
return field.id.split('_ref_')[1]; // 处理数据源显示字段
|
||||
}
|
||||
return field.id;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param field
|
||||
* @param fieldValue
|
||||
* @returns 获取系统字段的值展示
|
||||
*/
|
||||
export function getDisplayFieldText(field: FormCreateField, fieldValue: any) {
|
||||
const { t } = useI18n();
|
||||
const fieldKey = field.businessKey || getFieldItemId(field);
|
||||
|
||||
if (fieldKey === 'invalid') {
|
||||
if (fieldValue === true || fieldValue === 'true') {
|
||||
return t('common.voided');
|
||||
}
|
||||
if (fieldValue === false || fieldValue === 'false') {
|
||||
return t('common.normal');
|
||||
}
|
||||
}
|
||||
|
||||
const currentOption = field.options?.find((option: any) => {
|
||||
if (option.value === fieldValue) {
|
||||
return true;
|
||||
}
|
||||
if (typeof option.value === 'boolean' && typeof fieldValue === 'string') {
|
||||
return String(option.value) === fieldValue;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return currentOption ? currentOption.label : fieldValue;
|
||||
}
|
||||
|
||||
export function parseModuleFieldValue(item: FormCreateField, fieldValue: string | string[], options?: any[]) {
|
||||
if (fieldValue === undefined || fieldValue === null || fieldValue === '') {
|
||||
return '-';
|
||||
}
|
||||
const { t } = useI18n();
|
||||
let value: string | string[] = fieldValue;
|
||||
if (options) {
|
||||
// 若字段值是选项值,则取选项值的name
|
||||
if (Array.isArray(fieldValue)) {
|
||||
value = fieldValue.map((e) => {
|
||||
const option = options.find((opt) => opt.id === e);
|
||||
if (option) {
|
||||
return option.name || t('common.optionNotExist');
|
||||
}
|
||||
return t('common.optionNotExist');
|
||||
});
|
||||
} else {
|
||||
value = options.find((e) => e.id === fieldValue)?.name || t('common.optionNotExist');
|
||||
}
|
||||
} else if (
|
||||
[
|
||||
FieldTypeEnum.DATA_SOURCE,
|
||||
FieldTypeEnum.DATA_SOURCE_MULTIPLE,
|
||||
FieldTypeEnum.MEMBER,
|
||||
FieldTypeEnum.MEMBER_MULTIPLE,
|
||||
FieldTypeEnum.DEPARTMENT,
|
||||
FieldTypeEnum.DEPARTMENT_MULTIPLE,
|
||||
].includes(item.type)
|
||||
) {
|
||||
// 数据源/成员/部门类型字段,且没有匹配到 options,则显示不存在
|
||||
if (Array.isArray(fieldValue)) {
|
||||
value = fieldValue.map(() => t('common.optionNotExist'));
|
||||
} else {
|
||||
value = t('common.optionNotExist');
|
||||
}
|
||||
} else if (item.type === FieldTypeEnum.LOCATION) {
|
||||
const addressArr: string[] = (fieldValue as string)?.split('-')?.filter(Boolean) || [];
|
||||
if (!addressArr.length) {
|
||||
value = '-';
|
||||
} else {
|
||||
const country = addressArr[0];
|
||||
const rest = addressArr.filter((e, i) => i > 0).join('-');
|
||||
value = rest ? `${getCityPath(country, item.scope)}-${rest}` : getCityPath(country, item.scope);
|
||||
}
|
||||
} else if (item.type === FieldTypeEnum.INDUSTRY) {
|
||||
value = fieldValue ? getIndustryPath(fieldValue as string) : '-';
|
||||
} else if (item.type === FieldTypeEnum.INPUT_NUMBER) {
|
||||
value = formatNumberValueToString(fieldValue as unknown as number, item);
|
||||
if (value.includes('NaN') || value.includes('%%')) {
|
||||
value = fieldValue.toString();
|
||||
}
|
||||
} else if (item.type === FieldTypeEnum.DATE_TIME) {
|
||||
value = formatTimeValue(fieldValue as string, item.dateType);
|
||||
}
|
||||
if (Array.isArray(value) && item.resourceFieldId) {
|
||||
value = value.join(',');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseFormDetailValue(item: FormCreateField, form: FormDetail, sourceName?: Ref<string>) {
|
||||
const { t } = useI18n();
|
||||
if (item.businessKey && !item.resourceFieldId) {
|
||||
// 引用数据源字段使用 id 读取数据,而不是 businessKey
|
||||
const options = form.optionMap?.[item.businessKey];
|
||||
// 业务标准字段读取最外层,读取form[item.businessKey]取到 id 值,然后去 options 里取 name
|
||||
let name: string | string[] = '';
|
||||
const value = form[item.businessKey];
|
||||
// 若字段值是选项值,则取选项值的name
|
||||
if (options) {
|
||||
if (Array.isArray(value)) {
|
||||
name = value.map((e) => {
|
||||
const option = options.find((opt) => opt.id === e);
|
||||
if (option) {
|
||||
return option.name || t('common.optionNotExist');
|
||||
}
|
||||
return t('common.optionNotExist');
|
||||
});
|
||||
} else if (value) {
|
||||
name = options.find((e) => e.id === value)?.name || t('common.optionNotExist');
|
||||
}
|
||||
}
|
||||
if (item.type === FieldTypeEnum.DATE_TIME) {
|
||||
return formatTimeValue(name || form[item.businessKey], item.dateType);
|
||||
}
|
||||
if (item.type === FieldTypeEnum.INPUT_NUMBER) {
|
||||
return formatNumberValueToString(name || form[item.businessKey], item);
|
||||
}
|
||||
if (item.type === FieldTypeEnum.ATTACHMENT) {
|
||||
return form.attachmentMap?.[item.businessKey] || [];
|
||||
}
|
||||
if (item.businessKey === 'name' && sourceName) {
|
||||
sourceName.value = name || form[item.businessKey];
|
||||
}
|
||||
return name || form[item.businessKey];
|
||||
}
|
||||
const options = form.optionMap?.[item.id];
|
||||
// 其他的字段读取moduleFields
|
||||
const field = form.moduleFields?.find((moduleField: ModuleField) => moduleField.fieldId === item.id);
|
||||
if (item.type === FieldTypeEnum.ATTACHMENT) {
|
||||
return form.attachmentMap?.[item.id] || [];
|
||||
}
|
||||
if (field) {
|
||||
return parseModuleFieldValue(item, field.fieldValue, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单配置表格回显数据
|
||||
*/
|
||||
export function transformData({
|
||||
item,
|
||||
fields,
|
||||
originalData,
|
||||
excludeFieldIds,
|
||||
needParseSubTable = false,
|
||||
}: {
|
||||
fields: FormCreateField[];
|
||||
item: any;
|
||||
originalData?: CommonList<any>;
|
||||
excludeFieldIds?: string[];
|
||||
needParseSubTable?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const businessFieldAttr: Record<string, any> = {};
|
||||
const customFieldAttr: Record<string, any> = {};
|
||||
const addressFieldIds: string[] = [];
|
||||
const industryFieldIds: string[] = [];
|
||||
const dataSourceFieldIds: string[] = [];
|
||||
const memberFieldIds: string[] = [];
|
||||
const departmentFieldIds: string[] = [];
|
||||
const timeFieldIds: string[] = [];
|
||||
const fieldOptionMap: Record<string, any[]> = {};
|
||||
|
||||
fields.forEach((field) => {
|
||||
const fieldId = field.resourceFieldId ? field.id : field.businessKey || field.id;
|
||||
if (field.type === FieldTypeEnum.LOCATION) {
|
||||
addressFieldIds.push(fieldId);
|
||||
} else if (field.type === FieldTypeEnum.INDUSTRY) {
|
||||
industryFieldIds.push(fieldId);
|
||||
} else if (field.type === FieldTypeEnum.DATA_SOURCE || field.type === FieldTypeEnum.DATA_SOURCE_MULTIPLE) {
|
||||
dataSourceFieldIds.push(fieldId);
|
||||
} else if (field.type === FieldTypeEnum.MEMBER || field.type === FieldTypeEnum.MEMBER_MULTIPLE) {
|
||||
memberFieldIds.push(fieldId);
|
||||
} else if (field.type === FieldTypeEnum.DEPARTMENT || field.type === FieldTypeEnum.DEPARTMENT_MULTIPLE) {
|
||||
departmentFieldIds.push(fieldId);
|
||||
} else if (field.type === FieldTypeEnum.DATE_TIME) {
|
||||
timeFieldIds.push(fieldId);
|
||||
} else if ([FieldTypeEnum.SUB_PRICE, FieldTypeEnum.SUB_PRODUCT].includes(field.type) && needParseSubTable) {
|
||||
field.subFields?.forEach((subField) => {
|
||||
const subFieldData = (
|
||||
item[fieldId] || item.moduleFields?.find((mf: any) => mf.fieldId === fieldId)?.fieldValue
|
||||
)?.map((subItem: Record<string, any>) => {
|
||||
if (subField.resourceFieldId) {
|
||||
subItem[`${subField.id}_original`] = subItem[field.id]; // 备份原始值以供编辑时填充数据源
|
||||
subItem[subField.id] = parseModuleFieldValue(
|
||||
subField,
|
||||
subItem[subField.id],
|
||||
// 数据源显示字段不使用业务 key,直接使用字段 id 去取值
|
||||
originalData?.optionMap?.[subField.id]
|
||||
);
|
||||
fieldOptionMap[subField.id] = originalData?.optionMap?.[subField.id] || [];
|
||||
} else {
|
||||
subItem[`${subField.id}_original`] = subItem[subField.businessKey || subField.id]; // 备份原始值以供编辑时填充数据源
|
||||
subItem[subField.id] = parseModuleFieldValue(
|
||||
subField,
|
||||
subItem[subField.businessKey || subField.id],
|
||||
originalData?.optionMap?.[subField.businessKey || subField.id]
|
||||
);
|
||||
fieldOptionMap[subField.businessKey || subField.id] =
|
||||
originalData?.optionMap?.[subField.businessKey || subField.id] || [];
|
||||
}
|
||||
return subItem;
|
||||
});
|
||||
item[fieldId] = subFieldData;
|
||||
if (fieldId === field.businessKey) {
|
||||
// 子表格字段可能会被设置为数据源的显示字段,而数据源显示字段都通过 id 读取,所以这里需要用 id 备份一份数据以供数据源显示字段场景读取
|
||||
item[field.id] = subFieldData;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (field.businessKey && !field.resourceFieldId) {
|
||||
const fieldId = field.businessKey;
|
||||
const options = originalData?.optionMap?.[fieldId]?.map((e: any) => ({
|
||||
...e,
|
||||
name: e.name || t('common.optionNotExist'),
|
||||
}));
|
||||
fieldOptionMap[fieldId] = options || [];
|
||||
if (addressFieldIds.includes(fieldId)) {
|
||||
// 地址类型字段,解析代码替换成省市区
|
||||
const addressArr: string[] = item[fieldId]?.split('-')?.filter(Boolean) || [];
|
||||
let value = '';
|
||||
if (!addressArr.length) {
|
||||
value = '-';
|
||||
} else {
|
||||
const country = addressArr[0];
|
||||
const rest = addressArr.filter((e, i) => i > 0).join('-');
|
||||
value = rest ? `${getCityPath(country)}-${rest}` : getCityPath(country);
|
||||
}
|
||||
businessFieldAttr[fieldId] = value;
|
||||
} else if (industryFieldIds.includes(fieldId)) {
|
||||
// 行业类型字段,解析代码替换成行业名称
|
||||
businessFieldAttr[fieldId] = item[fieldId] ? getIndustryPath(item[fieldId] as string) : '-';
|
||||
} else if (timeFieldIds.includes(fieldId)) {
|
||||
// 时间类型字段,格式化时间显示
|
||||
businessFieldAttr[fieldId] = formatTimeValue(item[fieldId], field.dateType);
|
||||
} else if (options && options.length > 0) {
|
||||
let name: string | string[] = '';
|
||||
if (item[fieldId] === '' || item[fieldId] === null) {
|
||||
name = '-';
|
||||
} else if (dataSourceFieldIds.includes(fieldId)) {
|
||||
// 处理数据源字段,需要赋值为数组
|
||||
if (typeof item[fieldId] === 'string' || typeof item[fieldId] === 'number') {
|
||||
// 单选
|
||||
name = options?.find((e) => e.id === item[fieldId])?.name || t('common.optionNotExist');
|
||||
} else {
|
||||
// 多选
|
||||
name = options?.filter((e) => item[fieldId]?.includes(e.id)).map((e) => e.name) || [
|
||||
t('common.optionNotExist'),
|
||||
];
|
||||
}
|
||||
} else if (typeof item[fieldId] === 'string' || typeof item[fieldId] === 'number') {
|
||||
// 若值是单个字符串/数字
|
||||
name = options?.find((e) => e.id === item[fieldId])?.name || t('common.optionNotExist');
|
||||
} else {
|
||||
// 若值是数组
|
||||
name = options?.filter((e) => item[fieldId]?.includes(e.id)).map((e) => e.name) || [
|
||||
t('common.optionNotExist'),
|
||||
];
|
||||
if (Array.isArray(name) && name.length === 0) {
|
||||
name = [t('common.optionNotExist')];
|
||||
}
|
||||
}
|
||||
if (!excludeFieldIds?.includes(field.businessKey)) {
|
||||
if (specialBusinessKeyMap[fieldId]) {
|
||||
// 处理特殊业务 key 映射关系
|
||||
businessFieldAttr[specialBusinessKeyMap[fieldId]] = name || t('common.optionNotExist');
|
||||
} else {
|
||||
businessFieldAttr[fieldId] = name || t('common.optionNotExist');
|
||||
}
|
||||
}
|
||||
if (fieldId === 'owner') {
|
||||
businessFieldAttr.ownerId = item.owner;
|
||||
}
|
||||
} else if (specialBusinessKeyMap[fieldId]) {
|
||||
// 处理特殊业务 key 映射关系
|
||||
businessFieldAttr[specialBusinessKeyMap[fieldId]] = item[specialBusinessKeyMap[fieldId]];
|
||||
}
|
||||
businessFieldAttr[field.id] = businessFieldAttr[fieldId] || item[fieldId];
|
||||
}
|
||||
});
|
||||
|
||||
item.moduleFields?.forEach((field: ModuleField) => {
|
||||
const options = originalData?.optionMap?.[field.fieldId]?.map((e) => ({
|
||||
...e,
|
||||
name: e.name || t('common.optionNotExist'),
|
||||
}));
|
||||
fieldOptionMap[field.fieldId] = options || [];
|
||||
if (addressFieldIds.includes(field.fieldId)) {
|
||||
// 地址类型字段,解析代码替换成省市区
|
||||
const addressArr: string[] = (field?.fieldValue as string)?.split('-')?.filter(Boolean) || [];
|
||||
let value = '';
|
||||
if (!addressArr.length) {
|
||||
value = '-';
|
||||
} else {
|
||||
const country = addressArr[0];
|
||||
const scope = fields.find((f) => f.id === field.fieldId)?.scope;
|
||||
const rest = addressArr.filter((e, i) => i > 0).join('-');
|
||||
value = rest ? `${getCityPath(country, scope)}-${rest}` : getCityPath(country, scope);
|
||||
}
|
||||
customFieldAttr[field.fieldId] = value;
|
||||
} else if (industryFieldIds.includes(field.fieldId)) {
|
||||
// 行业类型字段,解析代码替换成行业名称
|
||||
customFieldAttr[field.fieldId] = field.fieldValue ? getIndustryPath(field.fieldValue as string) : '-';
|
||||
} else if (timeFieldIds.includes(field.fieldId)) {
|
||||
// 时间类型字段,格式化时间显示
|
||||
customFieldAttr[field.fieldId] = formatTimeValue(
|
||||
field.fieldValue as string,
|
||||
fields.find((f) => f.id === field.fieldId)?.dateType
|
||||
);
|
||||
} else if (options && options.length > 0) {
|
||||
let name: string | string[] = '';
|
||||
if (dataSourceFieldIds.includes(field.fieldId)) {
|
||||
// 处理数据源字段,需要赋值为数组
|
||||
if (typeof field.fieldValue === 'string' || typeof field.fieldValue === 'number') {
|
||||
// 单选
|
||||
name = [options.find((e) => e.id === field.fieldValue)?.name || t('common.optionNotExist')];
|
||||
} else {
|
||||
// 多选
|
||||
name = field.fieldValue?.map((e) => options.find((o) => o.id === e)?.name || t('common.optionNotExist'));
|
||||
}
|
||||
} else if (typeof field.fieldValue === 'string' || typeof field.fieldValue === 'number') {
|
||||
// 若值是单个字符串/数字
|
||||
name = options.find((e) => e.id === field.fieldValue)?.name || t('common.optionNotExist');
|
||||
} else {
|
||||
// 若值是数组
|
||||
name = field.fieldValue?.map((fv) => options.find((e) => e.id === fv)?.name || t('common.optionNotExist'));
|
||||
if (Array.isArray(name) && name.length === 0) {
|
||||
name = [t('common.optionNotExist')];
|
||||
}
|
||||
}
|
||||
customFieldAttr[field.fieldId] = name || [t('common.optionNotExist')];
|
||||
} else if (
|
||||
[...dataSourceFieldIds, ...memberFieldIds, ...departmentFieldIds].includes(field.fieldId) &&
|
||||
(!options || options.length === 0)
|
||||
) {
|
||||
// 处理匹配不到 optionsMap 的数据源/成员/部门字段
|
||||
if (typeof field.fieldValue === 'string' || typeof field.fieldValue === 'number') {
|
||||
// 单选
|
||||
customFieldAttr[field.fieldId] = field.fieldValue !== '' ? [t('common.optionNotExist')] : ['-'];
|
||||
} else {
|
||||
// 避免这里返回 [['选项不存在']] 这样的嵌套数组
|
||||
customFieldAttr[field.fieldId] = field.fieldValue?.map((e) => (e !== '' ? t('common.optionNotExist') : '-'));
|
||||
}
|
||||
} else {
|
||||
// 其他类型字段,直接赋值
|
||||
customFieldAttr[field.fieldId] = field.fieldValue;
|
||||
}
|
||||
});
|
||||
// 根据 moduleFields 集合判断 fields 完整字段集合中是否有自定义字段无值,因为无值后台不会在 moduleFields 里返回该字段,需要手动置空
|
||||
fields.forEach((field) => {
|
||||
if (!field.resourceFieldId && !field.businessKey) {
|
||||
const fieldId = field.id;
|
||||
// 避免将 0 有效计算结果误判为空
|
||||
if (customFieldAttr[fieldId] === undefined || customFieldAttr[fieldId] === null) {
|
||||
customFieldAttr[fieldId] = undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
...customFieldAttr,
|
||||
...businessFieldAttr,
|
||||
optionMap: fieldOptionMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单子表单计算汇总数值转换
|
||||
*/
|
||||
export function normalizeNumber(val: unknown): number {
|
||||
if (val === null || val === undefined || val === '') return 0;
|
||||
if (typeof val === 'number') {
|
||||
return Number.isFinite(val) ? val : 0;
|
||||
}
|
||||
if (typeof val === 'string') {
|
||||
let str = val.trim();
|
||||
if (!str) return 0;
|
||||
// 是否是百分比
|
||||
const isPercent = str.endsWith('%');
|
||||
// 去掉百分号
|
||||
if (isPercent) {
|
||||
str = str.slice(0, -1);
|
||||
}
|
||||
// 去掉千分位
|
||||
str = str.replace(/,/g, '');
|
||||
const num = Number(str);
|
||||
return Number.isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并初始选项和追加选项,去重后返回新的初始选项数组
|
||||
* @param sumInitialOptions
|
||||
* @param appendOptions
|
||||
* @returns
|
||||
*/
|
||||
export function mergeUniqueOptions(sumInitialOptions: Record<string, any>[], appendOptions: Record<string, any>[]) {
|
||||
const optionMap = new Map<any, Record<string, any>>();
|
||||
[...sumInitialOptions, ...appendOptions].forEach((option) => {
|
||||
if (!option) {
|
||||
return;
|
||||
}
|
||||
const optionKey = option.id ?? option.value;
|
||||
if (optionKey !== undefined) {
|
||||
if (optionMap.has(optionKey)) {
|
||||
Object.assign(optionMap.get(optionKey) || {}, option);
|
||||
} else {
|
||||
optionMap.set(optionKey, option);
|
||||
}
|
||||
}
|
||||
});
|
||||
sumInitialOptions = Array.from(optionMap.values());
|
||||
return sumInitialOptions;
|
||||
}
|
||||
737
frontend/packages/lib-shared/method/index.ts
Normal file
737
frontend/packages/lib-shared/method/index.ts
Normal file
@@ -0,0 +1,737 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
import JSEncrypt from 'jsencrypt';
|
||||
|
||||
import { isObject } from './is';
|
||||
import { CHINA_PCD, COUNTRIES_TREE } from '@cordys/web/src/components/business/crm-city-select/config';
|
||||
import type {
|
||||
FormCreateField,
|
||||
FormCreateFieldDateType,
|
||||
} from '@cordys/web/src/components/business/crm-form-create/types';
|
||||
import { getLocalStorage } from '@lib/shared/method/local-storage';
|
||||
import industryOptions from '@cordys/web/src/components/pure/crm-industry-select/config';
|
||||
|
||||
/**
|
||||
* 递归深度合并
|
||||
* @param src 源对象
|
||||
* @param target 待合并的目标对象
|
||||
* @returns 合并后的对象
|
||||
*/
|
||||
export const deepMerge = <T = any>(src: any = {}, target: any = {}): T => {
|
||||
Object.keys(target).forEach((key) => {
|
||||
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
|
||||
});
|
||||
return src;
|
||||
};
|
||||
|
||||
/**
|
||||
* 遍历对象属性并一一添加到 url 地址参数上
|
||||
* @param baseUrl 需要添加参数的 url
|
||||
* @param obj 参数对象
|
||||
* @returns 拼接后的 url
|
||||
*/
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string {
|
||||
let parameters = '';
|
||||
Object.keys(obj).forEach((key) => {
|
||||
parameters += `${key}=${encodeURIComponent(obj[key])}&`;
|
||||
});
|
||||
parameters = parameters.replace(/&$/, '');
|
||||
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
* @param input 输入的字符串
|
||||
* @param publicKey 公钥
|
||||
* @returns
|
||||
*/
|
||||
export function encrypted(input: string) {
|
||||
const publicKey = getLocalStorage('publicKey') || '';
|
||||
const encrypt = new JSEncrypt({ default_key_size: '1024' });
|
||||
encrypt.setPublicKey(publicKey);
|
||||
|
||||
return encrypt.encrypt(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 休眠
|
||||
* @param ms 睡眠时长,单位毫秒
|
||||
* @returns
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function getQueryVariable(variable: string) {
|
||||
const urlString = window.location.href;
|
||||
const queryIndex = urlString.indexOf('?');
|
||||
if (queryIndex !== -1) {
|
||||
// 先获取?到#之间的内容,如果没有#则获取到结尾
|
||||
const hashIndex = urlString.indexOf('#');
|
||||
const queryEnd = hashIndex !== -1 ? hashIndex : urlString.length;
|
||||
const query = urlString.substring(queryIndex + 1, queryEnd);
|
||||
|
||||
// 分割查询参数
|
||||
const params = query.split('&');
|
||||
// 遍历参数,找到 _token 参数的值
|
||||
let variableValue;
|
||||
params.forEach((param) => {
|
||||
const equalIndex = param.indexOf('=');
|
||||
const variableName = param.substring(0, equalIndex);
|
||||
if (variableName === variable) {
|
||||
variableValue = param.substring(equalIndex + 1);
|
||||
}
|
||||
});
|
||||
return variableValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUrlParameterWidthRegExp(name: string) {
|
||||
const url = window.location.href;
|
||||
name = name.replace(/[[\]]/g, '\\$&');
|
||||
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
|
||||
const results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 SSE 连接
|
||||
* @param url 连接地址
|
||||
* @param host 连接主机
|
||||
* @returns EventSource 实例
|
||||
*/
|
||||
export const apiSSE = (url: string, host?: string): EventSource => {
|
||||
let protocol = 'http://';
|
||||
|
||||
// 判断是否使用 HTTPS
|
||||
if (!host?.startsWith('http') && (window.location.protocol === 'https:' || host?.startsWith('https'))) {
|
||||
protocol = 'https://';
|
||||
}
|
||||
|
||||
// 解析 URL,自动适配 host
|
||||
const uri = protocol + (host?.split('://')[1] || window.location.host) + url;
|
||||
|
||||
return new EventSource(uri, {
|
||||
withCredentials: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 SSE 连接
|
||||
* @param sseUrl,自定义 SSE 地址
|
||||
* @param host 自定义主机
|
||||
* @returns EventSource 实例
|
||||
*/
|
||||
export function getSSE(sseUrl: string, params: Record<string, string>, host?: string): EventSource {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
return apiSSE(`${sseUrl}?${queryString}`, host);
|
||||
}
|
||||
|
||||
export interface TreeNode<T> {
|
||||
children?: TreeNode<T>[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树形数组或树
|
||||
* @param tree 树形数组或树
|
||||
* @param customNodeFn 自定义节点函数
|
||||
* @param customChildrenKey 自定义子节点的key
|
||||
* @param continueCondition 继续递归的条件,某些情况下需要无需递归某些节点的子孙节点,可传入该条件
|
||||
*/
|
||||
export function traverseTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
customNodeFn: (node: TreeNode<T>) => void,
|
||||
continueCondition?: (node: TreeNode<T>) => boolean,
|
||||
customChildrenKey = 'children'
|
||||
) {
|
||||
if (!Array.isArray(tree)) {
|
||||
tree = [tree];
|
||||
}
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = (tree as TreeNode<T>[])[i];
|
||||
if (typeof customNodeFn === 'function') {
|
||||
customNodeFn(node);
|
||||
}
|
||||
if (node[customChildrenKey] && Array.isArray(node[customChildrenKey]) && node[customChildrenKey].length > 0) {
|
||||
if (typeof continueCondition === 'function' && !continueCondition(node)) {
|
||||
// 如果有继续递归的条件,则判断是否继续递归
|
||||
break;
|
||||
}
|
||||
traverseTree(node[customChildrenKey], customNodeFn, continueCondition, customChildrenKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 id 序列号
|
||||
* @returns
|
||||
*/
|
||||
let lastTimestamp = 0;
|
||||
let sequence = 0;
|
||||
export const getGenerateId = () => {
|
||||
let timestamp = new Date().getTime();
|
||||
if (timestamp === lastTimestamp) {
|
||||
sequence++;
|
||||
if (sequence >= 100000) {
|
||||
// 如果超过999,则重置为0,等待下一秒
|
||||
sequence = 0;
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = new Date().getTime();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sequence = 0;
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
|
||||
return timestamp.toString() + sequence.toString().padStart(5, '0');
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除树形数组中的某个节点
|
||||
* @param treeArr 目标树
|
||||
* @param targetKey 目标节点唯一值
|
||||
*/
|
||||
export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number, customKey = 'key'): void {
|
||||
function deleteNodeInTree(tree: TreeNode<T>[]): void {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (node[customKey] === targetKey) {
|
||||
tree.splice(i, 1); // 直接删除当前节点
|
||||
// 重新调整剩余子节点的 sort 序号
|
||||
for (let j = i; j < tree.length; j++) {
|
||||
tree[j].sort = j + 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
deleteNodeInTree(node.children); // 递归删除子节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteNodeInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历树形数组或树,返回新的树
|
||||
* @param tree 树形数组或树
|
||||
* @param customNodeFn 自定义节点函数
|
||||
* @param customChildrenKey 自定义子节点的key
|
||||
* @param parent 父节点
|
||||
* @param parentPath 父节点路径
|
||||
* @param level 节点层级
|
||||
* @returns 遍历后的树形数组
|
||||
*/
|
||||
export function mapTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
customNodeFn: (node: TreeNode<T>, path: string, _level: number) => TreeNode<T> | null = (node) => node,
|
||||
customChildrenKey = 'children',
|
||||
parentPath = '',
|
||||
level = 0,
|
||||
parent: TreeNode<T> | null = null
|
||||
): T[] {
|
||||
let cloneTree = cloneDeep(tree);
|
||||
if (!Array.isArray(cloneTree)) {
|
||||
cloneTree = [cloneTree];
|
||||
}
|
||||
|
||||
function mapFunc(
|
||||
_tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
_parentPath = '',
|
||||
_level = 0,
|
||||
_parent: TreeNode<T> | null = null
|
||||
): T[] {
|
||||
if (!Array.isArray(_tree)) {
|
||||
_tree = [_tree];
|
||||
}
|
||||
return _tree
|
||||
.map((node: TreeNode<T>, i: number) => {
|
||||
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
|
||||
node.sort = i + 1; // sort 从 1 开始
|
||||
node.parent = _parent || undefined; // 没有父节点说明是树的第一层
|
||||
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath, _level) : node;
|
||||
if (newNode) {
|
||||
newNode.level = _level;
|
||||
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
|
||||
newNode[customChildrenKey] = mapFunc(newNode[customChildrenKey], fullPath, _level + 1, newNode);
|
||||
}
|
||||
}
|
||||
return newNode;
|
||||
})
|
||||
.filter((node: TreeNode<T> | null) => node !== null);
|
||||
}
|
||||
return mapFunc(cloneTree, parentPath, level, parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取树形数据所有有子节点的父节点
|
||||
* @param treeData 树形数组
|
||||
* @param childrenKey 自定义子节点的key
|
||||
* @returns 遍历后父节点数组
|
||||
*/
|
||||
export function getAllParentNodeIds<T>(
|
||||
treeData: TreeNode<T>[],
|
||||
childrenKey = 'children',
|
||||
customKey = 'id'
|
||||
): Array<string | number> {
|
||||
const parentIds: Array<string | number> = [];
|
||||
const traverse = (nodes: TreeNode<T>) => {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i];
|
||||
if (node[childrenKey] && node[childrenKey].length > 0) {
|
||||
parentIds.push(node[customKey]); // 记录当前节点的 ID
|
||||
traverse(node[childrenKey]); // 递归遍历子节点
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
traverse(treeData); // 开始递归
|
||||
return parentIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤树形数组或树
|
||||
* @param tree 树形数组或树
|
||||
* @param customNodeFn 自定义节点函数
|
||||
* @param customChildrenKey 自定义子节点的key
|
||||
* @returns 遍历后的树形数组
|
||||
*/
|
||||
export function filterTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
filterFn: (node: TreeNode<T>, nodeIndex: number, parent?: TreeNode<T> | null) => boolean,
|
||||
customChildrenKey = 'children',
|
||||
parentNode: TreeNode<T> | null = null
|
||||
): TreeNode<T>[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
tree = [tree];
|
||||
}
|
||||
const filteredTree: TreeNode<T>[] = [];
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = (tree as TreeNode<T>[])[i];
|
||||
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
|
||||
if (filterFn(node, i, parentNode)) {
|
||||
const newNode = cloneDeep({ ...node, [customChildrenKey]: [] });
|
||||
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
|
||||
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
|
||||
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey, node);
|
||||
} else {
|
||||
newNode[customChildrenKey] = [];
|
||||
}
|
||||
filteredTree.push(newNode);
|
||||
}
|
||||
}
|
||||
return filteredTree;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 返回文件的大小
|
||||
* @param fileSize file文件的大小size
|
||||
* @returns
|
||||
*/
|
||||
export function formatFileSize(fileSize: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = fileSize;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
const unit = units[unitIndex];
|
||||
if (size) {
|
||||
const formattedSize = size.toFixed(2);
|
||||
return `${formattedSize} ${unit}`;
|
||||
}
|
||||
const formattedSize = 0;
|
||||
return `${formattedSize} ${unit}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串脱敏
|
||||
* @param str 需要脱敏的字符串
|
||||
* @returns 脱敏后的字符串
|
||||
*/
|
||||
export function desensitize(str: string): string {
|
||||
if (!str || typeof str !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return str.replace(/./g, '*');
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话框标题动态内容字符限制
|
||||
* @param str 标题的动态内容
|
||||
* @returns 转化后的字符串
|
||||
*/
|
||||
export function characterLimit(str?: string, length?: number): string {
|
||||
if (!str) return '';
|
||||
const limit = length ?? 20;
|
||||
if (str.length <= limit) return str;
|
||||
return `${str.slice(0, limit - 3)}...`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据属性 key 查找树形数组中匹配的某个节点
|
||||
* @param trees 属性数组
|
||||
* @param targetKey 需要匹配的属性值
|
||||
* @param customKey 默认为 key,可自定义需要匹配的属性名
|
||||
* @returns 匹配的节点/null
|
||||
*/
|
||||
export function findNodeByKey<T>(
|
||||
trees: TreeNode<T>[],
|
||||
targetKey: string | number,
|
||||
customKey = 'key',
|
||||
dataKey: string | undefined = undefined
|
||||
): TreeNode<T> | T | null {
|
||||
for (let i = 0; i < trees.length; i++) {
|
||||
const node = trees[i];
|
||||
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
|
||||
return node; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
const _node = findNodeByKey(node.children, targetKey, customKey, dataKey); // 递归在子节点中查找
|
||||
if (_node) {
|
||||
return _node; // 如果在子节点中找到了匹配的节点,则返回该节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // 如果在整个树形数组中都没有找到匹配的节点,则返回 null
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 遍历树,并返回找到的节点路径和节点
|
||||
*/
|
||||
export function findNodePathByKey<T>(
|
||||
tree: TreeNode<T>[],
|
||||
targetKey: string,
|
||||
dataKey?: string,
|
||||
customKey = 'key'
|
||||
): TreeNode<T> | null {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
if (dataKey ? node[dataKey]?.[customKey] === targetKey : node[customKey] === targetKey) {
|
||||
return { ...node, treePath: [dataKey ? node[dataKey] : node] }; // 如果当前节点的 key 与目标 key 匹配,则返回当前节点
|
||||
}
|
||||
|
||||
if (Array.isArray(node.children) && node.children.length > 0) {
|
||||
const result = findNodePathByKey(node.children, targetKey, dataKey, customKey); // 递归在子节点中查找
|
||||
if (result) {
|
||||
result.treePath.unshift(dataKey ? node[dataKey] : node);
|
||||
return result; // 如果在子节点中找到了匹配的节点,则返回该节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 cityId 返回城市路径
|
||||
*/
|
||||
export function getCityPath(cityId: string | null, scope?: string): string {
|
||||
if (!cityId) return '';
|
||||
const nodePathObject = findNodePathByKey(scope === 'CN' ? CHINA_PCD.children : [CHINA_PCD, ...COUNTRIES_TREE], cityId, undefined, 'value');
|
||||
const nodePathName = (nodePathObject?.treePath || []).map((item: any) => item.label);
|
||||
return nodePathName.length === 1 ? nodePathName[0] : nodePathName.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 industryId 返回行业路径
|
||||
*/
|
||||
export function getIndustryPath(industryId: string | null): string {
|
||||
if (!industryId) return '';
|
||||
const nodePathObject = findNodePathByKey(industryOptions, industryId, undefined, 'value');
|
||||
const nodePathName = (nodePathObject?.treePath || []).map((item: any) => item.label);
|
||||
return nodePathName.length === 1 ? nodePathName[0] : nodePathName.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回添加节点下一个有效未命名name
|
||||
* @param existingNames 已存在名称列表
|
||||
* @param baseName 基础名称
|
||||
*/
|
||||
export function getNextAvailableName(existingNames: string[], baseName: string): string {
|
||||
const baseNamePattern = new RegExp(`^${baseName}(\\d+)$`);
|
||||
|
||||
const existingSuffixes = existingNames.reduce((suffixes: number[], name: string) => {
|
||||
const match = baseNamePattern.exec(name);
|
||||
if (match) {
|
||||
suffixes.push(parseInt(match[1], 10));
|
||||
}
|
||||
return suffixes;
|
||||
}, []);
|
||||
|
||||
if (existingSuffixes.length === 0) {
|
||||
return existingNames.includes(baseName) ? `${baseName}1` : baseName;
|
||||
}
|
||||
|
||||
return `${baseName}${Math.max(...existingSuffixes) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分步处理分数表达式
|
||||
* @param str 分数表达式
|
||||
*/
|
||||
export function safeFractionConvert(str: string | number) {
|
||||
if (!str) {
|
||||
return 1;
|
||||
}
|
||||
if (typeof str === 'number') {
|
||||
return str;
|
||||
}
|
||||
const parts = str.split('/').map(Number); // 分割分子分母
|
||||
if (parts.length !== 2 || parts.some((e) => Number.isNaN(e))) return 1;
|
||||
return parts[0] / parts[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开网页链接
|
||||
* @param url 链接地址
|
||||
*/
|
||||
export function openDocumentLink(url: string) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener noreferrer'; // 防止打开页面控制当前页面
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param value 时间戳
|
||||
* @param type 类型
|
||||
*/
|
||||
export function formatTimeValue(value: string | number, type?: FormCreateFieldDateType) {
|
||||
if (value) {
|
||||
const date = dayjs(Number(value));
|
||||
switch (type) {
|
||||
case 'month':
|
||||
return date.format('YYYY-MM');
|
||||
case 'date':
|
||||
return date.format('YYYY-MM-DD');
|
||||
case 'datetime':
|
||||
default:
|
||||
return date.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param byte 字节流
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
export const downloadByteFile = (byte: BlobPart, fileName: string) => {
|
||||
// 创建一个Blob对象
|
||||
const blob = new Blob([byte], { type: 'application/octet-stream' });
|
||||
// 创建一个URL对象,用于生成下载链接
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
// 创建一个虚拟的<a>标签来触发下载
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName; // 设置下载文件的名称
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// 释放URL对象
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取每三位使用逗号隔开数字格式
|
||||
* @param number 目标值
|
||||
*/
|
||||
|
||||
export function addCommasToNumber(number: number) {
|
||||
if (number === 0 || number === undefined) {
|
||||
return '0';
|
||||
}
|
||||
// 将数字转换为字符串
|
||||
const numberStr = number.toString();
|
||||
|
||||
// 分割整数部分和小数部分
|
||||
const parts = numberStr.split('.');
|
||||
const integerPart = parts[0];
|
||||
const decimalPart = parts[1] || ''; // 如果没有小数部分,则设为空字符串
|
||||
|
||||
// 对整数部分添加逗号分隔
|
||||
const integerWithCommas = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
|
||||
// 拼接整数部分和小数部分(如果有)
|
||||
const result = decimalPart ? `${integerWithCommas}.${decimalPart}` : integerWithCommas;
|
||||
|
||||
return result;
|
||||
}
|
||||
// 是否是企业微信端打开
|
||||
export function isWeComBrowser() {
|
||||
const ua = window.navigator.userAgent.toLowerCase();
|
||||
return ua.includes('wxwork'); // 企业微信 UA 一定包含 wxwork
|
||||
}
|
||||
|
||||
export function isDingTalkBrowser() {
|
||||
const ua = window.navigator.userAgent.toLowerCase();
|
||||
return (
|
||||
ua.includes('dingtalk') ||
|
||||
ua.includes('aliapp(dingtalk') ||
|
||||
(getQueryVariable('authCode') !== '' &&
|
||||
getQueryVariable('authCode') !== undefined &&
|
||||
getQueryVariable('authCode') !== null)
|
||||
);
|
||||
}
|
||||
|
||||
// 飞书
|
||||
export function isLarkBrowser(): boolean {
|
||||
const ua = window.navigator.userAgent.toLowerCase();
|
||||
return ua.includes('lark') || ua.includes('feishu') || getQueryVariable('state') === 'LARK';
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际单位数字缩写
|
||||
* @param amount 金额数字
|
||||
* @param decimals 保留小数位数
|
||||
* @param currency 货币单位
|
||||
*/
|
||||
export function abbreviateNumber(count: number | string, currency: string, decimals = 2) {
|
||||
if (typeof count !== 'number') {
|
||||
return { value: '-', unit: '', full: '-' };
|
||||
}
|
||||
|
||||
const locale = localStorage.getItem('CRM-locale') || 'zh-CN';
|
||||
const truncateNumber = (num: number) => {
|
||||
const factor = 10 ** decimals;
|
||||
return Math.round(num * factor) / factor;
|
||||
};
|
||||
|
||||
const full = `${count.toLocaleString('en-US', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
})} (${currency})`;
|
||||
|
||||
let value = '';
|
||||
let unit = '';
|
||||
|
||||
if (locale === 'zh-CN') {
|
||||
if (count >= 1e8) {
|
||||
value = truncateNumber(count / 1e8).toString();
|
||||
unit = '亿';
|
||||
} else if (count >= 1e4) {
|
||||
value = truncateNumber(count / 1e4).toString();
|
||||
unit = '万';
|
||||
} else {
|
||||
value = truncateNumber(count).toString();
|
||||
unit = '';
|
||||
}
|
||||
} else if (locale === 'en-US') {
|
||||
if (count >= 1e9) {
|
||||
value = truncateNumber(count / 1e9).toString();
|
||||
unit = 'B';
|
||||
} else if (count >= 1e6) {
|
||||
value = truncateNumber(count / 1e6).toString();
|
||||
unit = 'M';
|
||||
} else if (count >= 1e3) {
|
||||
value = truncateNumber(count / 1e3).toString();
|
||||
unit = 'K';
|
||||
} else {
|
||||
value = truncateNumber(count).toString();
|
||||
unit = '';
|
||||
}
|
||||
}
|
||||
|
||||
return { value, unit, full };
|
||||
}
|
||||
|
||||
export function getFileIconType(type: string) {
|
||||
switch (type) {
|
||||
case 'zip':
|
||||
return 'icona-icon_file-compressed_colorful';
|
||||
case 'ppt':
|
||||
return 'iconicon_file-ppt_colorful';
|
||||
case 'pdf':
|
||||
return 'iconicon_file-pdf_colorful';
|
||||
case 'docx':
|
||||
return 'iconicon_file-word_colorful';
|
||||
case 'xlsx':
|
||||
return 'iconicon_file-excel_colorful';
|
||||
case 'csv':
|
||||
return 'iconicon_file-CSV_colorful';
|
||||
case 'xmind':
|
||||
return 'iconicon_file-xmind_colorful';
|
||||
case 'sql':
|
||||
return 'iconicon_file-sql_colorful';
|
||||
case 'jar':
|
||||
return 'icona-icon_file-jar_colorful';
|
||||
case 'json':
|
||||
return 'icona-icon_file-json';
|
||||
case 'jmx':
|
||||
return 'icona-icon_file-JMX';
|
||||
case 'har':
|
||||
return 'iconicon_file_har';
|
||||
case 'mp4':
|
||||
case 'mov':
|
||||
case 'wmv':
|
||||
return 'iconicon_file_video_colorful';
|
||||
default:
|
||||
return /(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(type)
|
||||
? 'iconicon_file-image_colorful'
|
||||
: 'iconicon_file-unknown_colorful1';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制字符串长度并添加后缀(如 copy),保证总长度不超过 maxLen
|
||||
* @param name 原名称
|
||||
* @param suffix 后缀(默认 "copy")
|
||||
* @param maxLen 最大长度(默认 255)
|
||||
*/
|
||||
export function getCopiedName(name: string, suffix = 'copy', maxLen = 255): string {
|
||||
const baseName = name || '';
|
||||
if (baseName.length + suffix.length > maxLen) {
|
||||
return baseName.slice(0, maxLen - suffix.length) + suffix;
|
||||
}
|
||||
return baseName + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用数字千分位展示
|
||||
* @param value 数值或数值字符串
|
||||
* @param options 小数位配置
|
||||
* @returns 格式化后的字符串
|
||||
*/
|
||||
export function formatThousands(
|
||||
value: string | number | null | undefined,
|
||||
options?: {
|
||||
minimumFractionDigits?: number;
|
||||
maximumFractionDigits?: number;
|
||||
}
|
||||
) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const numberValue = Number(value);
|
||||
if (!Number.isFinite(numberValue)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
const decimalLength = String(value).includes('.') ? String(value).split('.')[1]?.length || 0 : 0;
|
||||
|
||||
return numberValue.toLocaleString('en-US', {
|
||||
minimumFractionDigits: options?.minimumFractionDigits,
|
||||
maximumFractionDigits: options?.maximumFractionDigits ?? decimalLength,
|
||||
});
|
||||
}
|
||||
54
frontend/packages/lib-shared/method/is.ts
Normal file
54
frontend/packages/lib-shared/method/is.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
const opt = Object.prototype.toString;
|
||||
|
||||
export function isArray(obj: any): obj is any[] {
|
||||
return opt.call(obj) === '[object Array]';
|
||||
}
|
||||
|
||||
export function isObject(obj: any): obj is { [key: string]: any } {
|
||||
return opt.call(obj) === '[object Object]';
|
||||
}
|
||||
|
||||
export function isString(obj: any): obj is string {
|
||||
return opt.call(obj) === '[object String]';
|
||||
}
|
||||
|
||||
export function isNumber(obj: any): obj is number {
|
||||
return opt.call(obj) === '[object Number]' && obj === obj; // eslint-disable-line
|
||||
}
|
||||
|
||||
export function isRegExp(obj: any) {
|
||||
return opt.call(obj) === '[object RegExp]';
|
||||
}
|
||||
|
||||
export function isFile(obj: any): obj is File {
|
||||
return opt.call(obj) === '[object File]';
|
||||
}
|
||||
|
||||
export function isBlob(obj: any): obj is Blob {
|
||||
return opt.call(obj) === '[object Blob]';
|
||||
}
|
||||
|
||||
export function isUndefined(obj: any): obj is undefined {
|
||||
return obj === undefined;
|
||||
}
|
||||
|
||||
export function isNull(obj: any): obj is null {
|
||||
return obj === null;
|
||||
}
|
||||
|
||||
export function isFunction(obj: any): obj is (...args: any[]) => any {
|
||||
return typeof obj === 'function';
|
||||
}
|
||||
|
||||
export function isEmptyObject(obj: any): boolean {
|
||||
return isObject(obj) && Object.keys(obj).length === 0;
|
||||
}
|
||||
|
||||
export function isExist(obj: any): boolean {
|
||||
return obj || obj === 0;
|
||||
}
|
||||
|
||||
// 判断变量非空值
|
||||
export function isNotEmpty(obj: any): boolean {
|
||||
return obj !== undefined && obj !== null && obj !== '';
|
||||
}
|
||||
53
frontend/packages/lib-shared/method/local-storage.ts
Normal file
53
frontend/packages/lib-shared/method/local-storage.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export const getLocalStorage = <T = string>(name: string, isJson?: boolean): T | null => {
|
||||
try {
|
||||
const value = localStorage.getItem(name);
|
||||
if (value && isJson) {
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
return value as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const setLocalStorage = (name: string, value: any): void => {
|
||||
try {
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
localStorage.setItem(name, value);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
export const removeLocalStorage = (name: string) => {
|
||||
try {
|
||||
localStorage.removeItem(name);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
export const setSessionStorageTempState = (name: string, value: any) => {
|
||||
try {
|
||||
if (typeof value !== 'string') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
sessionStorage.setItem(name, value);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
export const getSessionStorageTempState = <T>(name: string, isJson?: boolean): T | null => {
|
||||
try {
|
||||
const value = sessionStorage.getItem(name);
|
||||
if (value && isJson) {
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
return value as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
39
frontend/packages/lib-shared/method/route-listener.ts
Normal file
39
frontend/packages/lib-shared/method/route-listener.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
|
||||
*/
|
||||
import mitt, { Handler } from 'mitt';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
const emitter = mitt();
|
||||
|
||||
const key = Symbol('ROUTE_CHANGE');
|
||||
|
||||
let latestRoute: RouteLocationNormalized;
|
||||
|
||||
/**
|
||||
* 设置路由监听
|
||||
* @param to 要跳转的路由信息
|
||||
*/
|
||||
export function setRouteEmitter(to: RouteLocationNormalized) {
|
||||
emitter.emit(key, to);
|
||||
latestRoute = to;
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听路由变化
|
||||
* @param handler 处理回调
|
||||
* @param immediate 是否立即执行
|
||||
*/
|
||||
export function listenerRouteChange(handler: (route: RouteLocationNormalized) => void, immediate = true) {
|
||||
emitter.on(key, handler as Handler);
|
||||
if (immediate && latestRoute) {
|
||||
handler(latestRoute);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除路由监听
|
||||
*/
|
||||
export function removeRouteListener() {
|
||||
emitter.off(key);
|
||||
}
|
||||
93
frontend/packages/lib-shared/method/scriptLoader.ts
Normal file
93
frontend/packages/lib-shared/method/scriptLoader.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { setupDrag } from './setupDrag';
|
||||
import { CompanyTypeEnum } from '@lib/shared/enums/commonEnum';
|
||||
|
||||
interface ScriptOptions {
|
||||
identifier: string; // 脚本标识
|
||||
}
|
||||
|
||||
const scriptElementsMap = new Map<string, string>();
|
||||
|
||||
function extractSQLBotId(input: string) {
|
||||
const regex = /sqlbot-[^\s"']+/;
|
||||
const match = input.match(regex);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
export function loadScript(scriptContent: string, options: ScriptOptions): Promise<void> {
|
||||
if (!scriptContent) {
|
||||
return Promise.reject(new Error('scriptContent is empty'));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = scriptContent?.trim();
|
||||
if (scriptElementsMap.has(options.identifier)) return;
|
||||
|
||||
// 处理IIFE格式
|
||||
if (content.startsWith('(function')) {
|
||||
const scriptId = extractSQLBotId(content);
|
||||
if (scriptId) {
|
||||
scriptElementsMap.set(options.identifier, scriptId);
|
||||
}
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(content);
|
||||
setTimeout(() => {
|
||||
const button = document.querySelector('.sqlbot-assistant-chat-button');
|
||||
setupDrag(button as HTMLElement);
|
||||
}, 300); // 等待DOM渲染
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理<script>标签
|
||||
if (content.startsWith('<script')) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = content;
|
||||
const originalScript = div.querySelector('script');
|
||||
if (!originalScript) {
|
||||
reject(new Error('无效的script标签'));
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
// 复制所有属性
|
||||
for (let i = 0; i < originalScript.attributes.length; i++) {
|
||||
const attr = originalScript.attributes[i];
|
||||
if (attr.name === 'id') {
|
||||
scriptElementsMap.set(options.identifier, attr.value);
|
||||
}
|
||||
script.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
script.onload = () => {
|
||||
setTimeout(() => {
|
||||
const button = document.querySelector('.sqlbot-assistant-chat-button');
|
||||
setupDrag(button as HTMLElement);
|
||||
}, 300); // 等待DOM渲染
|
||||
};
|
||||
|
||||
document.body.appendChild(script);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('不支持的脚本格式'));
|
||||
});
|
||||
}
|
||||
|
||||
export function removeScript(identifier: string): void {
|
||||
const scriptId = scriptElementsMap.get(identifier);
|
||||
if (scriptId && identifier === CompanyTypeEnum.SQLBot) {
|
||||
// 清理全局单例标记
|
||||
const propName = `${scriptId}-state`;
|
||||
delete (window as any)[propName];
|
||||
if ((window as any).sqlbot_assistant_handler) {
|
||||
delete (window as any).sqlbot_assistant_handler;
|
||||
}
|
||||
// 删除页面上渲染的
|
||||
const floatingElements = document.querySelectorAll('[id^="sqlbot-"]');
|
||||
floatingElements.forEach((el) => {
|
||||
if (el.parentNode && !el.parentNode.isEqualNode(document.body) && !el.parentNode.isEqualNode(document.head)) {
|
||||
(el.parentNode as Element).remove();
|
||||
}
|
||||
el.remove();
|
||||
});
|
||||
scriptElementsMap.delete(identifier);
|
||||
}
|
||||
}
|
||||
113
frontend/packages/lib-shared/method/setupDrag.ts
Normal file
113
frontend/packages/lib-shared/method/setupDrag.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
interface DragPosition {
|
||||
startX: number;
|
||||
startY: number;
|
||||
startLeft: number;
|
||||
startTop: number;
|
||||
}
|
||||
|
||||
export function setupDrag(dragElement: HTMLElement | null) {
|
||||
if (!dragElement) return;
|
||||
let isDragging = false;
|
||||
let dragPosition: DragPosition;
|
||||
|
||||
const moveHandler = (clientX: number, clientY: number) => {
|
||||
dragElement.style.cursor = 'grabbing';
|
||||
|
||||
// 移动超过5px才认为是拖动
|
||||
if (!isDragging && (Math.abs(clientX - dragPosition.startX) > 5 || Math.abs(clientY - dragPosition.startY) > 5)) {
|
||||
isDragging = true;
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
// 计算新位置
|
||||
const newLeft = dragPosition.startLeft + clientX - dragPosition.startX;
|
||||
const newTop = dragPosition.startTop + clientY - dragPosition.startY;
|
||||
|
||||
// 边界检查(可选) - 确保元素不会移出视口
|
||||
const maxX = window.innerWidth - dragElement.offsetWidth;
|
||||
const maxY = window.innerHeight - dragElement.offsetHeight;
|
||||
|
||||
// 设置新位置(限制在边界内)
|
||||
dragElement.style.left = `${Math.max(0, Math.min(newLeft, maxX))}px`;
|
||||
dragElement.style.top = `${Math.max(0, Math.min(newTop, maxY))}px`;
|
||||
|
||||
dragElement.style.right = 'auto';
|
||||
dragElement.style.bottom = 'auto';
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标/触摸移动处理
|
||||
const move = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault();
|
||||
if (e instanceof MouseEvent) {
|
||||
moveHandler(e.clientX, e.clientY);
|
||||
} else if (e.touches?.[0]) {
|
||||
moveHandler(e.touches[0].clientX, e.touches[0].clientY);
|
||||
}
|
||||
};
|
||||
|
||||
// 停止拖动
|
||||
const stopDrag = () => {
|
||||
document.removeEventListener('mousemove', move);
|
||||
document.removeEventListener('touchmove', move);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
document.removeEventListener('touchend', stopDrag);
|
||||
dragElement.style.cursor = 'pointer';
|
||||
|
||||
// 如果是拖拽操作(不是点击),则阻止接下来的点击事件
|
||||
if (isDragging) {
|
||||
const clickHandler = (e: Event) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
dragElement.removeEventListener('click', clickHandler);
|
||||
};
|
||||
|
||||
dragElement.addEventListener('click', clickHandler, true);
|
||||
|
||||
// 300ms后移除点击拦截
|
||||
setTimeout(() => {
|
||||
dragElement.removeEventListener('click', clickHandler, true);
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始拖动 - 鼠标事件
|
||||
const startMouseDrag = (e: MouseEvent) => {
|
||||
isDragging = false;
|
||||
const style = window.getComputedStyle(dragElement);
|
||||
|
||||
dragPosition = {
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
startLeft: parseInt(style.left, 10) || 0,
|
||||
startTop: parseInt(style.top, 10) || 0,
|
||||
};
|
||||
// 添加移动和松开事件
|
||||
document.addEventListener('mousemove', move);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
};
|
||||
|
||||
// 开始拖动 - 触摸事件
|
||||
const startTouchDrag = (e: TouchEvent) => {
|
||||
isDragging = false;
|
||||
if (e.touches[0]) {
|
||||
const style = window.getComputedStyle(dragElement);
|
||||
|
||||
dragPosition = {
|
||||
startX: e.touches[0].clientX,
|
||||
startY: e.touches[0].clientY,
|
||||
startLeft: parseInt(style.left, 10) || 0,
|
||||
startTop: parseInt(style.top, 10) || 0,
|
||||
};
|
||||
|
||||
document.addEventListener('touchmove', move, { passive: false });
|
||||
document.addEventListener('touchend', stopDrag);
|
||||
}
|
||||
};
|
||||
|
||||
// 添加事件监听
|
||||
dragElement.addEventListener('mousedown', startMouseDrag);
|
||||
dragElement.addEventListener('touchstart', startTouchDrag, { passive: false });
|
||||
}
|
||||
|
||||
export default {};
|
||||
81
frontend/packages/lib-shared/method/validate.ts
Normal file
81
frontend/packages/lib-shared/method/validate.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
// 邮箱校验
|
||||
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
// 手机号校验,11位
|
||||
export const phoneRegex = /^\d{11}$/;
|
||||
// 密码校验,8-32位
|
||||
export const passwordLengthRegex = /^.{8,32}$/;
|
||||
// 密码校验,必须包含数字和字母,特殊符号范围校验
|
||||
export const passwordWordRegex = /^(?=.*\d)(?=.*[a-zA-Z])[0-9a-zA-Z!@#$%^&*()_+.]+$/;
|
||||
// Git地址校验
|
||||
export const gitRepositoryUrlRegex = /\.git$/;
|
||||
// Webhook 地址校验,允许 HTTP / HTTPS
|
||||
export const httpUrlRegex = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
||||
|
||||
/**
|
||||
* 校验邮箱
|
||||
* @param email 邮箱
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validateEmail(email: string): boolean {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验手机号
|
||||
* @param phone 手机号
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validatePhone(phone: string): boolean {
|
||||
return phoneRegex.test(phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码长度
|
||||
* @param password 密码
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validatePasswordLength(password: string): boolean {
|
||||
return passwordLengthRegex.test(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码组成
|
||||
* @param password 密码
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validateWordPassword(password: string): boolean {
|
||||
return passwordWordRegex.test(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验密码
|
||||
* @param password 密码
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validatePassword(password: string): boolean {
|
||||
return validatePasswordLength(password) && validateWordPassword(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 HTTP / HTTPS 地址
|
||||
* @param url 地址
|
||||
* @returns boolean
|
||||
*/
|
||||
export function validateHttpUrl(url: string): boolean {
|
||||
return httpUrlRegex.test(url.trim());
|
||||
}
|
||||
|
||||
export function getPatternByAreaCode(code: string): RegExp | null {
|
||||
switch (code) {
|
||||
case '+86': // 中国大陆
|
||||
return /^\d{10,12}$/;
|
||||
case '+852': // 香港
|
||||
return /^\d{8}$/;
|
||||
case '+853': // 澳门
|
||||
return /^\d{8}$/;
|
||||
case '+886': // 台湾
|
||||
return /^\d{8,11}$/;
|
||||
default: // 其他
|
||||
return /^\d+$/;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user