init
This commit is contained in:
45
src/utils/antiDebug.ts
Normal file
45
src/utils/antiDebug.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import DisableDevtool from 'disable-devtool';
|
||||
|
||||
/**
|
||||
* 安全警告的HTML模板
|
||||
* 现代风格的警告提示,压缩优化版本
|
||||
*/
|
||||
const SECURITY_WARNING_TEMPLATE = `
|
||||
<style>
|
||||
.w-box{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90%;max-width:400px;padding:1.5rem;background:#fff;box-shadow:0 15px 20px rgba(0,0,0,.1);border-radius:12px;text-align:center;font-family:system-ui,-apple-system,sans-serif}
|
||||
.w-icon{margin:0 auto 1.25rem;width:56px;height:56px;background:linear-gradient(45deg,#ff6b6b,#ff8787);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 8px rgba(255,107,107,.15);animation:p 2s infinite}
|
||||
.w-icon::before{content:"!";color:#fff;font-size:2rem;font-weight:700}
|
||||
.w-title{font-size:1.125rem;font-weight:600;color:#1f2937;margin-bottom:.75rem}
|
||||
.w-desc{font-size:.875rem;color:#6b7280}
|
||||
@keyframes p{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
|
||||
@media(prefers-color-scheme:dark){
|
||||
.w-box{background:#1f2937;box-shadow:0 15px 20px rgba(0,0,0,.2)}
|
||||
.w-icon{background:linear-gradient(45deg,#f87171,#fb7185)}
|
||||
.w-title{color:#f3f4f6}
|
||||
.w-desc{color:#9ca3af}
|
||||
}
|
||||
</style>
|
||||
<div class="w-box">
|
||||
<div class="w-icon"></div>
|
||||
<div class="w-title">不合规操作,系统将自动关闭退出!</div>
|
||||
<div class="w-desc">如您频繁此类操作,系统将记录上报。</div>
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* 初始化反调试功能,
|
||||
* 通过ddtk 参数来控制绕过反调试功能,
|
||||
* http://localhost/?ddtk=pig#/flow/task/started
|
||||
*/
|
||||
export function initAntiDebug(): void {
|
||||
if (import.meta.env.VITE_ENABLE_ANTI_DEBUG !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
const debugKey = import.meta.env.VITE_ANTI_DEBUG_KEY || 'pig';
|
||||
|
||||
DisableDevtool({
|
||||
md5: DisableDevtool.md5(debugKey),
|
||||
disableMenu: false,
|
||||
rewriteHTML: SECURITY_WARNING_TEMPLATE,
|
||||
});
|
||||
}
|
||||
71
src/utils/apiCrypto.ts
Normal file
71
src/utils/apiCrypto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import other from './other';
|
||||
|
||||
// 从环境变量获取加密密钥
|
||||
const ENCRYPTION_KEY = import.meta.env.VITE_PWD_ENC_KEY;
|
||||
const ENCRYPTION_ENABLED = import.meta.env.VITE_API_ENC_ENABLED === 'true' ?? false;
|
||||
|
||||
/**
|
||||
* 使用指定密钥加密数据
|
||||
* @param data - 需要加密的数据
|
||||
* @returns 加密后的数据,如果加密未启用则返回原始数据
|
||||
*/
|
||||
export function encrypt(data: any): string {
|
||||
if (!data) return '';
|
||||
if (!ENCRYPTION_ENABLED) return data;
|
||||
|
||||
// 如果不是 number 或者 string 类型的数据,转为 JSON 字符串
|
||||
if (typeof data !== 'string' && typeof data !== 'number') {
|
||||
data = JSON.stringify(data);
|
||||
}
|
||||
|
||||
return other.encryption(data, ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定密钥解密数据
|
||||
* @param encryptedData - 需要解密的数据
|
||||
* @returns 解密后的数据,如果加密未启用则返回原始数据
|
||||
*/
|
||||
export function decrypt(encryptedData: string): any {
|
||||
if (!encryptedData) return null;
|
||||
if (!ENCRYPTION_ENABLED) return encryptedData;
|
||||
|
||||
const decrypted = other.decryption(encryptedData, ENCRYPTION_KEY);
|
||||
try {
|
||||
return JSON.parse(decrypted);
|
||||
} catch {
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密GET请求的参数
|
||||
* @param params - 需要加密的请求参数对象
|
||||
* @returns 加密并URL编码后的参数对象
|
||||
*/
|
||||
export function encryptRequestParams(params: Record<string, any>): Record<string, string> {
|
||||
if (!ENCRYPTION_ENABLED) return params;
|
||||
|
||||
const encryptedParams: Record<string, string> = {};
|
||||
for (const [paramKey, value] of Object.entries(params)) {
|
||||
if (value != null) {
|
||||
const stringValue = value.toString();
|
||||
encryptedParams[paramKey] = ENCRYPTION_ENABLED ? encodeURIComponent(encrypt(stringValue)) : encodeURIComponent(stringValue);
|
||||
}
|
||||
}
|
||||
|
||||
return encryptedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数据包装成加密信封格式
|
||||
* @param data - 需要加密的数据
|
||||
* @returns 如果加密启用则返回包含加密数据的信封对象 { encryption: string },否则返回原始数据
|
||||
*/
|
||||
export function wrapEncryption(data: any): { encryption: string } | any {
|
||||
if (!ENCRYPTION_ENABLED) return data;
|
||||
|
||||
return {
|
||||
encryption: encrypt(data),
|
||||
};
|
||||
}
|
||||
65
src/utils/arrayOperation.ts
Normal file
65
src/utils/arrayOperation.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 判断两数组字符串是否相同(用于按钮权限验证),数组字符串中存在相同时会自动去重(按钮权限标识不会重复)
|
||||
* @param news 新数据
|
||||
* @param old 源数据
|
||||
* @returns 两数组相同返回 `true`,反之则反
|
||||
*/
|
||||
export function judementSameArr(newArr: unknown[] | string[], oldArr: string[]): boolean {
|
||||
const news = removeDuplicate(newArr);
|
||||
const olds = removeDuplicate(oldArr);
|
||||
let count = 0;
|
||||
const leng = news.length;
|
||||
for (let i in olds) {
|
||||
for (let j in news) {
|
||||
if (olds[i] === news[j]) count++;
|
||||
}
|
||||
}
|
||||
return count === leng ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断两个对象是否相同
|
||||
* @param a 要比较的对象一
|
||||
* @param b 要比较的对象二
|
||||
* @returns 相同返回 true,反之则反
|
||||
*/
|
||||
export function isObjectValueEqual<T>(a: T, b: T): boolean {
|
||||
if (!a || !b) return false;
|
||||
let aProps = Object.getOwnPropertyNames(a);
|
||||
let bProps = Object.getOwnPropertyNames(b);
|
||||
if (aProps.length != bProps.length) return false;
|
||||
for (let i = 0; i < aProps.length; i++) {
|
||||
let propName = aProps[i];
|
||||
let propA = a[propName];
|
||||
let propB = b[propName];
|
||||
if (!b.hasOwnProperty(propName)) return false;
|
||||
if (propA instanceof Object) {
|
||||
if (!isObjectValueEqual(propA, propB)) return false;
|
||||
} else if (propA !== propB) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数组、数组对象去重
|
||||
* @param arr 数组内容
|
||||
* @param attr 需要去重的键值(数组对象)
|
||||
* @returns
|
||||
*/
|
||||
export function removeDuplicate(arr: EmptyArrayType, attr?: string) {
|
||||
if (!Object.keys(arr).length) {
|
||||
return arr;
|
||||
} else {
|
||||
if (attr) {
|
||||
const obj: EmptyObjectType = {};
|
||||
return arr.reduce((cur: EmptyArrayType[], item: EmptyArrayType) => {
|
||||
obj[item[attr]] ? '' : (obj[item[attr]] = true && item[attr] && cur.push(item));
|
||||
return cur;
|
||||
}, []);
|
||||
} else {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/utils/authFunction.ts
Normal file
38
src/utils/authFunction.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useUserInfo } from '/@/stores/userInfo';
|
||||
import { judementSameArr } from '/@/utils/arrayOperation';
|
||||
|
||||
/**
|
||||
* 单个权限验证
|
||||
* @param value 权限值
|
||||
* @returns 有权限,返回 `true`,反之则反
|
||||
*/
|
||||
export function auth(value: string): boolean {
|
||||
const stores = useUserInfo();
|
||||
return stores.userInfos.authBtnList.some((v: string) => v === value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个权限验证,满足一个则为 true
|
||||
* @param value 权限值
|
||||
* @returns 有权限,返回 `true`,反之则反
|
||||
*/
|
||||
export function auths(value: Array<string>): boolean {
|
||||
let flag = false;
|
||||
const stores = useUserInfo();
|
||||
stores.userInfos.authBtnList.map((val: string) => {
|
||||
value.map((v: string) => {
|
||||
if (val === v) flag = true;
|
||||
});
|
||||
});
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个权限验证,全部满足则为 true
|
||||
* @param value 权限值
|
||||
* @returns 有权限,返回 `true`,反之则反
|
||||
*/
|
||||
export function authAll(value: Array<string>): boolean {
|
||||
const stores = useUserInfo();
|
||||
return judementSameArr(value, stores.userInfos.authBtnList);
|
||||
}
|
||||
66
src/utils/commonFunction.ts
Normal file
66
src/utils/commonFunction.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// 通用函数
|
||||
import useClipboard from 'vue-clipboard3';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { formatDate } from '/@/utils/formatTime';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
export default function () {
|
||||
const { t } = useI18n();
|
||||
const { toClipboard } = useClipboard();
|
||||
|
||||
// 百分比格式化
|
||||
const percentFormat = (row: EmptyArrayType, column: number, cellValue: string) => {
|
||||
return cellValue ? `${cellValue}%` : '-';
|
||||
};
|
||||
// 列表日期时间格式化
|
||||
const dateFormatYMD = (row: EmptyArrayType, column: number, cellValue: string) => {
|
||||
if (!cellValue) return '-';
|
||||
return formatDate(new Date(cellValue), 'YYYY-mm-dd');
|
||||
};
|
||||
// 列表日期时间格式化
|
||||
const dateFormatYMDHMS = (row: EmptyArrayType, column: number, cellValue: string) => {
|
||||
if (!cellValue) return '-';
|
||||
return formatDate(new Date(cellValue), 'YYYY-mm-dd HH:MM:SS');
|
||||
};
|
||||
// 列表日期时间格式化
|
||||
const dateFormatHMS = (row: EmptyArrayType, column: number, cellValue: string) => {
|
||||
if (!cellValue) return '-';
|
||||
let time = 0;
|
||||
if (typeof row === 'number') time = row;
|
||||
if (typeof cellValue === 'number') time = cellValue;
|
||||
return formatDate(new Date(time * 1000), 'HH:MM:SS');
|
||||
};
|
||||
// 小数格式化
|
||||
const scaleFormat = (value: string = '0', scale: number = 4) => {
|
||||
return Number.parseFloat(value).toFixed(scale);
|
||||
};
|
||||
// 小数格式化
|
||||
const scale2Format = (value: string = '0') => {
|
||||
return Number.parseFloat(value).toFixed(2);
|
||||
};
|
||||
// 点击复制文本
|
||||
const copyText = (text: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
//复制
|
||||
toClipboard(text);
|
||||
//下面可以设置复制成功的提示框等操作
|
||||
ElMessage.success(t('layout.copyTextSuccess'));
|
||||
resolve(text);
|
||||
} catch (e) {
|
||||
//复制失败
|
||||
ElMessage.error(t('layout.copyTextError'));
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
percentFormat,
|
||||
dateFormatYMD,
|
||||
dateFormatYMDHMS,
|
||||
dateFormatHMS,
|
||||
scaleFormat,
|
||||
scale2Format,
|
||||
copyText,
|
||||
};
|
||||
}
|
||||
13
src/utils/errorCode.ts
Normal file
13
src/utils/errorCode.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
'000': '操作太频繁,请勿重复请求',
|
||||
'401': '当前操作没有权限',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '资源不存在',
|
||||
'417': '未绑定登录账号,请使用密码登录后绑定',
|
||||
'423': '演示环境不能操作,如需了解联系我们',
|
||||
'426': '用户名不存在或密码错误',
|
||||
'428': '验证码错误,请重新输入',
|
||||
'429': '请求过频繁',
|
||||
'479': '演示环境,没有权限操作',
|
||||
default: '系统未知错误,请反馈给管理员',
|
||||
};
|
||||
235
src/utils/formatTime.ts
Normal file
235
src/utils/formatTime.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 时间日期转换
|
||||
* @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 {
|
||||
let we = date.getDay(); // 星期
|
||||
let z = getWeek(date); // 周
|
||||
let 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 (let k in opt) {
|
||||
let 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 {
|
||||
let temptTime = new Date(dateTime.getTime());
|
||||
// 周几
|
||||
let weekday = temptTime.getDay() || 7;
|
||||
// 周1+5天=周六
|
||||
temptTime.setDate(temptTime.getDate() - weekday + 1 + 5);
|
||||
let firstDay = new Date(temptTime.getFullYear(), 0, 1);
|
||||
let dayOfWeek = firstDay.getDay();
|
||||
let spendDay = 1;
|
||||
if (dayOfWeek != 0) spendDay = 7 - dayOfWeek + 1;
|
||||
firstDay = new Date(temptTime.getFullYear(), 0, 1 + spendDay);
|
||||
let d = Math.ceil((temptTime.valueOf() - firstDay.valueOf()) / 86400000);
|
||||
let 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: string = 'YYYY-mm-dd'): 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天
|
||||
let 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 {
|
||||
let 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 '夜里好';
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export function parseTime(time, pattern?: string) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
return null;
|
||||
}
|
||||
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
|
||||
let date;
|
||||
if (typeof time === 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time);
|
||||
} else if (typeof time === 'string') {
|
||||
time = time
|
||||
.replace(new RegExp(/-/gm), '/')
|
||||
.replace('T', ' ')
|
||||
.replace(new RegExp(/\.[\d]{3}/gm), '');
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000;
|
||||
}
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value];
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
// 日期格式化
|
||||
export function parseDate(time, pattern?) {
|
||||
if (arguments.length === 0 || !time) {
|
||||
return null;
|
||||
}
|
||||
const format = pattern || '{y}-{m}-{d}';
|
||||
let date;
|
||||
if (typeof time === 'object') {
|
||||
date = time;
|
||||
} else {
|
||||
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
|
||||
time = parseInt(time);
|
||||
} else if (typeof time === 'string') {
|
||||
time = time
|
||||
.replace(new RegExp(/-/gm), '/')
|
||||
.replace('T', ' ')
|
||||
.replace(new RegExp(/\.[\d]{3}/gm), '');
|
||||
}
|
||||
if (typeof time === 'number' && time.toString().length === 10) {
|
||||
time = time * 1000;
|
||||
}
|
||||
date = new Date(time);
|
||||
}
|
||||
const formatObj = {
|
||||
y: date.getFullYear(),
|
||||
m: date.getMonth() + 1,
|
||||
d: date.getDate(),
|
||||
h: date.getHours(),
|
||||
i: date.getMinutes(),
|
||||
s: date.getSeconds(),
|
||||
a: date.getDay(),
|
||||
};
|
||||
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
|
||||
let value = formatObj[key];
|
||||
// Note: getDay() returns 0 on Sunday
|
||||
if (key === 'a') {
|
||||
return ['日', '一', '二', '三', '四', '五', '六'][value];
|
||||
}
|
||||
if (result.length > 0 && value < 10) {
|
||||
value = '0' + value;
|
||||
}
|
||||
return value || 0;
|
||||
});
|
||||
return time_str;
|
||||
}
|
||||
|
||||
export const dateTimeStr: string = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
export const dateStr: string = 'YYYY-MM-DD';
|
||||
|
||||
export const timeStr: string = 'HH:mm:ss';
|
||||
127
src/utils/getStyleSheets.ts
Normal file
127
src/utils/getStyleSheets.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { nextTick } from 'vue';
|
||||
import * as svg from '@element-plus/icons-vue';
|
||||
|
||||
// 获取阿里字体图标
|
||||
const getAlicdnIconfont = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
const styles: any = document.styleSheets;
|
||||
let sheetsList = [] as any[];
|
||||
let sheetsIconList = [] as any[];
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
if (styles[i].href && styles[i].href.indexOf('at.alicdn.com') > -1) {
|
||||
sheetsList.push(styles[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < sheetsList.length; i++) {
|
||||
for (let j = 0; j < sheetsList[i].cssRules.length; j++) {
|
||||
if (sheetsList[i].cssRules[j].selectorText && sheetsList[i].cssRules[j].selectorText.indexOf('.icon-') > -1) {
|
||||
sheetsIconList.push(
|
||||
`${sheetsList[i].cssRules[j].selectorText.substring(1, sheetsList[i].cssRules[j].selectorText.length).replace(/\:\:before/gi, '')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sheetsIconList.length > 0) resolve(sheetsIconList);
|
||||
else reject('未获取到值,请刷新重试');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化获取 css 样式,获取 element plus 自带 svg 图标,增加了 ele- 前缀,使用时:ele-Aim
|
||||
const getElementPlusIconfont = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
const icons = svg as any;
|
||||
const sheetsIconList = [] as any[];
|
||||
for (const i in icons) {
|
||||
sheetsIconList.push(`ele-${icons[i].name}`);
|
||||
}
|
||||
if (sheetsIconList.length > 0) resolve(sheetsIconList);
|
||||
else reject('未获取到值,请刷新重试');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化获取 css 样式,这里使用 fontawesome 的图标
|
||||
const getAwesomeIconfont = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
const styles: any = document.styleSheets;
|
||||
let sheetsList = [] as any[];
|
||||
let sheetsIconList = [] as any[];
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
if (styles[i].href && styles[i].href.indexOf('font-awesome') > -1) {
|
||||
sheetsList.push(styles[i]);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < sheetsList.length; i++) {
|
||||
for (let j = 0; j < sheetsList[i].cssRules.length; j++) {
|
||||
if (
|
||||
sheetsList[i].cssRules[j].selectorText &&
|
||||
sheetsList[i].cssRules[j].selectorText.indexOf('.fa-') === 0 &&
|
||||
sheetsList[i].cssRules[j].selectorText.indexOf(',') === -1
|
||||
) {
|
||||
if (/::before/.test(sheetsList[i].cssRules[j].selectorText)) {
|
||||
sheetsIconList.push(
|
||||
`${sheetsList[i].cssRules[j].selectorText.substring(1, sheetsList[i].cssRules[j].selectorText.length).replace(/\:\:before/gi, '')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sheetsIconList.length > 0) resolve(sheetsIconList.reverse());
|
||||
else reject('未获取到值,请刷新重试');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* 获取本地自带的图标
|
||||
* /src/assets/icons文件夹内的svg文件
|
||||
*/
|
||||
const getLocalIconfontNames = () => {
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
nextTick(() => {
|
||||
let iconfonts: string[] = [];
|
||||
|
||||
const svgEl = document.getElementById('local-icon');
|
||||
if (svgEl?.dataset.iconName) {
|
||||
iconfonts = (svgEl?.dataset.iconName as string).split(',');
|
||||
}
|
||||
|
||||
if (iconfonts.length > 0) {
|
||||
resolve(iconfonts);
|
||||
} else {
|
||||
reject('No Local Icons');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取字体图标 `document.styleSheets`
|
||||
* @method ali 获取阿里字体图标 `<i class="iconfont 图标类名"></i>`
|
||||
* @method ele 获取 element plus 自带图标 `<i class="图标类名"></i>`
|
||||
* @method ali 获取 fontawesome 的图标 `<i class="fa 图标类名"></i>`
|
||||
*/
|
||||
const initIconfont = {
|
||||
// iconfont
|
||||
ali: () => {
|
||||
return getAlicdnIconfont();
|
||||
},
|
||||
// element plus
|
||||
ele: () => {
|
||||
return getElementPlusIconfont();
|
||||
},
|
||||
// fontawesome
|
||||
awe: () => {
|
||||
return getAwesomeIconfont();
|
||||
},
|
||||
local: () => {
|
||||
return getLocalIconfontNames();
|
||||
},
|
||||
};
|
||||
|
||||
// 导出方法
|
||||
export default initIconfont;
|
||||
12
src/utils/globalProperties.ts
Normal file
12
src/utils/globalProperties.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {parseTime, parseDate, dateTimeStr, dateStr, timeStr} from './formatTime';
|
||||
import {App} from "vue";
|
||||
|
||||
// 定义全局可以使用属性方法
|
||||
export const properties = (app: App) => {
|
||||
app.config.globalProperties.parseTime = parseTime;
|
||||
app.config.globalProperties.parseDate = parseDate;
|
||||
app.config.globalProperties.dateTimeStr = dateTimeStr;
|
||||
app.config.globalProperties.dateStr = dateStr;
|
||||
app.config.globalProperties.timeStr = timeStr;
|
||||
app.config.globalProperties.baseURL = import.meta.env.VITE_API_URL;
|
||||
};
|
||||
44
src/utils/loading.ts
Normal file
44
src/utils/loading.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { nextTick } from 'vue';
|
||||
import '/@/theme/loading.scss';
|
||||
|
||||
/**
|
||||
* 页面全局 Loading
|
||||
* @method start 创建 loading
|
||||
* @method done 移除 loading
|
||||
*/
|
||||
export const NextLoading = {
|
||||
// 创建 loading
|
||||
start: () => {
|
||||
const bodys: Element = document.body;
|
||||
const div = <HTMLElement>document.createElement('div');
|
||||
div.setAttribute('class', 'loading-next');
|
||||
const htmls = `
|
||||
<div class="loading-next-box">
|
||||
<div class="loading-next-box-warp">
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
<div class="loading-next-box-item"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
div.innerHTML = htmls;
|
||||
bodys.insertBefore(div, bodys.childNodes[0]);
|
||||
window.nextLoading = true;
|
||||
},
|
||||
// 移除 loading
|
||||
done: (time: number = 0) => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
window.nextLoading = false;
|
||||
const el = <HTMLElement>document.querySelector('.loading-next');
|
||||
el?.parentNode?.removeChild(el);
|
||||
}, time);
|
||||
});
|
||||
},
|
||||
};
|
||||
8
src/utils/mitt.ts
Normal file
8
src/utils/mitt.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// https://www.npmjs.com/package/mitt
|
||||
import mitt, { Emitter } from 'mitt';
|
||||
|
||||
// 类型
|
||||
const emitter: Emitter<MittType> = mitt<MittType>();
|
||||
|
||||
// 导出
|
||||
export default emitter;
|
||||
529
src/utils/other.ts
Normal file
529
src/utils/other.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
import {nextTick} from 'vue';
|
||||
import router from '/@/router/index';
|
||||
import pinia from '/@/stores/index';
|
||||
import {storeToRefs} from 'pinia';
|
||||
import {useThemeConfig} from '/@/stores/themeConfig';
|
||||
import {i18n} from '/@/i18n/index';
|
||||
import {Local} from '/@/utils/storage';
|
||||
import {verifyUrl} from '/@/utils/toolsValidate';
|
||||
import request from '/@/utils/request';
|
||||
import {useMessage} from '/@/hooks/message';
|
||||
import * as CryptoJS from 'crypto-js';
|
||||
import {sm4} from 'sm-crypto'
|
||||
import {validateNull} from './validate';
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器标题国际化
|
||||
* @method const title = useTitle(); ==> title()
|
||||
*/
|
||||
export function useTitle() {
|
||||
const stores = useThemeConfig(pinia);
|
||||
const {themeConfig} = storeToRefs(stores);
|
||||
nextTick(() => {
|
||||
let globalTitle: string = themeConfig.value.globalTitle;
|
||||
let webTitle = setTagsViewNameI18n(router.currentRoute.value);
|
||||
document.title = `${webTitle} - ${globalTitle}` || globalTitle;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
|
||||
* @param params 路由 query、params 中的 tagsViewName
|
||||
* @returns 返回当前 tagsViewName 名称
|
||||
*/
|
||||
export function setTagsViewNameI18n(item: any) {
|
||||
let tagsViewName: string = '';
|
||||
const {query, params} = item;
|
||||
//修复tagsViewName匹配到其他含下列单词的路由
|
||||
const pattern = /^\{("(zh-cn|en|zh-tw)":"[^,]+",?){1,3}}$/;
|
||||
if (query?.tagsViewName || params?.tagsViewName) {
|
||||
if (pattern.test(query?.tagsViewName) || pattern.test(params?.tagsViewName)) {
|
||||
// 国际化
|
||||
const urlTagsParams = (query?.tagsViewName && JSON.parse(query?.tagsViewName)) || (params?.tagsViewName && JSON.parse(params?.tagsViewName));
|
||||
tagsViewName = urlTagsParams[i18n.global.locale.value];
|
||||
} else {
|
||||
// 非国际化
|
||||
tagsViewName = query?.tagsViewName || params?.tagsViewName;
|
||||
}
|
||||
} else {
|
||||
// 非自定义 tagsView 名称
|
||||
tagsViewName = i18n.global.t(item.name);
|
||||
}
|
||||
return tagsViewName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片懒加载
|
||||
* @param el dom 目标元素
|
||||
* @param arr 列表数据
|
||||
* @description data-xxx 属性用于存储页面或应用程序的私有自定义数据
|
||||
*/
|
||||
export const lazyImg = (el: string, arr: EmptyArrayType) => {
|
||||
const io = new IntersectionObserver((res) => {
|
||||
res.forEach((v: any) => {
|
||||
if (v.isIntersecting) {
|
||||
const {img, key} = v.target.dataset;
|
||||
v.target.src = img;
|
||||
v.target.onload = () => {
|
||||
io.unobserve(v.target);
|
||||
arr[key]['loading'] = false;
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
nextTick(() => {
|
||||
document.querySelectorAll(el).forEach((img) => io.observe(img));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 全局组件大小
|
||||
* @returns 返回 `window.localStorage` 中读取的缓存值 `globalComponentSize`
|
||||
*/
|
||||
export const globalComponentSize = (): string => {
|
||||
const stores = useThemeConfig(pinia);
|
||||
const {themeConfig} = storeToRefs(stores);
|
||||
return Local.get('themeConfig')?.globalComponentSize || themeConfig.value?.globalComponentSize;
|
||||
};
|
||||
|
||||
/**
|
||||
* 对象深克隆
|
||||
* @param obj 源对象
|
||||
* @returns 克隆后的对象
|
||||
*/
|
||||
export function deepClone(obj: EmptyObjectType) {
|
||||
let newObj: EmptyObjectType;
|
||||
try {
|
||||
newObj = obj.push ? [] : {};
|
||||
} catch (error) {
|
||||
newObj = {};
|
||||
}
|
||||
for (let attr in obj) {
|
||||
if (obj[attr] && typeof obj[attr] === 'object') {
|
||||
newObj[attr] = deepClone(obj[attr]);
|
||||
} else {
|
||||
newObj[attr] = obj[attr];
|
||||
}
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是移动端
|
||||
*/
|
||||
export function isMobile() {
|
||||
if (
|
||||
navigator.userAgent.match(
|
||||
/('phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone')/i
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断数组对象中所有属性是否为空,为空则删除当前行对象
|
||||
* @description @感谢大黄
|
||||
* @param list 数组对象
|
||||
* @returns 删除空值后的数组对象
|
||||
*/
|
||||
export function handleEmpty(list: EmptyArrayType) {
|
||||
const arr = [] as any[];
|
||||
for (const i in list) {
|
||||
const d = [] as any[];
|
||||
for (const j in list[i]) {
|
||||
d.push(list[i][j]);
|
||||
}
|
||||
const leng = d.filter((item) => item === '').length;
|
||||
if (leng !== d.length) {
|
||||
arr.push(list[i]);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开外部链接
|
||||
* @param val 当前点击项菜单
|
||||
*/
|
||||
export function handleOpenLink(val: RouteItem) {
|
||||
router.push(val.path);
|
||||
if (verifyUrl(<string>val.meta?.isLink)) window.open(val.meta?.isLink);
|
||||
else window.open(`${val.meta?.isLink}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开小窗口
|
||||
*/
|
||||
export const openWindow = (url: string, title: string, w: number, h: number) => {
|
||||
// @ts-ignore
|
||||
const dualScreenLeft = window.screenLeft !== undefined ? window.screenLeft : screen.left;
|
||||
// @ts-ignore
|
||||
const dualScreenTop = window.screenTop !== undefined ? window.screenTop : screen.top;
|
||||
|
||||
const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width;
|
||||
const height = window.innerHeight
|
||||
? window.innerHeight
|
||||
: document.documentElement.clientHeight
|
||||
? document.documentElement.clientHeight
|
||||
: screen.height;
|
||||
|
||||
const left = width / 2 - w / 2 + dualScreenLeft;
|
||||
const top = height / 2 - h / 2 + dualScreenTop;
|
||||
return window.open(
|
||||
url,
|
||||
title,
|
||||
'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=yes, copyhistory=no, width=' +
|
||||
w +
|
||||
', height=' +
|
||||
h +
|
||||
', top=' +
|
||||
top +
|
||||
', left=' +
|
||||
left
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
*加密处理
|
||||
*/
|
||||
export function encryption(src: string, keyWord: string) {
|
||||
const key = CryptoJS.enc.Utf8.parse(keyWord);
|
||||
// 加密
|
||||
var encrypted = CryptoJS.AES.encrypt(src, key, {
|
||||
iv: key,
|
||||
mode: CryptoJS.mode.CFB,
|
||||
padding: CryptoJS.pad.NoPadding,
|
||||
});
|
||||
return encrypted.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
* @param {*} params 参数列表
|
||||
* @returns 明文
|
||||
*/
|
||||
export function decryption(src: string, keyWord: string) {
|
||||
const key = CryptoJS.enc.Utf8.parse(keyWord);
|
||||
// 解密逻辑
|
||||
var decryptd = CryptoJS.AES.decrypt(src, key, {
|
||||
iv: key,
|
||||
mode: CryptoJS.mode.CFB,
|
||||
padding: CryptoJS.pad.NoPadding,
|
||||
});
|
||||
|
||||
return decryptd.toString(CryptoJS.enc.Utf8);
|
||||
}
|
||||
|
||||
/**
|
||||
* SM4加密处理
|
||||
*/
|
||||
export function sm4Encryption(src: string, keyWord: string) {
|
||||
return sm4.encrypt(src, keyWord);
|
||||
}
|
||||
|
||||
/**
|
||||
* SM4解密处理
|
||||
* @param {*} params 参数列表
|
||||
* @returns 明文
|
||||
*/
|
||||
export function sm4Decryption(src: string, keyWord: string) {
|
||||
return sm4.decrypt(src, keyWord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 加密
|
||||
* @param {*} src 明文
|
||||
* @returns 密文
|
||||
*/
|
||||
export function base64Encrypt(src: string) {
|
||||
const encodedWord = CryptoJS.enc.Utf8.parse(src);
|
||||
return CryptoJS.enc.Base64.stringify(encodedWord);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url 目标下载接口
|
||||
* @param query 查询参数
|
||||
* @param fileName 文件名称
|
||||
* @returns {*}
|
||||
*/
|
||||
export function downBlobFile(url: any, query: any, fileName: string) {
|
||||
return request({
|
||||
url: url,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
params: query,
|
||||
}).then((response) => {
|
||||
handleBlobFile(response, fileName);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* blob 文件刘处理
|
||||
* @param response 响应结果
|
||||
* @returns
|
||||
*/
|
||||
export function handleBlobFile(response: any, fileName: string) {
|
||||
// 处理返回的文件流
|
||||
const blob = response;
|
||||
if (blob && blob.size === 0) {
|
||||
useMessage().error('内容为空,无法下载');
|
||||
return;
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
|
||||
// 兼容一下 入参不是 File Blob 类型情况
|
||||
var binaryData = [] as any;
|
||||
binaryData.push(response);
|
||||
link.href = window.URL.createObjectURL(new Blob(binaryData));
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
window.setTimeout(function () {
|
||||
// @ts-ignore
|
||||
URL.revokeObjectURL(blob);
|
||||
document.body.removeChild(link);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 生成唯一 uuid
|
||||
* @return string
|
||||
*/
|
||||
export function generateUUID() {
|
||||
if (typeof crypto === 'object') {
|
||||
if (typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
|
||||
const callback = (c: any) => {
|
||||
const num = Number(c);
|
||||
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16);
|
||||
};
|
||||
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback);
|
||||
}
|
||||
}
|
||||
let timestamp = new Date().getTime();
|
||||
let performanceNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0;
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
let random = Math.random() * 16;
|
||||
if (timestamp > 0) {
|
||||
random = (timestamp + random) % 16 | 0;
|
||||
timestamp = Math.floor(timestamp / 16);
|
||||
} else {
|
||||
random = (performanceNow + random) % 16 | 0;
|
||||
performanceNow = Math.floor(performanceNow / 16);
|
||||
}
|
||||
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一批量导出
|
||||
* @method useTitle 设置浏览器标题国际化
|
||||
* @method setTagsViewNameI18n 设置 自定义 tagsView 名称、 自定义 tagsView 名称国际化
|
||||
* @method lazyImg 图片懒加载
|
||||
* @method globalComponentSize() element plus 全局组件大小
|
||||
* @method deepClone 对象深克隆
|
||||
* @method isMobile 判断是否是移动端
|
||||
* @method handleEmpty 判断数组对象中所有属性是否为空,为空则删除当前行对象
|
||||
* @method handleOpenLink 打开外部链接
|
||||
*/
|
||||
const other = {
|
||||
useTitle: () => {
|
||||
useTitle();
|
||||
},
|
||||
setTagsViewNameI18n(route: RouteToFrom) {
|
||||
return setTagsViewNameI18n(route);
|
||||
},
|
||||
lazyImg: (el: string, arr: EmptyArrayType) => {
|
||||
lazyImg(el, arr);
|
||||
},
|
||||
globalComponentSize: () => {
|
||||
return globalComponentSize();
|
||||
},
|
||||
deepClone: (obj: EmptyObjectType) => {
|
||||
return deepClone(obj);
|
||||
},
|
||||
isMobile: () => {
|
||||
return isMobile();
|
||||
},
|
||||
handleEmpty: (list: EmptyArrayType) => {
|
||||
return handleEmpty(list);
|
||||
},
|
||||
handleOpenLink: (val: RouteItem) => {
|
||||
handleOpenLink(val);
|
||||
},
|
||||
encryption: (src: string, keyWord: string) => {
|
||||
return encryption(src, keyWord);
|
||||
},
|
||||
decryption: (src: string, keyWord: string) => {
|
||||
return decryption(src, keyWord);
|
||||
},
|
||||
base64Encrypt: (data: any) => {
|
||||
return base64Encrypt(data);
|
||||
},
|
||||
downBlobFile: (url: any, query: any, fileName: string) => {
|
||||
return downBlobFile(url, query, fileName);
|
||||
},
|
||||
toUnderline: (str: string) => {
|
||||
return toUnderline(str);
|
||||
},
|
||||
openWindow: (url: string, title: string, w: number, h: number) => {
|
||||
return openWindow(url, title, w, h);
|
||||
},
|
||||
getQueryString: (url: string, paraName: string) => {
|
||||
return getQueryString(url, paraName);
|
||||
},
|
||||
adaptationUrl: (url?: string) => {
|
||||
return adaptationUrl(url);
|
||||
},
|
||||
resolveAllEunuchNodeId: (json: any[], idArr: any[], temp: any[] = []) => {
|
||||
return resolveAllEunuchNodeId(json, idArr, temp);
|
||||
},
|
||||
getNonDuplicateID: () => {
|
||||
return getNonDuplicateID();
|
||||
},
|
||||
|
||||
addUnit: (value: string | number, unit = 'px') => {
|
||||
return addUnit(value, unit);
|
||||
},
|
||||
validateNull: (value: any) => {
|
||||
return validateNull(value);
|
||||
},
|
||||
getNumberRadixNum: (input: Number) => {
|
||||
return getNumberRadixNum(input);
|
||||
}
|
||||
};
|
||||
|
||||
export function getNumberRadixNum(input: Number) {
|
||||
let strings = input.toString().split(".");
|
||||
if (strings.length <= 1) {
|
||||
return 0;
|
||||
}
|
||||
return strings[1].toString().length;
|
||||
};
|
||||
|
||||
export function getQueryString(url: string, paraName: string) {
|
||||
const arrObj = url.split('?');
|
||||
if (arrObj.length > 1) {
|
||||
const arrPara = arrObj[1].split('&');
|
||||
let arr;
|
||||
for (let i = 0; i < arrPara.length; i++) {
|
||||
arr = arrPara[i].split('=');
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (arr != null && arr[0] == paraName) {
|
||||
return arr[1];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表结构转树结构
|
||||
* @param data
|
||||
* @param id
|
||||
* @param parentId
|
||||
* @param children
|
||||
* @param rootId
|
||||
* @returns {*}
|
||||
*/
|
||||
export function handleTree(data: any, id: any, parentId: any, children: any, rootId: any) {
|
||||
id = id || 'id';
|
||||
parentId = parentId || 'parentId';
|
||||
children = children || 'children';
|
||||
rootId =
|
||||
rootId ||
|
||||
Math.min.apply(
|
||||
Math,
|
||||
data.map((item: any) => {
|
||||
return item[parentId];
|
||||
})
|
||||
) ||
|
||||
0;
|
||||
//对源数据深度克隆
|
||||
const cloneData = JSON.parse(JSON.stringify(data));
|
||||
//循环所有项
|
||||
const treeData = cloneData.filter((father: any) => {
|
||||
const branchArr = cloneData.filter((child: any) => {
|
||||
//返回每一项的子级数组
|
||||
return father[id] === child[parentId];
|
||||
});
|
||||
branchArr.length > 0 ? (father[children] = branchArr) : '';
|
||||
//返回第一层
|
||||
return father[parentId] === rootId;
|
||||
});
|
||||
return treeData !== '' ? treeData : data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析所有太监节点ID
|
||||
* @returns
|
||||
*/
|
||||
const resolveAllEunuchNodeId = (json: any[], idArr: any[], temp: any[] = []) => {
|
||||
for (const item of json) {
|
||||
if (item.children && item.children.length !== 0) {
|
||||
resolveAllEunuchNodeId(item.children, idArr, temp);
|
||||
} else {
|
||||
temp.push(...idArr.filter((id) => id === item.id));
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param str 驼峰转下划线
|
||||
* @returns 下划线
|
||||
*/
|
||||
export function toUnderline(str: string) {
|
||||
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动适配不同的后端架构
|
||||
* 1. 例如 /act/oa/task ,在微服务架构保持不变,在单体架构编程 /admin/oa/task
|
||||
* 2. 特殊 /gen/xxx ,在微服务架构、单体架构编程 都需保持不变
|
||||
*
|
||||
* @param originUrl 原始路径
|
||||
*/
|
||||
const adaptationUrl = (originUrl?: string) => {
|
||||
// 微服务架构 不做路径转换,为空不做路径转换
|
||||
const isMicro = import.meta.env.VITE_IS_MICRO;
|
||||
if (validateNull(isMicro) || isMicro === 'true') {
|
||||
return originUrl;
|
||||
}
|
||||
|
||||
// 转为 /admin 路由前缀的请求
|
||||
return `/admin/${originUrl?.split('/').splice(2).join('/')}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 获取不重复的id
|
||||
* @param length { Number } id的长度
|
||||
* @return { String } id
|
||||
*/
|
||||
const getNonDuplicateID = (length = 8) => {
|
||||
let idStr = Date.now().toString(36);
|
||||
idStr += Math.random().toString(36).substring(3, length);
|
||||
return idStr;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 添加单位
|
||||
* @param {String | Number} value 值 100
|
||||
* @param {String} unit 单位 px em rem
|
||||
*/
|
||||
const addUnit = (value: string | number, unit = 'px') => {
|
||||
return !Object.is(Number(value), NaN) ? `${value}${unit}` : value;
|
||||
};
|
||||
|
||||
// 统一批量导出
|
||||
export default other;
|
||||
14
src/utils/params.ts
Normal file
14
src/utils/params.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {getObjDetails} from '/@/api/admin/param';
|
||||
|
||||
/**
|
||||
* 后端参数获取
|
||||
*/
|
||||
const params = {
|
||||
async get(key: string) {
|
||||
const result = await getObjDetails({publicKey: key})
|
||||
return result.data;
|
||||
},
|
||||
}
|
||||
|
||||
// 统一批量导出
|
||||
export default params;
|
||||
138
src/utils/request.ts
Normal file
138
src/utils/request.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import axios, { AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Session } from '/@/utils/storage';
|
||||
import { useMessageBox } from '/@/hooks/message';
|
||||
import qs from 'qs';
|
||||
import other from './other';
|
||||
import {paramsFilter} from "/@/flow";
|
||||
import { wrapEncryption, encryptRequestParams, decrypt } from './apiCrypto';
|
||||
|
||||
// 常用header
|
||||
export enum CommonHeaderEnum {
|
||||
'TENANT_ID' = 'TENANT-ID',
|
||||
'ENC_FLAG' = 'Enc-Flag',
|
||||
'AUTHORIZATION' = 'Authorization',
|
||||
'VERSION' = 'VERSION',
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并配置一个 Axios 实例对象
|
||||
*/
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
timeout: 50000, // 全局超时时间
|
||||
paramsSerializer: {
|
||||
serialize: (params: any) => {
|
||||
return qs.stringify(paramsFilter(params), { arrayFormat: 'repeat' });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Axios请求拦截器,对请求进行处理
|
||||
* 1. 序列化get请求参数
|
||||
* 2. 统一增加Authorization和TENANT-ID请求头
|
||||
* 3. 自动适配单体、微服务架构不同的URL
|
||||
* @param config AxiosRequestConfig对象,包含请求配置信息
|
||||
*/
|
||||
service.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
|
||||
config.headers['VERSION'] = 'A' // 目标版本
|
||||
|
||||
// 统一增加Authorization请求头, skipToken 跳过增加token
|
||||
const token = Session.getToken();
|
||||
if (token && !config.headers?.skipToken) {
|
||||
config.headers![CommonHeaderEnum.AUTHORIZATION] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 统一增加TENANT-ID请求头
|
||||
const tenantId = Session.getTenant();
|
||||
if (tenantId) {
|
||||
config.headers![CommonHeaderEnum.TENANT_ID] = tenantId;
|
||||
}
|
||||
|
||||
// 增加 gray_version 请求头
|
||||
const version = import.meta.env.VITE_GRAY_VERSION;
|
||||
if (version) {
|
||||
config.headers![CommonHeaderEnum.VERSION] = version;
|
||||
}
|
||||
|
||||
// 请求报文加密 ,如果请求头中不包含 ENC_FLAG : false 则加密
|
||||
if (config.data && !config.headers![CommonHeaderEnum.ENC_FLAG]) {
|
||||
config.data = wrapEncryption(config.data);
|
||||
}
|
||||
|
||||
// 如果是 GET ,加密 config.param 的每一个参数,并URLencode
|
||||
if (config.method === 'get' && config.params) {
|
||||
config.params = encryptRequestParams(config.params);
|
||||
}
|
||||
|
||||
// 自动适配单体和微服务架构不同的URL
|
||||
config.url = other.adaptationUrl(config.url);
|
||||
|
||||
// 处理完毕,返回config对象
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误进行处理
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 响应拦截器处理函数
|
||||
* @param response 响应结果
|
||||
* @returns 如果响应成功,则返回响应的data属性;否则,抛出错误或者执行其他操作
|
||||
*/
|
||||
const handleResponse = (response: AxiosResponse<any>) => {
|
||||
if (response.data.code === 1) {
|
||||
throw response.data;
|
||||
}
|
||||
|
||||
// 针对密文返回解密
|
||||
if (response.data.encryption) {
|
||||
response.data = decrypt(response.data.encryption);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加 Axios 的响应拦截器,用于全局响应结果处理
|
||||
*/
|
||||
service.interceptors.response.use(handleResponse, (error) => {
|
||||
const status = Number(error.response.status) || 200;
|
||||
if (status === 423) {
|
||||
return Promise.reject({ msg: '"演示环境,仅供预览"' });
|
||||
}
|
||||
|
||||
if (status === 424) {
|
||||
useMessageBox()
|
||||
.confirm('令牌状态已过期,请点击重新登录')
|
||||
.then(() => {
|
||||
Session.clear(); // 清除浏览器全部临时缓存
|
||||
window.location.href = '/'; // 去登录页
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
if (status === 426) {
|
||||
useMessageBox()
|
||||
.confirm('租户状态已过期,请联系管理员')
|
||||
.then(() => {
|
||||
Session.clear(); // 清除浏览器全部临时缓存
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
// 针对密文返回解密
|
||||
if (error.response?.data?.encryption) {
|
||||
error.response.data = decrypt(error.response?.data.encryption);
|
||||
}
|
||||
|
||||
return Promise.reject(error.response.data);
|
||||
});
|
||||
|
||||
// 导出 axios 实例
|
||||
export default service;
|
||||
48
src/utils/setIconfont.ts
Normal file
48
src/utils/setIconfont.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// 字体图标 url
|
||||
const cssCdnUrlList: Array<string> = [
|
||||
'//at.alicdn.com/t/c/font_4818804_1phbmp8acde.css', //
|
||||
'/assets/styles/font-awesome.min.css',
|
||||
];
|
||||
// 第三方 js url
|
||||
const jsCdnUrlList: Array<string> = [];
|
||||
|
||||
// 动态批量设置字体图标
|
||||
export function setCssCdn() {
|
||||
if (cssCdnUrlList.length <= 0) return false;
|
||||
cssCdnUrlList.map((v) => {
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = v;
|
||||
link.crossOrigin = 'anonymous';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
// 动态批量设置第三方js
|
||||
export function setJsCdn() {
|
||||
if (jsCdnUrlList.length <= 0) return false;
|
||||
jsCdnUrlList.map((v) => {
|
||||
let link = document.createElement('script');
|
||||
link.src = v;
|
||||
document.body.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量设置字体图标、动态js
|
||||
* @method cssCdn 动态批量设置字体图标
|
||||
* @method jsCdn 动态批量设置第三方js
|
||||
*/
|
||||
const setIntroduction = {
|
||||
// 设置css
|
||||
cssCdn: () => {
|
||||
setCssCdn();
|
||||
},
|
||||
// 设置js
|
||||
jsCdn: () => {
|
||||
setJsCdn();
|
||||
},
|
||||
};
|
||||
|
||||
// 导出函数方法
|
||||
export default setIntroduction;
|
||||
83
src/utils/storage.ts
Normal file
83
src/utils/storage.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
/**
|
||||
* window.localStorage 浏览器永久缓存
|
||||
* @method set 设置永久缓存
|
||||
* @method get 获取永久缓存
|
||||
* @method remove 移除永久缓存
|
||||
* @method clear 移除全部永久缓存
|
||||
*/
|
||||
export const Local = {
|
||||
// 查看 v2.4.3版本更新日志
|
||||
setKey(key: string) {
|
||||
// @ts-ignore
|
||||
if (key.startsWith(`${__NEXT_NAME__}:`)) {
|
||||
return key;
|
||||
}
|
||||
return `${__NEXT_NAME__}:${key}`;
|
||||
},
|
||||
// 设置永久缓存
|
||||
set<T>(key: string, val: T) {
|
||||
window.localStorage.setItem(Local.setKey(key), JSON.stringify(val));
|
||||
},
|
||||
// 获取永久缓存
|
||||
get(key: string) {
|
||||
let json = <string>window.localStorage.getItem(Local.setKey(key));
|
||||
return JSON.parse(json);
|
||||
},
|
||||
// 移除永久缓存
|
||||
remove(key: string) {
|
||||
window.localStorage.removeItem(Local.setKey(key));
|
||||
},
|
||||
// 移除全部永久缓存
|
||||
clear() {
|
||||
window.localStorage.clear();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* window.sessionStorage 浏览器临时缓存
|
||||
* @method set 设置临时缓存
|
||||
* @method get 获取临时缓存
|
||||
* @method remove 移除临时缓存
|
||||
* @method clear 移除全部临时缓存
|
||||
*/
|
||||
export const Session = {
|
||||
// 设置临时缓存
|
||||
set(key: string, val: any) {
|
||||
if (key === 'token' || key === 'refresh_token' || key === 'tenantId') {
|
||||
Cookies.set(key, val);
|
||||
}
|
||||
window.sessionStorage.setItem(key, JSON.stringify(val));
|
||||
},
|
||||
// 获取临时缓存
|
||||
get(key: string) {
|
||||
if (key === 'token' || key === 'refresh_token' || key === 'tenantId') {
|
||||
// 兼容移动端
|
||||
let token = Cookies.get(key);
|
||||
if (token) return token;
|
||||
}
|
||||
let json = <string>window.sessionStorage.getItem(key);
|
||||
return JSON.parse(json);
|
||||
},
|
||||
// 移除临时缓存
|
||||
remove(key: string) {
|
||||
if (key === 'token' || key === 'refresh_token' || key === 'tenantId') Cookies.remove(key);
|
||||
window.sessionStorage.removeItem(key);
|
||||
},
|
||||
// 移除全部临时缓存
|
||||
clear() {
|
||||
Cookies.remove('token');
|
||||
Cookies.remove('refresh_token');
|
||||
Cookies.remove('tenantId');
|
||||
window.sessionStorage.clear();
|
||||
},
|
||||
// 获取当前存储的 token
|
||||
getToken() {
|
||||
return this.get('token');
|
||||
},
|
||||
// 获取当前的租户
|
||||
getTenant() {
|
||||
return Local.get('tenantId') ? Local.get('tenantId') : 1;
|
||||
},
|
||||
};
|
||||
63
src/utils/theme.ts
Normal file
63
src/utils/theme.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 颜色转换函数
|
||||
* @method hexToRgb hex 颜色转 rgb 颜色
|
||||
* @method rgbToHex rgb 颜色转 Hex 颜色
|
||||
* @method getDarkColor 加深颜色值
|
||||
* @method getLightColor 变浅颜色值
|
||||
*/
|
||||
export function useChangeColor() {
|
||||
// str 颜色值字符串
|
||||
const hexToRgb = (str: string): any => {
|
||||
let hexs: any = '';
|
||||
let reg = /^\#?[0-9A-Fa-f]{6}$/;
|
||||
if (!reg.test(str)) {
|
||||
ElMessage.warning('输入错误的hex');
|
||||
return '';
|
||||
}
|
||||
str = str.replace('#', '');
|
||||
hexs = str.match(/../g);
|
||||
for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);
|
||||
return hexs;
|
||||
};
|
||||
// r 代表红色 | g 代表绿色 | b 代表蓝色
|
||||
const rgbToHex = (r: any, g: any, b: any): string => {
|
||||
let reg = /^\d{1,3}$/;
|
||||
if (!reg.test(r) || !reg.test(g) || !reg.test(b)) {
|
||||
ElMessage.warning('输入错误的rgb颜色值');
|
||||
return '';
|
||||
}
|
||||
let hexs = [r.toString(16), g.toString(16), b.toString(16)];
|
||||
for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;
|
||||
return `#${hexs.join('')}`;
|
||||
};
|
||||
// color 颜色值字符串 | level 变浅的程度,限0-1之间
|
||||
const getDarkColor = (color: string, level: number): string => {
|
||||
let reg = /^\#?[0-9A-Fa-f]{6}$/;
|
||||
if (!reg.test(color)) {
|
||||
ElMessage.warning('输入错误的hex颜色值');
|
||||
return '';
|
||||
}
|
||||
let rgb = useChangeColor().hexToRgb(color);
|
||||
for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));
|
||||
return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]);
|
||||
};
|
||||
// color 颜色值字符串 | level 加深的程度,限0-1之间
|
||||
const getLightColor = (color: string, level: number): string => {
|
||||
let reg = /^\#?[0-9A-Fa-f]{6}$/;
|
||||
if (!reg.test(color)) {
|
||||
ElMessage.warning('输入错误的hex颜色值');
|
||||
return '';
|
||||
}
|
||||
let rgb = useChangeColor().hexToRgb(color);
|
||||
for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);
|
||||
return useChangeColor().rgbToHex(rgb[0], rgb[1], rgb[2]);
|
||||
};
|
||||
return {
|
||||
hexToRgb,
|
||||
rgbToHex,
|
||||
getDarkColor,
|
||||
getLightColor,
|
||||
};
|
||||
}
|
||||
370
src/utils/toolsValidate.ts
Normal file
370
src/utils/toolsValidate.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* 2020.11.29 lyt 整理
|
||||
* 工具类集合,适用于平时开发
|
||||
* 新增多行注释信息,鼠标放到方法名即可查看
|
||||
*/
|
||||
|
||||
/**
|
||||
* 验证百分比(不可以小数)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyNumberPercentage(val: string): string {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 只能是数字和小数点,不能是其他输入
|
||||
v = v.replace(/[^\d]/g, '');
|
||||
// 不能以0开始
|
||||
v = v.replace(/^0/g, '');
|
||||
// 数字超过100,赋值成最大值100
|
||||
v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证百分比(可以小数)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyNumberPercentageFloat(val: string): string {
|
||||
let v = verifyNumberIntegerAndFloat(val);
|
||||
// 数字超过100,赋值成最大值100
|
||||
v = v.replace(/^[1-9]\d\d{1,3}$/, '100');
|
||||
// 超过100之后不给再输入值
|
||||
v = v.replace(/^100\.$/, '100');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小数或整数(不可以负数)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyNumberIntegerAndFloat(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 只能是数字和小数点,不能是其他输入
|
||||
v = v.replace(/[^\d.]/g, '');
|
||||
// 以0开始只能输入一个
|
||||
v = v.replace(/^0{2}$/g, '0');
|
||||
// 保证第一位只能是数字,不能是点
|
||||
v = v.replace(/^\./g, '');
|
||||
// 小数只能出现1位
|
||||
v = v.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
|
||||
// 小数点后面保留2位
|
||||
v = v.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 正整数验证
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifiyNumberInteger(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 去掉 '.' , 防止贴贴的时候出现问题 如 0.1.12.12
|
||||
v = v.replace(/[\.]*/g, '');
|
||||
// 去掉以 0 开始后面的数, 防止贴贴的时候出现问题 如 00121323
|
||||
v = v.replace(/(^0[\d]*)$/g, '0');
|
||||
// 首位是0,只能出现一次
|
||||
v = v.replace(/^0\d$/g, '0');
|
||||
// 只匹配数字
|
||||
v = v.replace(/[^\d]/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去掉中文及空格
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyCnAndSpace(val: string) {
|
||||
// 匹配中文与空格
|
||||
let v = val.replace(/[\u4e00-\u9fa5\s]+/g, '');
|
||||
// 匹配空格
|
||||
v = v.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去掉英文及空格
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyEnAndSpace(val: string) {
|
||||
// 匹配英文与空格
|
||||
let v = val.replace(/[a-zA-Z]+/g, '');
|
||||
// 匹配空格
|
||||
v = v.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止输入空格
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyAndSpace(val: string) {
|
||||
// 匹配空格
|
||||
let v = val.replace(/(^\s*)|(\s*$)/g, '');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 金额用 `,` 区分开
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyNumberComma(val: string) {
|
||||
// 调用小数或整数(不可以负数)方法
|
||||
let v: any = verifyNumberIntegerAndFloat(val);
|
||||
// 字符串转成数组
|
||||
v = v.toString().split('.');
|
||||
// \B 匹配非单词边界,两边都是单词字符或者两边都是非单词字符
|
||||
v[0] = v[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
// 数组转字符串
|
||||
v = v.join('.');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配文字变色(搜索时)
|
||||
* @param val 当前值字符串
|
||||
* @param text 要处理的字符串值
|
||||
* @param color 搜索到时字体高亮颜色
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyTextColor(val: string, text = '', color = 'red') {
|
||||
// 返回内容,添加颜色
|
||||
let v = text.replace(new RegExp(val, 'gi'), `<span style='color: ${color}'>${val}</span>`);
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数字转中文大写
|
||||
* @param val 当前值字符串
|
||||
* @param unit 默认:仟佰拾亿仟佰拾万仟佰拾元角分
|
||||
* @returns 返回处理后的字符串
|
||||
*/
|
||||
export function verifyNumberCnUppercase(val: any, unit = '仟佰拾亿仟佰拾万仟佰拾元角分', v = '') {
|
||||
// 当前内容字符串添加 2个0,为什么??
|
||||
val += '00';
|
||||
// 返回某个指定的字符串值在字符串中首次出现的位置,没有出现,则该方法返回 -1
|
||||
let lookup = val.indexOf('.');
|
||||
// substring:不包含结束下标内容,substr:包含结束下标内容
|
||||
if (lookup >= 0) val = val.substring(0, lookup) + val.substr(lookup + 1, 2);
|
||||
// 根据内容 val 的长度,截取返回对应大写
|
||||
unit = unit.substr(unit.length - val.length);
|
||||
// 循环截取拼接大写
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
v += '零壹贰叁肆伍陆柒捌玖'.substr(val.substr(i, 1), 1) + unit.substr(i, 1);
|
||||
}
|
||||
// 正则处理
|
||||
v = v
|
||||
.replace(/零角零分$/, '整')
|
||||
.replace(/零[仟佰拾]/g, '零')
|
||||
.replace(/零{2,}/g, '零')
|
||||
.replace(/零([亿|万])/g, '$1')
|
||||
.replace(/零+元/, '元')
|
||||
.replace(/亿零{0,3}万/, '亿')
|
||||
.replace(/^元/, '零元');
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号码
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 手机号码正确
|
||||
*/
|
||||
export function verifyPhone(val: string) {
|
||||
// false: 手机号码不正确
|
||||
if (!/^((12[0-9])|(13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(18[0|1,5-9]))\d{8}$/.test(val)) return false;
|
||||
// true: 手机号码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国内电话号码
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 国内电话号码正确
|
||||
*/
|
||||
export function verifyTelPhone(val: string) {
|
||||
// false: 国内电话号码不正确
|
||||
if (!/\d{3}-\d{8}|\d{4}-\d{7}/.test(val)) return false;
|
||||
// true: 国内电话号码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录账号 (字母开头,允许5-16字节,允许字母数字下划线)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 登录账号正确
|
||||
*/
|
||||
export function verifyAccount(val: string) {
|
||||
// false: 登录账号不正确
|
||||
if (!/^[a-zA-Z][a-zA-Z0-9_]{4,15}$/.test(val)) return false;
|
||||
// true: 登录账号正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码 (以字母开头,长度在6~16之间,只能包含字母、数字和下划线)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 密码正确
|
||||
*/
|
||||
export function verifyPassword(val: string) {
|
||||
// false: 密码不正确
|
||||
if (!/^[a-zA-Z]\w{5,15}$/.test(val)) return false;
|
||||
// true: 密码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强密码 (字母+数字+特殊字符,长度在6-16之间)
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 强密码正确
|
||||
*/
|
||||
export function verifyPasswordPowerful(val: string) {
|
||||
// false: 强密码不正确
|
||||
if (!/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,16}$/.test(val))
|
||||
return false;
|
||||
// true: 强密码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码强度
|
||||
* @param val 当前值字符串
|
||||
* @description 弱:纯数字,纯字母,纯特殊字符
|
||||
* @description 中:字母+数字,字母+特殊字符,数字+特殊字符
|
||||
* @description 强:字母+数字+特殊字符
|
||||
* @returns 返回处理后的字符串:弱、中、强
|
||||
*/
|
||||
export function verifyPasswordStrength(val: string) {
|
||||
let v = '0';
|
||||
// 弱:纯数字,纯字母,纯特殊字符
|
||||
if (/^(?:\d+|[a-zA-Z]+|[!@#$%^&\.*]+){6,100}$/.test(val)) v = '1';
|
||||
// 中:字母+数字,字母+特殊字符,数字+特殊字符
|
||||
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,100}$/.test(val)) v = '2';
|
||||
// 强:字母+数字+特殊字符
|
||||
if (/^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&\.*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&\.*]+$)(?![\d!@#$%^&\.*]+$)[a-zA-Z\d!@#$%^&\.*]{6,100}$/.test(val))
|
||||
v = '3';
|
||||
// 返回结果
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* IP地址
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: IP地址正确
|
||||
*/
|
||||
export function verifyIPAddress(val: string) {
|
||||
// false: IP地址不正确
|
||||
if (
|
||||
!/^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true: IP地址正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 邮箱正确
|
||||
*/
|
||||
export function verifyEmail(val: string) {
|
||||
// false: 邮箱不正确
|
||||
if (
|
||||
!/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true: 邮箱正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 身份证
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 身份证正确
|
||||
*/
|
||||
export function verifyIdCard(val: string) {
|
||||
// false: 身份证不正确
|
||||
if (!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(val)) return false;
|
||||
// true: 身份证正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 姓名正确
|
||||
*/
|
||||
export function verifyFullName(val: string) {
|
||||
// false: 姓名不正确
|
||||
if (!/^[\u4e00-\u9fa5]{1,6}(·[\u4e00-\u9fa5]{1,6}){0,2}$/.test(val)) return false;
|
||||
// true: 姓名正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮政编码
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: 邮政编码正确
|
||||
*/
|
||||
export function verifyPostalCode(val: string) {
|
||||
// false: 邮政编码不正确
|
||||
if (!/^[1-9][0-9]{5}$/.test(val)) return false;
|
||||
// true: 邮政编码正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* url 处理
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true: url 正确
|
||||
*/
|
||||
export function verifyUrl(val: string) {
|
||||
// false: url不正确
|
||||
if (
|
||||
!/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true: url正确
|
||||
else return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 车牌号
|
||||
* @param val 当前值字符串
|
||||
* @returns 返回 true:车牌号正确
|
||||
*/
|
||||
export function verifyCarNum(val: string) {
|
||||
// false: 车牌号不正确
|
||||
if (
|
||||
!/^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[DF])|([DF]([A-HJ-NP-Z0-9])[0-9]{4})))|([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$/.test(
|
||||
val
|
||||
)
|
||||
)
|
||||
return false;
|
||||
// true:车牌号正确
|
||||
else return true;
|
||||
}
|
||||
234
src/utils/validate.ts
Normal file
234
src/utils/validate.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* 判断是否为空
|
||||
* @param val 数据
|
||||
*/
|
||||
export const validateNull = (val: any) => {
|
||||
if (typeof val === 'boolean') {
|
||||
return false;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
return false;
|
||||
}
|
||||
if (val instanceof Array) {
|
||||
if (val.length === 0) return true;
|
||||
} else if (val instanceof Object) {
|
||||
if (JSON.stringify(val) === '{}') return true;
|
||||
} else {
|
||||
if (val === 'null' || val == null || val === 'undefined' || val === undefined || val === '') return true;
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const rule = {
|
||||
/**
|
||||
* 校验用户输入的长度避免超长
|
||||
* 0-255个字符
|
||||
* 超长
|
||||
*/
|
||||
overLength(rule: any, value: any, callback: any) {
|
||||
if (value?.length > 255) {
|
||||
callback(new Error('输入内容过长,请重新输入'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 校验 请输入中文、英文、数字包括下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorNameCn(rule: any, value: any, callback: any) {
|
||||
const acount = /^[\u4E00-\u9FA5A-Za-z0-9_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入中文、英文、数字包括下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 校验 请输入大写英文、下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorCapital(rule: any, value: any, callback: any) {
|
||||
const acount = /^[A-Z_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入大写英文、下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验 请输入小写英文、下划线
|
||||
* 名称校验
|
||||
*/
|
||||
validatorLowercase(rule: any, value: any, callback: any) {
|
||||
const acount = /^[a-z_]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入小写英文、下划线'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验 请输入小写英文
|
||||
* 名称校验
|
||||
*/
|
||||
validatorLower(rule: any, value: any, callback: any) {
|
||||
const acount = /^[a-z]+$/;
|
||||
if (value && !acount.test(value)) {
|
||||
callback(new Error('请输入小写英文'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验首尾空白字符的正则表达式
|
||||
*
|
||||
*/
|
||||
checkSpace(rule: any, value: any, callback: any) {
|
||||
const longrg = /[^\s]+$/;
|
||||
if (!longrg.test(value)) {
|
||||
callback(new Error('请输入非空格信息'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 校验手机号
|
||||
*/
|
||||
validatePhone(rule: any, value: any, callback: any) {
|
||||
var isPhone = /^1(3\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\d|9[0-35-9])\d{8}$/;
|
||||
|
||||
if (value.indexOf('****') >= 0) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!isPhone.test(value)) {
|
||||
callback(new Error('请输入合法手机号'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
/* 数字 */
|
||||
number(rule, value, callback) {
|
||||
validateFn('number', rule, value, callback, '包含非数字字符');
|
||||
},
|
||||
|
||||
/* 字母 */
|
||||
letter(rule, value, callback) {
|
||||
validateFn('letter', rule, value, callback, '包含非字母字符');
|
||||
},
|
||||
|
||||
/* 字母和数字 */
|
||||
letterAndNumber(rule, value, callback) {
|
||||
validateFn('letterAndNumber', rule, value, callback, '只能输入字母或数字');
|
||||
},
|
||||
|
||||
/* 手机号码 */
|
||||
mobilePhone(rule, value, callback) {
|
||||
validateFn('mobilePhone', rule, value, callback, '手机号码格式有误');
|
||||
},
|
||||
|
||||
/* 字母开头,仅可包含数字 */
|
||||
letterStartNumberIncluded(rule, value, callback) {
|
||||
validateFn('letterStartNumberIncluded', rule, value, callback, '必须以字母开头,可包含数字');
|
||||
},
|
||||
|
||||
/* 禁止中文输入 */
|
||||
noChinese(rule, value, callback) {
|
||||
validateFn('noChinese', rule, value, callback, '不可输入中文字符');
|
||||
},
|
||||
|
||||
/* 必须中文输入 */
|
||||
chinese(rule, value, callback) {
|
||||
validateFn('chinese', rule, value, callback, '只能输入中文字符');
|
||||
},
|
||||
|
||||
/* 电子邮箱 */
|
||||
email(rule, value, callback) {
|
||||
validateFn('email', rule, value, callback, '邮箱格式有误');
|
||||
},
|
||||
|
||||
/* URL网址 */
|
||||
url(rule, value, callback) {
|
||||
validateFn('url', rule, value, callback, 'URL格式有误');
|
||||
},
|
||||
|
||||
/* json 格式 */
|
||||
json(rule, value, callback) {
|
||||
if (validateNull(value) || value.length <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(value);
|
||||
callback();
|
||||
} catch (error) {
|
||||
callback(new Error('json 格式有误'));
|
||||
}
|
||||
},
|
||||
|
||||
regExp(rule, value, callback) {
|
||||
if (validateNull(value) || value.length <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const pattern = new RegExp(rule.regExp);
|
||||
|
||||
if (!pattern.test(value)) {
|
||||
const errTxt = rule.errorMsg || 'invalid value';
|
||||
callback(new Error(errTxt));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @desc [自定义校验规则]
|
||||
* @example
|
||||
* import { validateRule } from "@/utils/validateRules";
|
||||
* rules: [
|
||||
* { validator: validateRule.emailValue, trigger: 'blur'}
|
||||
* ]
|
||||
*/
|
||||
|
||||
export const getRegExp = function (validatorName) {
|
||||
const commonRegExp = {
|
||||
number: '^[-]?\\d+(\\.\\d+)?$',
|
||||
letter: '^[A-Za-z]+$',
|
||||
letterAndNumber: '^[A-Za-z0-9]+$',
|
||||
mobilePhone: '^[1][3-9][0-9]{9}$',
|
||||
letterStartNumberIncluded: '^[A-Za-z]+[A-Za-z\\d]*$',
|
||||
noChinese: '^[^\u4e00-\u9fa5]+$',
|
||||
chinese: '^[\u4e00-\u9fa5]+$',
|
||||
email: '^([-_A-Za-z0-9.]+)@([_A-Za-z0-9]+\\.)+[A-Za-z0-9]{2,3}$',
|
||||
url: '(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]'
|
||||
};
|
||||
return commonRegExp[validatorName];
|
||||
};
|
||||
|
||||
const validateFn = (validatorName, rule, value, callback, defaultErrorMsg) => {
|
||||
if (validateNull(value) || value.length <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const reg = new RegExp(getRegExp(validatorName));
|
||||
|
||||
if (!reg.test(value)) {
|
||||
const errTxt = rule.errorMsg || defaultErrorMsg;
|
||||
callback(new Error(errTxt));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
47
src/utils/wartermark.ts
Normal file
47
src/utils/wartermark.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 页面添加水印效果
|
||||
const setWatermark = (str: string) => {
|
||||
const id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null) document.body.removeChild(<HTMLElement>document.getElementById(id));
|
||||
const can = document.createElement('canvas');
|
||||
can.width = 200;
|
||||
can.height = 130;
|
||||
const cans = <CanvasRenderingContext2D>can.getContext('2d');
|
||||
cans.rotate((-20 * Math.PI) / 180);
|
||||
cans.font = '12px Vedana';
|
||||
cans.fillStyle = 'rgba(200, 200, 200, 0.30)';
|
||||
cans.textBaseline = 'middle';
|
||||
cans.fillText(str, can.width / 10, can.height / 2);
|
||||
const div = document.createElement('div');
|
||||
div.id = id;
|
||||
div.style.pointerEvents = 'none';
|
||||
div.style.top = '0px';
|
||||
div.style.left = '0px';
|
||||
div.style.position = 'fixed';
|
||||
div.style.zIndex = '10000000';
|
||||
div.style.width = `${document.documentElement.clientWidth}px`;
|
||||
div.style.height = `${document.documentElement.clientHeight}px`;
|
||||
div.style.background = `url(${can.toDataURL('image/png')}) left top repeat`;
|
||||
document.body.appendChild(div);
|
||||
return id;
|
||||
};
|
||||
|
||||
/**
|
||||
* 页面添加水印效果
|
||||
* @method set 设置水印
|
||||
* @method del 删除水印
|
||||
*/
|
||||
const watermark = {
|
||||
// 设置水印
|
||||
set: (str: string) => {
|
||||
let id = setWatermark(str);
|
||||
if (document.getElementById(id) === null) id = setWatermark(str);
|
||||
},
|
||||
// 删除水印
|
||||
del: () => {
|
||||
let id = '1.23452384164.123412416';
|
||||
if (document.getElementById(id) !== null) document.body.removeChild(<HTMLElement>document.getElementById(id));
|
||||
},
|
||||
};
|
||||
|
||||
// 导出方法
|
||||
export default watermark;
|
||||
Reference in New Issue
Block a user