464 lines
13 KiB
TypeScript
464 lines
13 KiB
TypeScript
/* 工具函数 */
|
||
import router from '@/router';
|
||
import * as CryptoJS from 'crypto-js';
|
||
import pick from 'lodash/pick';
|
||
import pickBy from 'lodash/pickBy';
|
||
import type { createIssueReqType } from '@/api/issue/types';
|
||
import type { RepoItemResData } from '@/utils/types';
|
||
import DOMPurify from 'dompurify';
|
||
import { Message } from 'vue-devui/message';
|
||
import qs from 'qs';
|
||
/**
|
||
* 返回 axios 的 data, 或者 obj 最内层的 data
|
||
*/
|
||
export const escapeResData = (obj: object) => {
|
||
if (!obj) return obj;
|
||
if (!Object.prototype.hasOwnProperty.call(obj, 'data')) return obj;
|
||
if (obj?.headers?.constructor?.name === 'AxiosHeaders') { return obj?.data; };
|
||
return escapeResData(obj?.data);
|
||
};
|
||
|
||
export function getRepoName(): string {
|
||
//
|
||
return router.currentRoute.value.params.repoName;
|
||
};
|
||
|
||
/**
|
||
* Parse the time to string
|
||
* @param {(Object|string|number)} time
|
||
* @param {string} cFormat
|
||
* @returns {string | null}
|
||
*/
|
||
function parseTime(time: string | number | Date, cFormat?: string) {
|
||
if (!time) {
|
||
return null;
|
||
}
|
||
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}';
|
||
let date;
|
||
if (typeof time === 'object') {
|
||
date = time;
|
||
} else {
|
||
if ((typeof time === 'string')) {
|
||
if ((/^[0-9]+$/.test(time))) {
|
||
time = parseInt(time);
|
||
} else {
|
||
// support safari
|
||
time = time.replace(new RegExp(/-/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(/{([ymdhisa])+}/g, (result, key) => {
|
||
const value = formatObj[key];
|
||
// Note: getDay() returns 0 on Sunday
|
||
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value]; }
|
||
return value.toString().padStart(2, '0');
|
||
});
|
||
return time_str;
|
||
}
|
||
|
||
export function fileToBlob(file: any) {
|
||
// 创建 FileReader 对象
|
||
const reader = new FileReader();
|
||
return new Promise(resolve => {
|
||
// FileReader 添加 load 事件
|
||
reader.addEventListener('load', (e) => {
|
||
let blob;
|
||
if (typeof e.target.result === 'object') {
|
||
blob = new Blob([e.target.result]);
|
||
} else {
|
||
blob = e.target.result;
|
||
}
|
||
|
||
resolve(blob);
|
||
});
|
||
// FileReader 以 ArrayBuffer 格式 读取 File 对象中数据
|
||
reader.readAsArrayBuffer(file);
|
||
});
|
||
}
|
||
|
||
// 图片转 base64
|
||
export function imageToBase64(file: File) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.readAsDataURL(file);
|
||
reader.onload = () => resolve(reader.result);
|
||
reader.onerror = error => reject(error);
|
||
});
|
||
}
|
||
|
||
export function generateHash(userName: string, salt: string): string {
|
||
const hashBytes = CryptoJS.SHA256(userName + salt);
|
||
return hashBytes.toString(CryptoJS.enc.Hex);
|
||
}
|
||
|
||
export function getFilePath(hash: string, filetype: string): string {
|
||
const chars: string[] = hash.split('');
|
||
const sb: string[] = [];
|
||
for (const c of chars) {
|
||
if (isNaN(Number(c))) {
|
||
sb.push(c);
|
||
}
|
||
}
|
||
return `${sb.slice(0, 2).join('')}/${sb.slice(2, 4).join('')}/${hash}.${filetype}`;
|
||
}
|
||
|
||
export function getImageUrl(iam_id?: string, fileType: string = 'png', isAvatar: boolean = true) {
|
||
if (!iam_id) {
|
||
const accountInfo = JSON.parse(localStorage.getItem('opUserInfo') || '{}');
|
||
iam_id = accountInfo.iam_id;
|
||
}
|
||
if (isAvatar) {
|
||
const host = 'https://gitcode-img.obs.cn-south-1.myhuaweicloud.com:443/';
|
||
const salt = 'avatar';
|
||
const hash = generateHash(iam_id || '', salt);
|
||
const path = getFilePath(hash, fileType || 'png');
|
||
return host + path + '?time=' + new Date().getTime();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理issue put 数据
|
||
*/
|
||
export function formatIssuePutData(data: createIssueReqType) {
|
||
const params = pick(data, ['title', 'description', 'confidential', 'discussions', 'assignee', 'assignee_id', 'assignee_ids', 'project_id', 'issue_iid', 'labels', 'milestone_id', 'discussion_locked', 'state_event']);
|
||
let { labels } = params;
|
||
if (typeof labels === 'object') {
|
||
labels = labels.map(item => {
|
||
if (typeof item === 'string') {
|
||
return item;
|
||
} else if (typeof item.name === 'string') {
|
||
return item.name;
|
||
} else {
|
||
return null;
|
||
}
|
||
});
|
||
}
|
||
|
||
return ({
|
||
...params,
|
||
labels,
|
||
issue_category: '-',
|
||
issue_stage: '-',
|
||
issue_severity: '-'
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 提取展示的姓名
|
||
*/
|
||
export const pickNickName = (author?: any): string => {
|
||
return author?.nick_name || author?.nickname || author?.name_cn || author?.name || author?.username || '';
|
||
};
|
||
|
||
/**
|
||
* 去除对象中的空值
|
||
*/
|
||
export function removeEmptyValue(params: object): object {
|
||
return pickBy(params, (e) => (e !== undefined && e !== '' && e !== null));
|
||
}
|
||
|
||
/**
|
||
* 替换对象中的null为 undefined
|
||
*/
|
||
export function replaceNull(obj: {[name:string]: any}): any {
|
||
for (const k in obj) {
|
||
if (obj[k] === null) {
|
||
obj[k] = undefined;
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
|
||
/**
|
||
* 处理多选数据 1.空值 清空select 2.重复项取消 3.未选中的添加
|
||
*/
|
||
export function formatSelectedData(current: string | object | undefined | null, selectedList: any[], judge: (a: { [name: string]: unknown }, b: { [name: string]: unknown } | unknown) => boolean): object[] {
|
||
if (!current) return [];
|
||
const index = selectedList.findIndex((item) => {
|
||
if (typeof item === 'string') {
|
||
return item === current;
|
||
} else {
|
||
return judge && judge(item, current);
|
||
}
|
||
});
|
||
if (index > -1) {
|
||
selectedList.splice(index, 1);
|
||
} else {
|
||
selectedList.push(current);
|
||
}
|
||
return selectedList;
|
||
}
|
||
|
||
/**
|
||
* 处理引用回复文本
|
||
*/
|
||
export const formatQuoteReply = (str: string) => {
|
||
str = str.trim();
|
||
if (!str) return '';
|
||
str = str.split('\n').map(e => '>' + e).join('\n') + '\n\n';
|
||
return str;
|
||
};
|
||
|
||
/**
|
||
* 判断父级元素有没有类名
|
||
*/
|
||
export function hasClassInParent(target: Element, classNameList: string[]): boolean {
|
||
if (target?.parentElement) {
|
||
if (classNameList.every(name => target?.classList?.contains(name))) {
|
||
return true;
|
||
} else {
|
||
return hasClassInParent(target?.parentElement, classNameList);
|
||
}
|
||
} else {
|
||
return target ? classNameList.every(name => target?.classList?.contains(name)) : false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 优先展示已经选择的项,已勾选的排在前面
|
||
*/
|
||
export const sortList = (list: Object[] = [], select: Object[] = []):Object[] => {
|
||
list = list.slice(0);
|
||
select = select.slice(0);
|
||
if (select[0]) {
|
||
const sList:any[] = [];
|
||
for (let i = 0; i < select.length; i++) {
|
||
const index = list.findIndex(e => e.value === select[i]?.value);
|
||
if (index > -1) {
|
||
const one = list.splice(index, 1)[0];
|
||
sList.push(one);
|
||
}
|
||
}
|
||
return sList.concat(list);
|
||
} else {
|
||
return list;
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理repo数据便于使用repoItem组件
|
||
*/
|
||
export const repoDataHandler = (data: RepoItemResData[]) => {
|
||
return data.filter((item) => {
|
||
return !!item && (item.id || item.resource_id);
|
||
}).map((item) => {
|
||
const langs = (item.main_repository_language || []).filter((lang) => {
|
||
return !!lang;
|
||
});
|
||
return {
|
||
id: item.id || item.resource_id || '',
|
||
imgSrc: '',
|
||
title: item.name || '-',
|
||
desc: item.description || '-',
|
||
isStar: item.starred || false,
|
||
tag: item.visibility || '',
|
||
to: `/${item.namespace}`,
|
||
web_url: item.web_url,
|
||
iconHandleList: [
|
||
{ icon: 'icon-dot-status', value: langs.join(',') || '-', type: 'language', iconColor: 'red', label: '', to: '' },
|
||
{ icon: 'gt-star', value: item.star_count, label: '', to: '' },
|
||
{ icon: 'gt-fork', value: item.forks_count, label: '', to: '' },
|
||
{ icon: 'gt-date', value: item.last_activity_at, label: '', to: '' }
|
||
]
|
||
};
|
||
});
|
||
};
|
||
|
||
/**
|
||
* 文案关键词高亮
|
||
*/
|
||
export const highlightWords = (Word: string, title?: string) => {
|
||
if (!Word) return xssPurify(title || '');
|
||
title = title ? xssPurify(title) : '';
|
||
const regexPattern = new RegExp(`(${Word})`, 'gi');
|
||
const str = title?.replace(regexPattern, (_, match) => `<span style="color:red">${match}</span>`);
|
||
return str;
|
||
};
|
||
/* 过滤对象中的空属性值
|
||
* @param obj
|
||
* @returns
|
||
*/
|
||
export function filterEmptyObj(obj: object) {
|
||
const newObj = obj;
|
||
for (const key in newObj) { // 删除空属性值
|
||
if (Object.prototype.hasOwnProperty.call(newObj, key)) {
|
||
if (!newObj[key]) {
|
||
delete newObj[key];
|
||
}
|
||
}
|
||
}
|
||
return newObj;
|
||
}
|
||
|
||
export const fullscreen = (id: string): void => {
|
||
if (document.fullscreenElement || document.webkitCurrentFullScreenElement) {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen();
|
||
} else if (document.mozCancelFullScreen) {
|
||
// 兼容Firefox
|
||
document.mozCancelFullScreen();
|
||
} else if (document.webkitExitFullscreen) {
|
||
// 兼容Chrome, Safari and Opera等
|
||
document.webkitExitFullscreen();
|
||
} else if (document.msExitFullscreen) {
|
||
// 兼容IE/Edge
|
||
document.msExitFullscreen();
|
||
}
|
||
} else {
|
||
const dom = document.getElementById(id);
|
||
if (dom?.requestFullscreen) {
|
||
dom.requestFullscreen();
|
||
} else if (dom?.mozRequestFullScreen) {
|
||
// 兼容Firefox
|
||
dom.mozRequestFullScreen();
|
||
} else if (dom?.webkitRequestFullScreen) {
|
||
// 兼容Chrome, Safari and Opera等
|
||
dom.webkitRequestFullScreen();
|
||
} else if (dom?.msRequestFullscreen) {
|
||
// 兼容IE/Edge
|
||
dom.msRequestFullscreen();
|
||
}
|
||
}
|
||
};
|
||
// 对象深层递归合并
|
||
export const deepMerge = function(target: object, source: object) {
|
||
for (const key in source) {
|
||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
||
deepMerge(target[key], source[key]);
|
||
} else {
|
||
target[key] = source[key];
|
||
}
|
||
}
|
||
}
|
||
return target;
|
||
};
|
||
// 最长公共子序列(模糊匹配)
|
||
export const longestCommonSubsequence = function(str1: string, str2: string) {
|
||
const m = str1.length;
|
||
const n = str2.length;
|
||
const dp = new Array(m + 1);
|
||
for (let i = 0; i <= m; i++) {
|
||
dp[i] = new Array(n + 1).fill(0);
|
||
}
|
||
for (let i = 1; i <= m; i++) {
|
||
for (let j = 1; j <= n; j++) {
|
||
if (str1[i - 1] === str2[j - 1]) {
|
||
dp[i][j] = dp[i - 1][j - 1] + 1;
|
||
} else {
|
||
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||
}
|
||
}
|
||
}
|
||
let lcs = '';
|
||
let i = m; let j = n;
|
||
while (i > 0 && j > 0) {
|
||
if (str1[i - 1] === str2[j - 1]) {
|
||
lcs = str1[i - 1] + lcs;
|
||
i--;
|
||
j--;
|
||
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
||
i--;
|
||
} else {
|
||
j--;
|
||
}
|
||
}
|
||
return lcs;
|
||
};
|
||
export const blurMatch = function(target: string, keywords: string) {
|
||
const m = target.length;
|
||
const n = keywords.length;
|
||
const dp = new Array(m + 1);
|
||
for (let i = 0; i <= m; i++) {
|
||
dp[i] = new Array(n + 1).fill('');
|
||
}
|
||
const match = [];
|
||
for (let i = 1; i <= m; i++) {
|
||
for (let j = 1; j <= n; j++) {
|
||
if (target[i - 1] === keywords[j - 1]) {
|
||
dp[i][j] = dp[i - 1][j - 1] + keywords[j - 1];
|
||
if (typeof match[j - 1] === 'undefined') match[j - 1] = i - 1;
|
||
} else {
|
||
dp[i][j] = dp[i - 1][j].length > dp[i][j - 1].length ? dp[i - 1][j] : dp[i][j - 1];
|
||
}
|
||
}
|
||
}
|
||
return match;
|
||
};
|
||
/**
|
||
* xss 过滤
|
||
* purify 规则参考:https://github.com/cure53/DOMPurify#control-our-allow-lists-and-block-lists
|
||
*/
|
||
const purifyConfig = { FORBID_TAGS: ['img'] };
|
||
export const xssPurify = (str: string) => DOMPurify.sanitize(str, purifyConfig);
|
||
|
||
export const messageError = ({ error_message, message }) => Message.error(error_message || message);
|
||
|
||
export const savePageRef = (fullPath:string) => {
|
||
// const BASE_URL = (import.meta as any).env.VITE_HOST;
|
||
const BASE_URL = window.location.origin + '/openRepoPortal';
|
||
const beforeRef = sessionStorage.getItem('ref');
|
||
const ref = beforeRef || document.referrer || '';
|
||
window.page_ref = ref;
|
||
sessionStorage.setItem('ref', BASE_URL + fullPath);
|
||
};
|
||
|
||
/**
|
||
* 处理上报header中的地址,防止 header size 过大 报错431
|
||
* @param href string访问地址
|
||
* @returns {string} 访问地址
|
||
*/
|
||
export const cutParamsInUrl = (href: string) => {
|
||
if (!href) return '';
|
||
try {
|
||
const location = new URL(href);
|
||
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
|
||
for (const k in query) {
|
||
if (typeof query[k] === 'string') {
|
||
query[k] = query[k]?.substring(0, 100);
|
||
}
|
||
}
|
||
location.search = qs.stringify(query);
|
||
return location.href;
|
||
} catch (error) {
|
||
console.log(error);
|
||
return href;
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 获取注册来源utm_source(有效时间一小时)
|
||
* @returns {string} utm_source
|
||
*/
|
||
export const getSignUtmSource = () => {
|
||
const utm_source_sign = localStorage.getItem('utm_source_sign');
|
||
if (utm_source_sign) {
|
||
const intervalTime = 1000 * 60 * 60;
|
||
const currentTime = Date.now();
|
||
const utm_source_sign_time = Number(localStorage.getItem('utm_source_sign_time'));
|
||
if (utm_source_sign_time + intervalTime < currentTime) {
|
||
// 超过一小时,清除utm_source
|
||
localStorage.removeItem('utm_source_sign');
|
||
localStorage.removeItem('utm_source_sign_time');
|
||
return '';
|
||
} else {
|
||
return JSON.parse(utm_source_sign);
|
||
}
|
||
}
|
||
return '';
|
||
};
|