搜索结果列表页面开发
This commit is contained in:
92
src/utils/apiStorage.ts
Normal file
92
src/utils/apiStorage.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as localForage from 'localforage';
|
||||
|
||||
export interface IRequestCache {
|
||||
url: string; // 匹配 url,必填
|
||||
retry?: number; // 错误重试次数,非必填,默认为 0
|
||||
method?: 'get' | 'post' | 'put' | 'delete'; // 接口请求方法,默认为 'get'
|
||||
maxAge?: number; // 接口存储保留时间,默认为无限制
|
||||
maxRows?: number; // 接口存储规格上限配置,默认 10 条
|
||||
timeout?: number; // 前端判断接口超时时间,非必填,默认为无限制
|
||||
degradedTime?: number; // 接口熔断时间,非必填,默认为30秒
|
||||
withHeaders?: string[]; // 需要识别的 header,非必填,默认为空
|
||||
ignoreUrlParams?: string[]; // 需要忽略的 url 参数,非必填,默认为空,即会保存携带不同参数的所有url
|
||||
withBodys?: string[]; // 区分请求体不同的请求
|
||||
excludeStatusCode?: number[]; // 忽略的http状态码
|
||||
}
|
||||
|
||||
export class StorageService {
|
||||
instanceName = '';
|
||||
instanceMap:any = {};
|
||||
requestCacheList: IRequestCache[] = [];
|
||||
SINGLE_SIZE_LIMIT = 1024 * 1024; //单条数据大小限制1M
|
||||
TOTAL_NUMBER_LIMIT = 200; //单数据库实例存储条数不超过200
|
||||
|
||||
setRequestCacheList(requestCacheList: IRequestCache[]) {
|
||||
this.requestCacheList = requestCacheList;
|
||||
}
|
||||
|
||||
getRequestCacheList() {
|
||||
return this.requestCacheList;
|
||||
}
|
||||
|
||||
|
||||
setInstanceName(name: string) {
|
||||
this.instanceName = name;
|
||||
}
|
||||
|
||||
getInstance(name: string) {
|
||||
if (!this.instanceMap[name]) {
|
||||
this.instanceMap[name] = localForage.createInstance({
|
||||
name: `GitcodeDB_${name}`,
|
||||
storeName: `GitcodeStore_${name}`,
|
||||
});
|
||||
}
|
||||
return this.instanceMap[name];
|
||||
}
|
||||
setItem(key: string, value: any, instanceName = this.instanceName) {
|
||||
const store = this.getInstance(instanceName);
|
||||
this.chkAndCtlIntSize(store);
|
||||
store.setItem(key, value);
|
||||
}
|
||||
|
||||
getItem(key: string, instanceName = this.instanceName) {
|
||||
const store = this.getInstance(instanceName);
|
||||
return store.getItem(key);
|
||||
}
|
||||
|
||||
removeItem(key: string, instanceName = this.instanceName) {
|
||||
const store = this.getInstance(instanceName);
|
||||
store.removeItem(key);
|
||||
}
|
||||
|
||||
// 控制数据库总条数,超过后删除最早的一条
|
||||
chkAndCtlIntSize(store: any, size = this.TOTAL_NUMBER_LIMIT) {
|
||||
store.keys().then((keys: any) => {
|
||||
const len = keys.length;
|
||||
if (len > size) {
|
||||
store.removeItem(keys[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 获取字符串占用存储大小
|
||||
sizeOf(str: any) {
|
||||
let total = 0;
|
||||
let charCode;
|
||||
str.split('').forEach((element: any, index: any) => {
|
||||
charCode = str.charCodeAt(index);
|
||||
if (charCode <= 0x007f) {
|
||||
total += 1;
|
||||
} else if (charCode <= 0x07ff) {
|
||||
total += 2;
|
||||
} else if (charCode <= 0xffff) {
|
||||
total += 3;
|
||||
} else {
|
||||
total += 4;
|
||||
}
|
||||
});
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
export const storageService = new StorageService();
|
||||
31
src/utils/asset.ts
Normal file
31
src/utils/asset.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const FindFileName = (path: string) => {
|
||||
/* find filename */
|
||||
const isName = /.*\/.*\/(.*\..*)/;
|
||||
const result = isName.exec(path)?.length
|
||||
? (isName.exec(path) as Array<string>)[1]
|
||||
: '';
|
||||
return result.split('.')[0];
|
||||
};
|
||||
/** 用于合并静态资源请求数量, 如果目录中的资源文件过多而需要引用的较少时不建议使用
|
||||
* 可以将数量可观并且公用的抽放到同一层文件夹,可以提升页面渲染速度
|
||||
*/
|
||||
export const TransAssetsUrl = (
|
||||
source: Record<string, any>,
|
||||
key: string,
|
||||
path?: string
|
||||
): any => {
|
||||
if (key) {
|
||||
let target: string | undefined = '';
|
||||
target = Object.keys(source).find((pathName) => {
|
||||
const p = path ? `${path}${key}` : key;
|
||||
return p === FindFileName(pathName);
|
||||
});
|
||||
if (target) {
|
||||
return source[target].default;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
16
src/utils/base64.ts
Normal file
16
src/utils/base64.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import CryptoJS from 'crypto-js';
|
||||
|
||||
const encode = (sourceStr: string): string => {
|
||||
const wordArray = CryptoJS.enc.Utf8.parse(sourceStr);
|
||||
|
||||
return CryptoJS.enc.Base64.stringify(wordArray);
|
||||
};
|
||||
|
||||
const decode = (encodedStr: string): string => {
|
||||
return CryptoJS.enc.Base64.parse(encodedStr).toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode
|
||||
};
|
||||
45
src/utils/catch.ts
Normal file
45
src/utils/catch.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { emitEvent } from '@/utils/eventBus';
|
||||
export interface ReqReturn<T>{
|
||||
data:AxiosResponse<T> | T |null;
|
||||
error:any;
|
||||
}
|
||||
type ReqFn<T = any, D = any> = (params: D) => Promise<AxiosResponse<T> | T>;
|
||||
type ReqFnSelect<T = any, D = any> = (params?: D) => Promise<AxiosResponse<T> | T>;
|
||||
export async function reqCatch<T = any, D = any>(req: ReqFn<T, D> | ReqFnSelect<T, D>, params?: D):
|
||||
Promise<ReqReturn<T>> {
|
||||
try {
|
||||
const data = await req(params);
|
||||
return {
|
||||
data,
|
||||
error: null
|
||||
};
|
||||
} catch (e) {
|
||||
// 可在此处定义全局的错误处理方法,也可在外部处理
|
||||
return {
|
||||
data: null,
|
||||
error: e
|
||||
};
|
||||
}
|
||||
}
|
||||
interface ReqReturnV2<T>{
|
||||
data:AxiosResponse<T>|null;
|
||||
error:any;
|
||||
}
|
||||
export type catchRt<T> = Promise<ReqReturnV2<T>>;
|
||||
export async function reqCatchV2<R=any>(req:()=>Promise<AxiosResponse<R>>):catchRt<R> { // 设置请求函数的返回值,R类型可以自动推导
|
||||
try {
|
||||
const data = await req();
|
||||
return {
|
||||
data,
|
||||
error: null
|
||||
};
|
||||
} catch (e) {
|
||||
// 可在此处定义全局的错误处理方法,也可在外部处理
|
||||
emitEvent('responseError', e);
|
||||
return {
|
||||
data: null,
|
||||
error: e
|
||||
};
|
||||
}
|
||||
}
|
||||
77
src/utils/color.ts
Normal file
77
src/utils/color.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/** 混入颜色透明度 */
|
||||
/**
|
||||
*
|
||||
* @param color #000 | #000000
|
||||
* @param opacity 0~1
|
||||
* @returns rgba-color []
|
||||
*/
|
||||
export function convertRgbaColor(color: string, opacity: number | number[], theme: 'dark' | 'light' = 'light'): string[] {
|
||||
const hexColorRegex = /^#([A-Fa-f0-9]{3}){1,2}$/;
|
||||
if (!hexColorRegex.test(color)) {
|
||||
console.error("Invalid color format. Please use '#RGB' or '#RRGGBB'.");
|
||||
return [];
|
||||
}
|
||||
|
||||
// 如果颜色值是 "#RGB" 格式,则将其转换为 "#RRGGBB" 格式
|
||||
if (color.length === 4) {
|
||||
color = color.replace(/^#(.)(.)(.)$/, '#$1$1$2$2$3$3');
|
||||
}
|
||||
if (Array.isArray(opacity)) {
|
||||
return opacity.map(o => {
|
||||
// 确保透明度值在有效范围 [0, 1] 内
|
||||
return convertRgba(hydrateColor(color, o, theme), o);
|
||||
});
|
||||
} else {
|
||||
return [convertRgba(hydrateColor(color, opacity, theme), opacity)];
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateColor(color: string, opacity: number, theme: 'dark' | 'light' = 'light'): string {
|
||||
let mergeColor;
|
||||
if (theme === 'dark') {
|
||||
mergeColor = '#000000';
|
||||
} else {
|
||||
mergeColor = '#ffffff';
|
||||
};
|
||||
const weight = Math.max(Math.min(Number(opacity), 1), 0);
|
||||
const r1 = Number.parseInt(color.substring(1, 3), 16);
|
||||
const g1 = Number.parseInt(color.substring(3, 5), 16);
|
||||
const b1 = Number.parseInt(color.substring(5, 7), 16);
|
||||
const r2 = Number.parseInt(mergeColor.substring(1, 3), 16);
|
||||
const g2 = Number.parseInt(mergeColor.substring(3, 5), 16);
|
||||
const b2 = Number.parseInt(mergeColor.substring(5, 7), 16);
|
||||
const r = Math.round(r1 * (1 - weight) + r2 * weight);
|
||||
const g = Math.round(g1 * (1 - weight) + g2 * weight);
|
||||
const b = Math.round(b1 * (1 - weight) + b2 * weight);
|
||||
const _r = (`0${(r || 0).toString(16)}`).slice(-2);
|
||||
const _g = (`0${(g || 0).toString(16)}`).slice(-2);
|
||||
const _b = (`0${(b || 0).toString(16)}`).slice(-2);
|
||||
return `#${_r}${_g}${_b}`;
|
||||
}
|
||||
|
||||
function convertRgba(color: string, opacity: number) {
|
||||
const r = Number.parseInt(color.slice(1, 3), 16);
|
||||
const g = Number.parseInt(color.slice(3, 5), 16);
|
||||
const b = Number.parseInt(color.slice(5, 7), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据颜色计算亮度值,用于判断深色还是浅色
|
||||
* @param color 16进制颜色
|
||||
* @returns
|
||||
*/
|
||||
export function rgbToLum(color: string) {
|
||||
let r = Number.parseInt(color.slice(1, 3), 16);
|
||||
let g = Number.parseInt(color.slice(3, 5), 16);
|
||||
let b = Number.parseInt(color.slice(5, 7), 16);
|
||||
const gamma = (value) => {
|
||||
value = value / 255;
|
||||
return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
|
||||
};
|
||||
|
||||
r = gamma(r);
|
||||
g = gamma(g);
|
||||
b = gamma(b);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
359
src/utils/degradeInterceptor.ts
Normal file
359
src/utils/degradeInterceptor.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import type { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
||||
import { getAdapter } from "axios";
|
||||
import { storageService } from "./apiStorage";
|
||||
type IRequestStatus = 'timeout' | 'error' | 'success';
|
||||
|
||||
// 请求状态表
|
||||
interface IRequestStatusRecord {
|
||||
id: string; // 请求匹配到的缓存策略 url
|
||||
status: IRequestStatus; // 请求状态
|
||||
updatedAt: number; // 请求状态更新时间
|
||||
}
|
||||
|
||||
const DEBUG_FLAG = 'common-degrade-interceptor-debug';
|
||||
const REQUEST_STATUS_TABLE_KEY = 'common-request-status-table';
|
||||
const PARAM_REG = '[\%0-9a-zA-Z-_.]+';
|
||||
|
||||
function debug(...args: any) {
|
||||
const debugMode = localStorage.getItem(DEBUG_FLAG) === 'true';
|
||||
if (debugMode) {
|
||||
console.log('[gitcode]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export class DegradeInterceptor {
|
||||
private storageService;
|
||||
constructor() {
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
onRequestFulfilled(config: AxiosRequestConfig) {
|
||||
// 是否走缓存策略?
|
||||
if (this.disableDegrade() || !this.isReqMatched(config)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const strategy = this.getReqCacheStrategy(config);
|
||||
if (strategy?.timeout) {
|
||||
config.timeout = strategy.timeout;
|
||||
}
|
||||
|
||||
debug('当前请求', config.method, config.url);
|
||||
debug('缓存策略', strategy);
|
||||
if (this.isNeedReadCache(config)) {
|
||||
debug('降级状态下读缓存');
|
||||
config.adapter = this.readCacheAdapter.bind(this);
|
||||
return config;
|
||||
}
|
||||
|
||||
config.adapter = this.retryAdapterEnhancer(config).bind(this);
|
||||
return config;
|
||||
}
|
||||
|
||||
disableDegrade() {
|
||||
const disableDegradeFeat = localStorage.getItem('disableDegradeFeat') && localStorage.getItem('disableDegradeFeat')?.toLowerCase() === 'true';
|
||||
debug('降级开关:', disableDegradeFeat);
|
||||
return disableDegradeFeat;
|
||||
}
|
||||
|
||||
onResponseFulfilled(response: AxiosResponse) {
|
||||
if (!this.disableDegrade() && !response?.request?.isCache && this.isReqMatched(response.config)) {
|
||||
this.handleResp(response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
onResponseRejected(error: any) {
|
||||
if (!this.disableDegrade() && error.config && this.isReqMatched(error.config)) {
|
||||
// 状态码屏蔽
|
||||
const strategy = this.getReqCacheStrategy(error.config);
|
||||
if (strategy?.excludeStatusCode?.includes(error?.request?.status)) {
|
||||
debug(`当前错误码为${error?.request?.status}不读缓存`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const isTimeout = error?.request?.status === 504 || error.code === 'ECONNABORTED';
|
||||
const errorStatus = isTimeout ? 'timeout' : 'error';
|
||||
this.setRequestCacheStatus(error.config, errorStatus);
|
||||
debug('错误状态下读缓存');
|
||||
return this.getRequestCache(error.config);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
handleResp(response: AxiosResponse) {
|
||||
// 请求没有报错,更新缓存并记录缓存的时间戳
|
||||
this.setRequestCache(response.config, response);
|
||||
this.setRequestCacheStatus(response.config, 'success');
|
||||
// 判断缓存的条数
|
||||
this.checkRequestCacheRows(response.config);
|
||||
}
|
||||
|
||||
checkRequestCacheRows(request: any) {
|
||||
const statusList = this.getAllRequestStatus();
|
||||
const strategy = this.getReqCacheStrategy(request);
|
||||
const maxRows = strategy.maxRows || 10;
|
||||
// 先清理一波过期缓存?
|
||||
const reqCacheRecord = Object.keys(statusList)
|
||||
.reduce((p, c) => {
|
||||
p.push({ storageKey: c, ...statusList[c] } as never);
|
||||
return p;
|
||||
}, [])
|
||||
.filter((item: IRequestStatusRecord) => item.id === strategy.url && item.status === 'success' && item.updatedAt)
|
||||
.sort((a: any, b: any) => b.updatedAt - a.updatedAt);
|
||||
|
||||
debug('reqCacheRecord', reqCacheRecord);
|
||||
|
||||
if (reqCacheRecord.length < maxRows) {
|
||||
return;
|
||||
}
|
||||
|
||||
reqCacheRecord.filter((_, index) => index + 1 > maxRows)
|
||||
.forEach((item: any) => {
|
||||
debug('删除超限缓存', item.storageKey);
|
||||
this.storageService.removeItem(item.storageKey);
|
||||
delete statusList[item.storageKey];
|
||||
})
|
||||
|
||||
this.saveAllRequestStatus(statusList);
|
||||
}
|
||||
|
||||
isReqMatched(config: AxiosRequestConfig): boolean {
|
||||
const strategy = this.getReqCacheStrategy(config);
|
||||
const method = strategy?.method || 'get';
|
||||
const isMatchMethod = config.method?.toLocaleLowerCase() === method.toLocaleLowerCase();
|
||||
return strategy && isMatchMethod;
|
||||
}
|
||||
|
||||
wrapHttpResponse(config: AxiosRequestConfig, responseCache: any) {
|
||||
return {
|
||||
...responseCache,
|
||||
config,
|
||||
request: {
|
||||
...responseCache?.request,
|
||||
isCache: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getStorageKey(config: AxiosRequestConfig) {
|
||||
const method = config.method?.toLocaleUpperCase();
|
||||
const strategy = this.getReqCacheStrategy(config);
|
||||
|
||||
// 处理 url 尾部参数
|
||||
const requestParams = this.getRequsetParams(config);
|
||||
|
||||
const paramsStringArr: any = [];
|
||||
Object.keys(requestParams).forEach((key) => {
|
||||
if (!strategy?.ignoreUrlParams?.includes(key) && key !== '_') {
|
||||
const value = requestParams[key];
|
||||
paramsStringArr.push(`${key}=${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理 url 内参数
|
||||
let shortUrl = config.url?.split('?')[0] || '';
|
||||
const matches: any = this.getReqUrlMatchResult(strategy.url, shortUrl);
|
||||
Object.keys(matches?.groups ?? {}).forEach((key) => {
|
||||
if (strategy?.ignoreUrlParams?.includes(key)) {
|
||||
const value = matches.groups[key];
|
||||
// 将 /abc123/xxx 替换成 /{serviceId}/xxx
|
||||
shortUrl = shortUrl.replace(value, `{${key}}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理 headers 参数
|
||||
|
||||
const headerString = strategy?.withHeaders?.map((header: any) => config.headers?.[header]).join('&');
|
||||
const headerStr = headerString ? `${headerString}~` : '';
|
||||
const paramsString = paramsStringArr.length ? '?' + paramsStringArr.join('&') : '';
|
||||
|
||||
return method + '~' + headerStr + shortUrl + paramsString;
|
||||
}
|
||||
|
||||
onRequestRejected = (error: AxiosError) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
getRequsetParams(config: AxiosRequestConfig) {
|
||||
if (config.method === 'get') {
|
||||
try {
|
||||
const idx = config.url?.indexOf('?') || -1;
|
||||
if (idx === -1) {
|
||||
return config.params || {};
|
||||
}
|
||||
|
||||
const params = config.url?.slice(idx + 1);
|
||||
const requestParams = params?.split('&').reduce((obj: any, curr) => {
|
||||
const [key = '', value = ''] = curr?.split('=') || [];
|
||||
obj[key] = value;
|
||||
return obj;
|
||||
}, {});
|
||||
Object.assign(requestParams, config.params);
|
||||
|
||||
return requestParams ?? {};
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return typeof config.data === 'string' ? JSON.parse(config.data) : config.data;
|
||||
}
|
||||
}
|
||||
|
||||
isNeedReadCache(config: AxiosRequestConfig) {
|
||||
// 降级状态或已经还未过熔断时间,默认为 30s
|
||||
const reqStatus = this.getRequestStatus(config);
|
||||
const strategy: any = this.getReqCacheStrategy(config);
|
||||
const degradedTime = strategy?.degradedTime || 30 * 1000;
|
||||
const isError = ['timeout', 'error'].includes(reqStatus?.status);
|
||||
const isInDegradedTime = Date.now() - reqStatus?.updatedAt < degradedTime;
|
||||
debug('isError', isError, 'isInDegradedTime', isInDegradedTime);
|
||||
return isError && isInDegradedTime;
|
||||
}
|
||||
|
||||
isCacheTimeout(config: AxiosRequestConfig) {
|
||||
const strategy: any = this.getReqCacheStrategy(config);
|
||||
const reqStatus = this.getRequestStatus(config);
|
||||
if (!strategy.maxAge) {
|
||||
return false;
|
||||
}
|
||||
if (Date.now() - reqStatus.updatedAt > strategy.maxAge) {
|
||||
this.removeRequestCache(config);
|
||||
this.deleteRequestStatus(config);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
readCacheAdapter(config: AxiosRequestConfig) {
|
||||
return this.getRequestCache(config).then((responseCache: any) => {
|
||||
debug('命中缓存');
|
||||
return this.wrapHttpResponse(config, responseCache);
|
||||
});
|
||||
}
|
||||
|
||||
retryAdapterEnhancer(config: AxiosRequestConfig) {
|
||||
const defaultAdapter: any = getAdapter(config.adapter);
|
||||
return (config: AxiosRequestConfig) => {
|
||||
const strategy: any = this.getReqCacheStrategy(config);
|
||||
const retryCount = strategy.retry ?? 0;
|
||||
let __retryCount = 0;
|
||||
const request: any = async () => {
|
||||
try {
|
||||
return await defaultAdapter(config);
|
||||
} catch (error: any) {
|
||||
if (strategy?.excludeStatusCode?.includes(error?.status)) {
|
||||
debug(`当前错误码为${error?.status}不需要重试`);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (!retryCount || __retryCount >= retryCount) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
__retryCount++;
|
||||
return request();
|
||||
}
|
||||
};
|
||||
return request();
|
||||
};
|
||||
}
|
||||
|
||||
// 请求状态表 start
|
||||
getAllRequestStatus(): { [key: string]: IRequestStatusRecord } {
|
||||
return JSON.parse(sessionStorage.getItem(REQUEST_STATUS_TABLE_KEY) || '{}');
|
||||
}
|
||||
saveAllRequestStatus(statusList: any) {
|
||||
sessionStorage.setItem(REQUEST_STATUS_TABLE_KEY, JSON.stringify(statusList));
|
||||
}
|
||||
getRequestStatus(config: AxiosRequestConfig): IRequestStatusRecord {
|
||||
const statusList = this.getAllRequestStatus();
|
||||
const key = this.getStorageKey(config);
|
||||
return statusList[key];
|
||||
}
|
||||
setRequestCacheStatus(config: AxiosRequestConfig, status: IRequestStatus) {
|
||||
const key = this.getStorageKey(config);
|
||||
const strategy: any = this.getReqCacheStrategy(config);
|
||||
const statusList = this.getAllRequestStatus();
|
||||
statusList[key] = {
|
||||
id: strategy.url,
|
||||
status,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
debug(key, '状态', status, '时间:', new Date().toLocaleTimeString('zh-cn'));
|
||||
this.saveAllRequestStatus(statusList);
|
||||
}
|
||||
deleteRequestStatus(config: AxiosRequestConfig) {
|
||||
const key = this.getStorageKey(config);
|
||||
const statusList = this.getAllRequestStatus();
|
||||
delete statusList[key];
|
||||
this.saveAllRequestStatus(statusList);
|
||||
}
|
||||
// 请求状态表 end
|
||||
|
||||
// 请求缓存表 start
|
||||
setRequestCache(config: AxiosRequestConfig, response: AxiosResponse) {
|
||||
const key = this.getStorageKey(config);
|
||||
// function cannot be cloned in localForage
|
||||
this.storageService.setItem(key, JSON.parse(JSON.stringify(response)));
|
||||
}
|
||||
getRequestCache(config: AxiosRequestConfig) {
|
||||
const key = this.getStorageKey(config);
|
||||
return this.storageService.getItem(key);
|
||||
}
|
||||
removeRequestCache(config: AxiosRequestConfig) {
|
||||
const key = this.getStorageKey(config);
|
||||
this.storageService.removeItem(key);
|
||||
}
|
||||
// 请求缓存表 end
|
||||
|
||||
// 请求缓存策略表 start
|
||||
getReqUrlMatchResult(regexpUrl: string, requestUrl: string) {
|
||||
// 将 /{serviceId}/xxx 替换成具名正则匹配 /(?<serviceId>[0-9a-z-]+)/xxx
|
||||
let regExp = regexpUrl;
|
||||
if (typeof regexpUrl === 'string') {
|
||||
regExp = regexpUrl.replace(/{/g, '(?<').replace(/}/g, `>${PARAM_REG})`) + '$';
|
||||
}
|
||||
const noParamUrl = requestUrl.split('?')[0];
|
||||
const matches = new RegExp(regExp).exec(noParamUrl);
|
||||
return matches;
|
||||
}
|
||||
getReqCacheStrategy(config: AxiosRequestConfig): any {
|
||||
const requestCacheList = this.storageService.getRequestCacheList();
|
||||
return requestCacheList
|
||||
.filter((item: any) => item.url && (typeof item.url === 'string' ? item.url?.length > 2 : true))
|
||||
.find((item: any) => {
|
||||
const matches = this.getReqUrlMatchResult(item.url, config.url || '');
|
||||
return !!matches;
|
||||
});
|
||||
}
|
||||
// 请求缓存策略表 end
|
||||
}
|
||||
|
||||
const degradeInterceptor = new DegradeInterceptor()
|
||||
degradeInterceptor.onRequestFulfilled = degradeInterceptor.onRequestFulfilled.bind(degradeInterceptor)
|
||||
degradeInterceptor.onRequestRejected = degradeInterceptor.onRequestRejected.bind(degradeInterceptor)
|
||||
degradeInterceptor.onResponseFulfilled = degradeInterceptor.onResponseFulfilled.bind(degradeInterceptor)
|
||||
degradeInterceptor.onResponseRejected = degradeInterceptor.onResponseRejected.bind(degradeInterceptor)
|
||||
|
||||
export default degradeInterceptor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
src/utils/editor.ts
Normal file
11
src/utils/editor.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
interface Params {
|
||||
project_id: string
|
||||
file_path: string
|
||||
branch: string
|
||||
}
|
||||
export const onlineEditorBaseLink = 'https://hn.devcloud.huaweicloud.com/codeartside/webide';
|
||||
export const autoLoginLink = `https://auth.huaweicloud.com/authui/saml/login?xAccountType=hid_ochgo94c_ruldd9_IDP&isFirstLogin=false&service=`;
|
||||
export function openEditor(params:Params):void {
|
||||
const { project_id, file_path, branch } = params;
|
||||
window.open(`/online_editor/?project_id=${project_id}&file_path=${file_path}&branch=${branch}`);
|
||||
}
|
||||
35
src/utils/eventBus.ts
Normal file
35
src/utils/eventBus.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import mitt from 'mitt';
|
||||
|
||||
export type EventBus = {
|
||||
logout?: boolean
|
||||
login?: any
|
||||
updateNaviBar: void
|
||||
responseError:string
|
||||
updateMrAction:string
|
||||
updateIssueAction:string
|
||||
notice: void
|
||||
forbiddenRefresh: void
|
||||
microRouterChange: void
|
||||
openModal:void
|
||||
updateNotice: number
|
||||
updateUserInfo: Record<string, any>
|
||||
reportView: void // 子应用中的路由主动上报
|
||||
}
|
||||
|
||||
const emitter = mitt<EventBus>();
|
||||
|
||||
export const addEventListener = (evtName: keyof EventBus, callback: (payload: any) => void) => emitter.on(evtName, callback);
|
||||
|
||||
export const emitEvent = (evtName: keyof EventBus, payload?: any) => emitter.emit(evtName, payload);
|
||||
|
||||
export const offEvent = (evtName: keyof EventBus, handler?:any) => emitter.off(evtName, handler || undefined);
|
||||
|
||||
export const offEvents = (list: Array<keyof EventBus>) => {
|
||||
list.forEach(offEvent);
|
||||
};
|
||||
|
||||
export const getEvents = () => emitter.all;
|
||||
|
||||
export const clearEvent = () => emitter.all.clear();
|
||||
|
||||
export default emitter;
|
||||
13
src/utils/getRepoId.ts
Normal file
13
src/utils/getRepoId.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import router from '@/router';
|
||||
|
||||
export default function getRepoId(connector: string = '%2F', namespace: string = '', routeName: string = '') {
|
||||
const routeParams = router.currentRoute.value.params;
|
||||
let routeNamespace = namespace || routeParams.namespace;
|
||||
const routeRepoName = routeName || routeParams.repoName;
|
||||
if (!routeNamespace || !routeRepoName) return null;
|
||||
routeNamespace = Array.isArray(routeNamespace)
|
||||
? routeNamespace.join(connector)
|
||||
: routeNamespace.split('/').join(connector);
|
||||
const repoId = `${routeNamespace}${connector}${routeRepoName}`;
|
||||
return repoId;
|
||||
}
|
||||
47
src/utils/hooks/useAccModal.ts
Normal file
47
src/utils/hooks/useAccModal.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { usePopup } from './usePopup';
|
||||
import AccModal from '@/components/LoginModal/widget/link.vue';
|
||||
import isPhone from '@/utils/isPhone';
|
||||
|
||||
import { useReport } from '@/utils/hooks/useReport';
|
||||
export function useAccModal() {
|
||||
const { mount, unMount, isMounted } = usePopup('g-acc-modal');
|
||||
const closeModal = () => {
|
||||
unMount();
|
||||
};
|
||||
const isMobile = isPhone();
|
||||
const openModal = () => {
|
||||
if (isMounted()) { /** 同一tick只允许弹出一次 */
|
||||
return;
|
||||
}
|
||||
// 统计
|
||||
useReport('quick_login_expo', {}, { 'homeweb-page-title': document.title });
|
||||
mount(AccModal, {
|
||||
modelValue: true,
|
||||
onConfirm: (type: 'csdn' | 'gitee' | 'github') => {
|
||||
// 统计
|
||||
useReport('quick_login_click', {}, { 'homeweb-page-title': document.title });
|
||||
|
||||
// 移动端第三方登录保存登录前页面地址
|
||||
if (isMobile) {
|
||||
localStorage.setItem('loginReturnUrl', location.href || '');
|
||||
} else {
|
||||
localStorage.removeItem('loginReturnUrl');
|
||||
}
|
||||
|
||||
const url = `${(import.meta as any).env.VITE_API_HOST}${(import.meta as any).env.VITE_PASSPORT_PREFIX || ''}/api/v1/oauth/login/${type}`;
|
||||
window.open(url, '_blank', 'width=800, height=800, left=400, top=200');
|
||||
localStorage.setItem('utm_source', `${type}_github_accelerator'`);
|
||||
},
|
||||
onClose: () => {
|
||||
// 统计
|
||||
useReport('quick_login_close', {}, { 'homeweb-page-title': document.title });
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
openModal,
|
||||
isMountedModal: isMounted,
|
||||
closeModal
|
||||
};
|
||||
}
|
||||
48
src/utils/hooks/useAccount.ts
Normal file
48
src/utils/hooks/useAccount.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useAccountStore, type AccountInfo } from '@/stores/user';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
export function useAccount() {
|
||||
const userInfo = useAccountStore();
|
||||
const { accountInfo } = storeToRefs(userInfo);
|
||||
const { saveStatus, saveAccountInfo } = userInfo;
|
||||
const RecordInfo = (source: Required<AccountInfo> | undefined, cached: boolean = true) => {
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
const { access_token, refresh_token, xauth_token, ...userInfo } = source;
|
||||
localStorage.setItem('access_token', access_token);
|
||||
localStorage.setItem('refresh_token', refresh_token);
|
||||
localStorage.setItem('xauth_token', xauth_token);
|
||||
localStorage.setItem('userInfo', JSON.stringify(userInfo));
|
||||
if (cached) {
|
||||
saveStatus(true);
|
||||
saveAccountInfo(userInfo);
|
||||
}
|
||||
};
|
||||
|
||||
const RemoveInfo = () => {
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('refresh_token');
|
||||
localStorage.removeItem('xauth_token');
|
||||
localStorage.removeItem('userInfo');
|
||||
localStorage.removeItem('validator_email');
|
||||
localStorage.removeItem('visited_repo_list');
|
||||
localStorage.removeItem('invite_link');
|
||||
localStorage.removeItem('group_quota');
|
||||
localStorage.removeItem('manageable_group_num');
|
||||
if (localStorage.getItem('cache_timeStamp')) {
|
||||
localStorage.removeItem('cache_timeStamp');
|
||||
}
|
||||
if (localStorage.getItem('mask')) {
|
||||
localStorage.removeItem('mask');
|
||||
}
|
||||
saveStatus(false);
|
||||
saveAccountInfo();
|
||||
};
|
||||
|
||||
return {
|
||||
RecordInfo,
|
||||
RemoveInfo,
|
||||
accountInfo
|
||||
};
|
||||
}
|
||||
24
src/utils/hooks/useBranchOptions.ts
Normal file
24
src/utils/hooks/useBranchOptions.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ref, toValue, watchEffect } from 'vue';
|
||||
import { getBranches } from '@/api/branch/index';
|
||||
import { escapeResData } from '@/utils/index';
|
||||
|
||||
export function useBranchOptions(repoId) {
|
||||
const sort = 'created_desc';
|
||||
|
||||
const options = ref([]);
|
||||
const firstItem = ref('');
|
||||
|
||||
watchEffect(() => {
|
||||
getBranches({ repoId: toValue(repoId), sort }).then((res) => {
|
||||
if (res?.data) {
|
||||
options.value = escapeResData(res)?.content?.map((x) => ({ ...x, value: x.name }));
|
||||
if (options.value.length) firstItem.value = options.value[0].value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
options,
|
||||
firstItem
|
||||
};
|
||||
}
|
||||
101
src/utils/hooks/useFile.ts
Normal file
101
src/utils/hooks/useFile.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// 文件夹ICON
|
||||
const FOLDER_ICON = {
|
||||
expanded: 'gt-folder-open-c',
|
||||
default: 'gt-folder-c'
|
||||
};
|
||||
// 特殊文件类型图标
|
||||
const TYPE_ICON = {
|
||||
commit: 'gt-folder-git-c', // submodule图标
|
||||
default: 'gt-file-c'
|
||||
};
|
||||
// 文件ICON
|
||||
const FILE_ICON = {
|
||||
zip: 'gt-file-zip-c',
|
||||
svg: 'gt-file-svg-c',
|
||||
img: 'gt-picture-c',
|
||||
txt: 'gt-file2-c',
|
||||
code: 'gt-file-code-c',
|
||||
default: 'gt-file-c'
|
||||
};
|
||||
|
||||
// 文件格式正则
|
||||
const REG = {
|
||||
zip: /^(zip|rar|7z)$/i,
|
||||
svg: /^(svg)$/i,
|
||||
img: /^(png|jpg|jpeg|gif|img|webp)$/i,
|
||||
txt: /^(txt|md)$/i,
|
||||
code: /^(json|conf|yml|toml|html|js|ts|css|py|vue|c|cpp|inc|h|makefile|csv|go|cs|java|xml|sh|cfg|Dockerfile|LICENSE|propertites)$|ignore$/i
|
||||
};
|
||||
const _regEntries = Object.entries(REG);
|
||||
|
||||
// 对指定文件后缀类型配置渲染方案
|
||||
const LANGUAGE_MAP = {
|
||||
js: 'javascript',
|
||||
ts: 'typescript',
|
||||
vue: 'html',
|
||||
svg: 'html',
|
||||
yml: 'javascript',
|
||||
py: 'python'
|
||||
};
|
||||
|
||||
export function useFile() {
|
||||
const getFileFormat = (filename: string) => {
|
||||
return filename?.split('.').pop()?.toLowerCase();
|
||||
};
|
||||
|
||||
const getFolderIcon = (expanded?: boolean) => {
|
||||
return expanded ? FOLDER_ICON.expanded : FOLDER_ICON.default;
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string): string => {
|
||||
return TYPE_ICON[type] || TYPE_ICON.default;
|
||||
};
|
||||
|
||||
const getFileIcon = (filename: string) => {
|
||||
try {
|
||||
const format = getFileFormat(filename);
|
||||
for (let i = 0; i < _regEntries.length; i++) {
|
||||
const [key, reg] = _regEntries[i];
|
||||
if (reg.test(format) && FILE_ICON[key]) {
|
||||
return FILE_ICON[key];
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return FILE_ICON.default;
|
||||
};
|
||||
|
||||
const getIcon = (file: object) => {
|
||||
if (file?.type === 'tree') {
|
||||
return getFolderIcon(file.expanded);
|
||||
} else if (file?.type === 'commit') {
|
||||
return getTypeIcon(file.type);
|
||||
}
|
||||
return getFileIcon(file?.name);
|
||||
};
|
||||
|
||||
const getFileLanguage = (filename: string) => {
|
||||
if (filename?.startsWith('.env')) {
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
const format = getFileFormat(filename);
|
||||
if (format?.endsWith('ignore') || format?.endsWith('config')) {
|
||||
return 'javascript';
|
||||
}
|
||||
return LANGUAGE_MAP[format] || format;
|
||||
};
|
||||
|
||||
return {
|
||||
getIcon,
|
||||
getFileIcon, // 通过文件名获取文件icon
|
||||
getTypeIcon, // 通过文件type获取文件icon
|
||||
getFolderIcon, // 通过文件夹的状态,获取文件夹icon
|
||||
|
||||
getFileFormat,
|
||||
|
||||
getFileLanguage
|
||||
};
|
||||
}
|
||||
332
src/utils/hooks/useForm.ts
Normal file
332
src/utils/hooks/useForm.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { computed, ref, reactive, shallowRef, onBeforeUnmount, watchEffect, nextTick } from 'vue';
|
||||
import type { FormListProps } from '@/components/Form/types';
|
||||
import type { Ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAccount } from './useAccount';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import { emitEvent } from '@/utils/eventBus';
|
||||
|
||||
export function useFormInteraction(currentForm: Ref<FormListProps[]>, flag: boolean = false, extraStatus: Ref<boolean> = ref(true)) {
|
||||
const router = useRouter();
|
||||
const { RecordInfo } = useAccount();
|
||||
let interval: any = null;
|
||||
const cacheForm = reactive<{ mobile: string, verificationcode: string, countdownSecond: number, user_id: string, mask: string }>({
|
||||
mobile: '',
|
||||
verificationcode: '',
|
||||
countdownSecond: 59,
|
||||
user_id: '',
|
||||
mask: ''
|
||||
});
|
||||
// 表单错误信息
|
||||
const formErrors = reactive<Record<string, string>>({});
|
||||
// 额外验证参数
|
||||
const extraErrors = reactive<{ agreement: string, requestInfo: string }>({
|
||||
agreement: '',
|
||||
requestInfo: ''
|
||||
});
|
||||
// 协议是否抖动
|
||||
const AgreementWarn = ref(false);
|
||||
// 禁用提交
|
||||
const disabled = ref(true);
|
||||
// 协议状态
|
||||
const status = ref(flag);
|
||||
// 表单ref
|
||||
const FormRef = shallowRef<any>(null);
|
||||
// 按钮loading
|
||||
const loading = ref(false);
|
||||
|
||||
/** 密码框文字间距样式 */
|
||||
const PasswordInputSpacing = computed(() => {
|
||||
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
|
||||
const isIos = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
||||
return isIos ? '0px' : '-1px';
|
||||
});
|
||||
|
||||
/** 当前表单错误信息 */
|
||||
const errorMsg = computed(() => {
|
||||
const keys = Object.keys(formErrors);
|
||||
if (currentForm.value?.length) {
|
||||
const obj = currentForm.value.find(item => keys.includes(item.key) && formErrors[item.key]);
|
||||
if (FormRef.value) {
|
||||
const filterKeys = keys.filter(key => key !== obj?.key && formErrors[key]);
|
||||
filterKeys.length && FormRef.value?.ClearFormFields(filterKeys);
|
||||
hackMsgError(obj?.key === 'verificationcode' || obj?.key === 'code');
|
||||
}
|
||||
if (obj) {
|
||||
return formErrors[obj.key];
|
||||
}
|
||||
return extraErrors.agreement || extraErrors.requestInfo || '';
|
||||
} else {
|
||||
return extraErrors.agreement || extraErrors.requestInfo || '';
|
||||
}
|
||||
});
|
||||
|
||||
/** 设置表单单个字段校验信息 */
|
||||
const setFormErrorKey = (conf: { errors: Record<string, any[]> } | Record<string, any>) => {
|
||||
let message: string = '';
|
||||
let key: string = '';
|
||||
if (Object.prototype.hasOwnProperty.call(conf, 'errors')) {
|
||||
const { errors } = conf;
|
||||
key = Object.keys(errors)[0];
|
||||
const formDict = errors[key];
|
||||
message = formDict?.length ? formDict[0].message : '';
|
||||
} else {
|
||||
key = Object.keys(conf)[0];
|
||||
message = (conf as Record<string, any>)[key];
|
||||
}
|
||||
formErrors[key] = message;
|
||||
};
|
||||
|
||||
/** 捕获整个表单校验信息 */
|
||||
const catchFormErrors = (conf: { errors: Record<string, any[]> }) => {
|
||||
const { errors } = conf;
|
||||
for (const key in errors) {
|
||||
const obj: Record<string, string> = {};
|
||||
obj[key] = errors[key][0].message as string;
|
||||
setFormErrorKey(obj);
|
||||
}
|
||||
};
|
||||
/** 清空表单校验信息 */
|
||||
const clearFormError = (key?: string) => {
|
||||
if (key) {
|
||||
formErrors[key] = '';
|
||||
} else {
|
||||
for (const key in formErrors) {
|
||||
formErrors[key] = '';
|
||||
}
|
||||
for (const key in extraErrors) {
|
||||
extraErrors[key as keyof typeof extraErrors] = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
/** 表单字段change事件 */
|
||||
const handleFormChange = (conf: { key: string, errors: Record<string, any> | null, source: Record<string, any> }) => {
|
||||
const { key, errors } = conf;
|
||||
if (errors) {
|
||||
setFormErrorKey({ errors });
|
||||
} else {
|
||||
formErrors[key] = '';
|
||||
}
|
||||
nextTick(() => {
|
||||
const keys = Object.keys(formErrors);
|
||||
if (currentForm.value?.length) {
|
||||
const obj = currentForm.value.find(item => keys.includes(item.key) && formErrors[item.key]);
|
||||
if (FormRef.value) {
|
||||
const filterKeys = keys.filter(k => k !== obj?.key && formErrors[k]);
|
||||
if (filterKeys.length) {
|
||||
FormRef.value?.ClearFormFields(filterKeys);
|
||||
} else {
|
||||
obj && !errors && FormRef.value?.ValidateFormKeys([obj.key]);
|
||||
}
|
||||
}
|
||||
hackMsgError(obj?.key === 'verificationcode' || obj?.key === 'code');
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 表单input事件 */
|
||||
const handleFormInput = (val: boolean) => {
|
||||
disabled.value = val;
|
||||
};
|
||||
/** 第三方登录 */
|
||||
const handleAuthLogin = (type: 'csdn' | 'gitee' | 'github' | 'mobile', callback?: Function, returnUrl? :string) => {
|
||||
if (localStorage.getItem('access_token')) {
|
||||
callback?.();
|
||||
} else {
|
||||
if (type === 'mobile') {
|
||||
if (returnUrl) {
|
||||
router.push('/loginByPhone?returnUrl=' + returnUrl);
|
||||
} else {
|
||||
router.push('/loginByPhone');
|
||||
}
|
||||
} else {
|
||||
if (!status.value) {
|
||||
extraErrors.agreement = `请阅读并同意用户协议以及其隐私政策`;
|
||||
AgreementWarn.value = true;
|
||||
setTimeout(() => {
|
||||
AgreementWarn.value = false;
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
if (localStorage.getItem('utm_source')) {
|
||||
localStorage.removeItem('utm_source');
|
||||
}
|
||||
// 保存登录前页面地址
|
||||
localStorage.setItem('loginReturnUrl', returnUrl || '');
|
||||
// window.location.href = 'http://localhost:5173/oauth/callback';
|
||||
const url = `${(import.meta as any).env.VITE_API_HOST}${(import.meta as any).env.VITE_PASSPORT_PREFIX || ''}/api/v1/oauth/login/${type}`;
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
/** 提交表单 */
|
||||
const handleSubmit = async(callback: (config: Record<string, any>) => Promise<void>, extraInfo: string = '') => {
|
||||
if (!status.value) {
|
||||
extraErrors.agreement = extraInfo || '请阅读并同意用户协议以及隐私政策';
|
||||
AgreementWarn.value = true;
|
||||
setTimeout(() => {
|
||||
AgreementWarn.value = false;
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
extraErrors.agreement = '';
|
||||
if (FormRef.value) {
|
||||
const formData = await FormRef.value.ValidateForm();
|
||||
if (formData.type === 'success') {
|
||||
loading.value = true;
|
||||
clearFormError();
|
||||
await callback(formData.forms);
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
} else {
|
||||
catchFormErrors(formData);
|
||||
}
|
||||
}
|
||||
};
|
||||
/** 倒计时 */
|
||||
const handleCountDown = async(conf: { key: string, value: any, sourceKey: string }, callback: () => Promise<boolean>) => {
|
||||
const formData = await FormRef.value.ValidateFormKeys([conf.key]);
|
||||
if (formData.type === 'success') {
|
||||
clearFormError(conf.key);
|
||||
const status = await callback();
|
||||
status && resetMsgStatus(conf.sourceKey);
|
||||
} else {
|
||||
setFormErrorKey(formData);
|
||||
}
|
||||
};
|
||||
/** gitcode 外链url */
|
||||
const links = {
|
||||
'agreement': 'https://gitcode.com/Gitcode-offical-team/GitCode-Docs/blob/main/%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE%2F%E6%9C%8D%E5%8A%A1%E6%9D%A1%E6%AC%BE.md',
|
||||
'privacy': 'https://gitcode.com/Gitcode-offical-team/GitCode-Docs/blob/main/%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE%2F%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.md'
|
||||
};
|
||||
/** 华为云 外链url */
|
||||
const links_hw = {
|
||||
agreement: 'https://www.huaweicloud.com/declaration/sa_cua.html',
|
||||
privacy: 'https://www.huaweicloud.com/declaration/sa_prp.html'
|
||||
};
|
||||
/** 展示声明 */
|
||||
const handleDisplay = (type: 'agreement' | 'privacy', class_type: 'gitcode' | 'hw' = 'gitcode') => {
|
||||
const link = class_type === 'gitcode' ? links[type] : links_hw[type];
|
||||
window.open(link);
|
||||
};
|
||||
|
||||
/** 重置短信按钮状态 */
|
||||
const resetMsgStatus = (key: string, status: boolean = true) => {
|
||||
if (currentForm.value?.length) {
|
||||
currentForm.value.forEach(item => {
|
||||
if (item.key === key) {
|
||||
if (item.props) {
|
||||
item.props.countdown = status;
|
||||
}
|
||||
}
|
||||
});
|
||||
status && toCountDown(key);
|
||||
}
|
||||
};
|
||||
|
||||
/** 缓存倒计时间 */
|
||||
const toCountDown = (key: string) => {
|
||||
if (!interval) {
|
||||
if (cacheForm.countdownSecond === 0) {
|
||||
cacheForm.countdownSecond = 59;
|
||||
}
|
||||
interval = setInterval(() => {
|
||||
if (cacheForm.countdownSecond >= 1) {
|
||||
cacheForm.countdownSecond -= 1;
|
||||
} else {
|
||||
if (localStorage.getItem('cache_countdown')) {
|
||||
localStorage.removeItem('cache_countdown');
|
||||
}
|
||||
cacheForm.countdownSecond = 59;
|
||||
interval && clearInterval(interval);
|
||||
interval = null;
|
||||
resetMsgStatus(key, false);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
/** 存贮用户信息以及跳转 */
|
||||
const saveUserInfo = (conf: any, returnUrl?: any) => {
|
||||
const { username, email } = conf;
|
||||
RecordInfo(conf);
|
||||
Message.success({
|
||||
message: '欢迎来到Gitcode!',
|
||||
onClose: () => {
|
||||
emitEvent('notice');
|
||||
}
|
||||
});
|
||||
if (`${username}@gitcode.com` === email) { // 需要修改默认邮箱的弹窗
|
||||
localStorage.setItem('validator_email', 'invalid');
|
||||
} else {
|
||||
localStorage.setItem('validator_email', 'valid');
|
||||
}
|
||||
if (returnUrl) {
|
||||
location.href = returnUrl;
|
||||
} else {
|
||||
router.replace('/');
|
||||
}
|
||||
};
|
||||
/**
|
||||
* redirect绑定华为云
|
||||
* @deprecated
|
||||
*/
|
||||
const redirectBind = (config: { user_id: string, mask: string, mobile: string }) => {
|
||||
for (const key in config) {
|
||||
cacheForm[key as keyof typeof config] = config[key as keyof typeof config];
|
||||
}
|
||||
// router.replace({
|
||||
// path: '/bindPhone',
|
||||
// query: config
|
||||
// });
|
||||
};
|
||||
|
||||
/** hack短信弹窗错误样式 */
|
||||
const hackMsgError = (show_error: boolean) => {
|
||||
const el = document.querySelector('.g-input-button');
|
||||
if (el) {
|
||||
if (show_error) {
|
||||
(el as HTMLElement).style.borderColor = 'var(--devui-danger-line, #f66f6a)';
|
||||
} else {
|
||||
(el as HTMLElement).style.borderColor = 'rgba(230, 230, 232, 1)';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (status.value && extraStatus.value && extraErrors.agreement) {
|
||||
extraErrors.agreement = '';
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
hackMsgError(false);
|
||||
});
|
||||
|
||||
return {
|
||||
AgreementWarn,
|
||||
status,
|
||||
disabled,
|
||||
FormRef,
|
||||
cacheForm,
|
||||
formErrors,
|
||||
PasswordInputSpacing,
|
||||
extraErrors,
|
||||
errorMsg,
|
||||
loading,
|
||||
setFormErrorKey,
|
||||
catchFormErrors,
|
||||
clearFormError,
|
||||
toCountDown,
|
||||
handleFormChange,
|
||||
handleFormInput,
|
||||
handleAuthLogin,
|
||||
handleCountDown,
|
||||
handleDisplay,
|
||||
handleSubmit,
|
||||
saveUserInfo
|
||||
// redirectBind
|
||||
};
|
||||
}
|
||||
228
src/utils/hooks/useIssueTemplate.ts
Normal file
228
src/utils/hooks/useIssueTemplate.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { ref, type Ref } from 'vue';
|
||||
import { fetchIssueTemplateList } from '@/api/issue';
|
||||
import { validateSchema, type ValidationsSchemaOutput } from '@/components/renderer-yaml/helper';
|
||||
import { decode } from '@/views/Repo/Tree/hooks/useRepoFile';
|
||||
import type { ContactLinks, SchemaItem } from '@/components/renderer-yaml/types';
|
||||
import { getSettingsValues } from '@/api/repo';
|
||||
type IssueLink = 'repoIssueCreateChoose' | 'repoIssueCreate' | '';
|
||||
import { reqCatch } from '@/utils/catch';
|
||||
interface Template {
|
||||
content: string;
|
||||
path: string;
|
||||
project_path_with_namespace?: string;
|
||||
default_branch?: string;
|
||||
}
|
||||
export interface ChooseItem {
|
||||
name: string;
|
||||
url: string;
|
||||
template?: string;
|
||||
description: string;
|
||||
title?: string;
|
||||
type: string;
|
||||
path?: string;
|
||||
project_path_with_namespace?: string;
|
||||
default_branch?: string
|
||||
}
|
||||
export interface State {
|
||||
chooses: ChooseItem[];
|
||||
blank_issues_enabled: boolean
|
||||
}
|
||||
function getPathLastName (path:string) {
|
||||
return path.split('/').pop();
|
||||
}
|
||||
export function isMd (path:string) {
|
||||
return path.endsWith('.md');
|
||||
}
|
||||
export function isYaml (path:string) {
|
||||
return path.endsWith('.yml') || path.endsWith('.yaml');
|
||||
}
|
||||
function parseConfigTemplate (links:ContactLinks[], name:string, template:Template):ChooseItem[] {
|
||||
return links.map(item => {
|
||||
return {
|
||||
template: name,
|
||||
name: item.name,
|
||||
url: item.url,
|
||||
description: item.about,
|
||||
path: template.path,
|
||||
project_path_with_namespace: template.project_path_with_namespace,
|
||||
default_branch: template.default_branch,
|
||||
type: 'out',
|
||||
} as ChooseItem
|
||||
})
|
||||
}
|
||||
export const regIssueBaseInfo = /---([\s\S]*?)---/;
|
||||
// 清除空白行的\n数据
|
||||
export function deleteIssueBeforeEntry (content:string):string {
|
||||
const splitContent = content.split('\n');
|
||||
let isFirst = true;
|
||||
return splitContent.filter(item => {
|
||||
if (item !== '') {
|
||||
isFirst = false;
|
||||
}
|
||||
return item !== '' && !isFirst;
|
||||
}).join('\n');
|
||||
}
|
||||
export function parseMdContent(content:string):Record<string, any> {
|
||||
let name = '空白 Issue';
|
||||
let description = '没有找到合适的 Issue 模板?可以通过非模板 Issue 向我们反馈你的问题';
|
||||
let title = '';
|
||||
let labels:string[] = [];
|
||||
const matchContent = (content || '').trim().match(regIssueBaseInfo);
|
||||
const targetContent = matchContent ? matchContent[1] : '';
|
||||
if (targetContent) {
|
||||
const items = targetContent.split('\n').filter(Boolean);
|
||||
items.forEach((child:string) => {
|
||||
const [key, value] = child.split(':').map(item => item.trim());
|
||||
if (key === 'name') {
|
||||
name = value;
|
||||
} else if (key === 'about') {
|
||||
description = value;
|
||||
} else if (key === 'title') {
|
||||
title = value === `''` ? '' : value
|
||||
} else if (key === 'labels') {
|
||||
try {
|
||||
labels = JSON.parse(value);
|
||||
} catch (error) {}
|
||||
}
|
||||
})
|
||||
}
|
||||
return { name, description, title, labels }
|
||||
}
|
||||
function parseMdTemplate (content:string, name:string, template:Template):ChooseItem {
|
||||
const info = parseMdContent(content);
|
||||
return {
|
||||
template: name,
|
||||
name: info.name, //'空白 Issue',
|
||||
description: info.description, // '没有找到合适的 Issue 模板?可以通过非模板 Issue 向我们反馈你的问题',
|
||||
type: 'markdown',
|
||||
url: 'issues/create',
|
||||
path: template.path,
|
||||
default_branch: template.default_branch,
|
||||
project_path_with_namespace: template.project_path_with_namespace || ''
|
||||
}
|
||||
}
|
||||
function parseYamlTemplate (content:ValidationsSchemaOutput, name:string, template:Template):ChooseItem {
|
||||
return {
|
||||
template: name,
|
||||
name: content.name,
|
||||
description: content.description,
|
||||
type: 'template',
|
||||
url: 'issues/create',
|
||||
path: template.path,
|
||||
default_branch: template.default_branch,
|
||||
project_path_with_namespace: template.project_path_with_namespace || ''
|
||||
}
|
||||
}
|
||||
export function fetchText () {
|
||||
return window.fetch('https://cdn-static.gitcode.com/security.yaml').then((response)=> {
|
||||
if (response.ok) return response.text();
|
||||
return '';
|
||||
})
|
||||
}
|
||||
export function useIssueTemplate () {
|
||||
const issueLink:Ref<IssueLink> = ref('repoIssueCreate');
|
||||
const issueLoading:Ref<boolean> = ref(false)
|
||||
// 模版数据
|
||||
const issueTemplates:Ref<ChooseItem[]> = ref([]);
|
||||
// 是否使用空白模版
|
||||
const isUsedEmptyIssueTemplate:Ref<boolean> = ref(true);
|
||||
const setIssueLink = (val: IssueLink)=> {
|
||||
issueLink.value = val;
|
||||
}
|
||||
const parseIssueTemplates = (templates:Template[]) => {
|
||||
let blank_issues_enabled = true;
|
||||
let formatTemplates:ChooseItem[] = [];
|
||||
let mdTemplates:ChooseItem[] = []
|
||||
let configTemplates:ChooseItem[] = [];
|
||||
templates.forEach((template) => {
|
||||
const name = getPathLastName(template.path) || '';
|
||||
if (['config.yml', 'config.yaml'].includes(name)) {
|
||||
const content = validateSchema(decode(template.content || '')) as ValidationsSchemaOutput;
|
||||
configTemplates = parseConfigTemplate(content.contact_links || [], name, template)
|
||||
blank_issues_enabled = content.blank_issues_enabled || false;
|
||||
} else if (isMd(template.path)){
|
||||
mdTemplates.push(parseMdTemplate(decode(template.content || ''), name || '', template));
|
||||
} else if (isYaml(template.path)) {
|
||||
const content = validateSchema(decode(template.content || '')) as ValidationsSchemaOutput;
|
||||
content.errors?.length === 0 && formatTemplates.push(parseYamlTemplate(content, name || '', template));
|
||||
}
|
||||
})
|
||||
isUsedEmptyIssueTemplate.value = blank_issues_enabled;
|
||||
issueTemplates.value = [...issueTemplates.value, ...formatTemplates, ...mdTemplates, ...configTemplates];
|
||||
const isIssueChoose:IssueLink = issueTemplates.value.length > 0 ? 'repoIssueCreateChoose' : 'repoIssueCreate';
|
||||
setIssueLink(isIssueChoose)
|
||||
createTemplates();
|
||||
}
|
||||
const createTemplates = ()=> {
|
||||
isUsedEmptyIssueTemplate.value && issueTemplates.value.push({
|
||||
template: '',
|
||||
name: '空白 Issue',
|
||||
description: '没有找到合适的 Issue 模板?可以通过非模板 Issue 向我们反馈你的问题',
|
||||
type: 'blank',
|
||||
url: 'issues/create'
|
||||
})
|
||||
}
|
||||
const parsePrivateIssue = (issue:any[], privateIssue?:string)=> {
|
||||
const isPrivateIssue = issue.some((item:any)=> item.key === 'SECURITY' && item.value === '1');
|
||||
if (isPrivateIssue) {
|
||||
issueTemplates.value.push({
|
||||
template: '',
|
||||
name: '安全漏洞',
|
||||
description: '向我们报告代码安全漏洞和隐私泄漏等敏感信息,促进项目的安全性和可靠性',
|
||||
type: 'security',
|
||||
url: 'issues/create'
|
||||
});
|
||||
}
|
||||
}
|
||||
const getIssueInfo = async (project_id:string)=> {
|
||||
issueLoading.value = true;
|
||||
let { data } = await reqCatch(getSettingsValues, {repoId: project_id as string}) || {};
|
||||
if (!data.error) {
|
||||
parsePrivateIssue(data.data?.data?.modules || [])
|
||||
if (issueTemplates.value.length > 0) {
|
||||
issueLoading.value = false;
|
||||
createTemplates();
|
||||
setIssueLink('repoIssueCreateChoose')
|
||||
return;
|
||||
}
|
||||
}
|
||||
return Promise.all([fetchIssueTemplateList(project_id)]).then((res)=> {
|
||||
const [issueInfo] = res;
|
||||
if (!issueInfo.error_code) {
|
||||
parseIssueTemplates(issueInfo.data || []);
|
||||
}
|
||||
}).finally(()=> {
|
||||
issueLoading.value = false;
|
||||
})
|
||||
|
||||
};
|
||||
const getAllIssuesInfo = async (project_id:string)=> {
|
||||
issueLoading.value = true;
|
||||
return Promise.all([getSettingsValues({repoId: project_id as string}), fetchIssueTemplateList(project_id)]).then((res)=> {
|
||||
const [settings ,issueInfo] = res;
|
||||
if (!settings.error_code) {
|
||||
parsePrivateIssue(settings.data?.data?.modules || [])
|
||||
}
|
||||
if (!issueInfo.error_code) {
|
||||
parseIssueTemplates(issueInfo.data || []);
|
||||
}
|
||||
}).finally(()=> {
|
||||
const hasBlankTemplate = issueTemplates.value.find(item => item.type === 'blank')
|
||||
const findEmpty = issueTemplates.value.length <= 1 && !hasBlankTemplate;
|
||||
if (findEmpty) {
|
||||
createTemplates();
|
||||
}
|
||||
issueLoading.value = false;
|
||||
})
|
||||
|
||||
};
|
||||
return {
|
||||
issueTemplates,
|
||||
issueLink,
|
||||
setIssueLink,
|
||||
getIssueInfo,
|
||||
isUsedEmptyIssueTemplate,
|
||||
issueLoading,
|
||||
getAllIssuesInfo
|
||||
}
|
||||
}
|
||||
43
src/utils/hooks/useLazy.ts
Normal file
43
src/utils/hooks/useLazy.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Component, DefineComponent } from 'vue';
|
||||
import { defineAsyncComponent, defineComponent, h } from 'vue';
|
||||
import { LoadingService } from 'vue-devui/loading';
|
||||
import { Message } from 'vue-devui/message';
|
||||
|
||||
export interface Fn<T = any, R = T> {
|
||||
(...arg: T[]): R
|
||||
}
|
||||
|
||||
const LoadingComponents = defineComponent({
|
||||
props: {
|
||||
delay: {
|
||||
type: Number,
|
||||
default: () => 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const LoadingComp = LoadingService.open({
|
||||
message: '加载中',
|
||||
backdrop: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
(LoadingComp?.loadingInstance as any)?.close();
|
||||
}, props.delay);
|
||||
}
|
||||
});
|
||||
|
||||
export function useLazyImport<T>(loader: Fn,
|
||||
options: { delay?: number; timeout?: number; loadingTime?: number; showLoading?: boolean } = {},
|
||||
LoadingComponent: Component = LoadingComponents
|
||||
): DefineComponent<T> {
|
||||
const { delay = 100, timeout = 2000, loadingTime = 200, showLoading = false } = options;
|
||||
return defineAsyncComponent({
|
||||
loader,
|
||||
loadingComponent: showLoading ? h(LoadingComponent, { delay: loadingTime }) : h('fragment', ''),
|
||||
timeout, // 如果load加载的时间超出了timeout会直接展示error组件指导被resolve或者直接被reject后
|
||||
delay, // delay时间一定需要比timeout以及组件加载消耗的时间短
|
||||
onError: (_, retry, fail) => {
|
||||
Message.error('加载失败');
|
||||
fail();
|
||||
}
|
||||
});
|
||||
}
|
||||
79
src/utils/hooks/useLogin.ts
Normal file
79
src/utils/hooks/useLogin.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { usePopup } from './usePopup';
|
||||
import LoginModal from '@/components/LoginModal/index.vue';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import { useAccount } from './useAccount';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import type { AccountInfo } from '@/stores/user';
|
||||
import isPhone from '@/utils/isPhone';
|
||||
|
||||
export interface LoginOptions {
|
||||
Authorization?: boolean,
|
||||
type?: 'login' | 'register'
|
||||
[x: string]: any
|
||||
triggerType?: string
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
const { mount, unMount, isMounted, closeModal, refreshModal } = usePopup();
|
||||
const { RecordInfo } = useAccount();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
/** 传入登录弹窗的属性配置
|
||||
* @param Authorization @default true
|
||||
* @description Authorization 如果登录的用户具备权限登录完成则继续留在当前页面, 否则登录完成立即跳转
|
||||
* @param type @default 'login'
|
||||
* @description type login为登录窗口 register为注册窗口
|
||||
*/
|
||||
const login = (options: LoginOptions = { Authorization: true, type: 'login' }) => {
|
||||
const { Authorization = true, type = 'login', triggerType = '' } = options;
|
||||
if (isMounted()) { /** 同一tick只允许弹出一次 */
|
||||
return;
|
||||
}
|
||||
if (isPhone()) {
|
||||
// 保存注册上报类型
|
||||
sessionStorage.setItem('loginType', triggerType || '');
|
||||
router.replace(`/${type}?returnUrl=${location.href}`);
|
||||
} else {
|
||||
closeModal();
|
||||
route.name !== 'oauth' && mount(LoginModal, {
|
||||
modelValue: true,
|
||||
defaultType: type,
|
||||
triggerType: triggerType,
|
||||
onLogin: async(conf: Required<AccountInfo>) => {
|
||||
unMount();
|
||||
const { username, email } = conf;
|
||||
RecordInfo(conf);
|
||||
Message.success({
|
||||
message: '欢迎来到GitCode!'
|
||||
});
|
||||
if (`${username}@gitcode.com` === email) { // 需要修改默认邮箱的弹窗
|
||||
localStorage.setItem('validator_email', 'invalid');
|
||||
} else {
|
||||
localStorage.setItem('validator_email', 'valid');
|
||||
}
|
||||
router.go(0);
|
||||
},
|
||||
onLink: (path: string) => {
|
||||
unMount();
|
||||
router.replace(path);
|
||||
},
|
||||
onClose: (refresh: boolean = false) => { // 多网页刷新状态
|
||||
unMount();
|
||||
if (refresh) {
|
||||
router.go(0);
|
||||
} else if (!Authorization) {
|
||||
// router.replace({ name: 'home' });
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
refreshModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return {
|
||||
login,
|
||||
unMount,
|
||||
isMounted
|
||||
};
|
||||
}
|
||||
19
src/utils/hooks/useLoginCheck.ts
Normal file
19
src/utils/hooks/useLoginCheck.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useAccountStore } from '@/stores/user';
|
||||
import { emitEvent } from '@/utils/eventBus';
|
||||
|
||||
/**
|
||||
* 验证登录转态,可触发登录操作
|
||||
* @returns
|
||||
*/
|
||||
export function useLoginCheck() {
|
||||
const account = useAccountStore();
|
||||
const loginCheck = (triggerType?: string) => {
|
||||
if (!account.isLogin) {
|
||||
emitEvent('login', { triggerType });
|
||||
}
|
||||
return account.isLogin;
|
||||
};
|
||||
return {
|
||||
loginCheck
|
||||
};
|
||||
}
|
||||
18
src/utils/hooks/useModel.ts
Normal file
18
src/utils/hooks/useModel.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { SetupContext } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
export function useModel(props: any, emits: SetupContext<{
|
||||
'update:modelValue': (val: any) => void
|
||||
}>['emit']) {
|
||||
const vModels = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
emits('update:modelValue', val);
|
||||
}
|
||||
});
|
||||
return {
|
||||
vModels
|
||||
};
|
||||
}
|
||||
25
src/utils/hooks/useNav.ts
Normal file
25
src/utils/hooks/useNav.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useRouter } from 'vue-router';
|
||||
import { checkUsername } from '@/api/user';
|
||||
export const useNav = () => {
|
||||
const router = useRouter();
|
||||
|
||||
async function naviTo(name: string, params?: any) {
|
||||
if (params && params.namespace) {
|
||||
let isCheckUser = true;
|
||||
const res = await checkUsername(params.namespace);
|
||||
if (!res.error) {
|
||||
if (res.data.data.result === false) {
|
||||
isCheckUser = false;
|
||||
}
|
||||
} else {
|
||||
isCheckUser = false;
|
||||
}
|
||||
if (!isCheckUser) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
router.push({ name, params });
|
||||
}
|
||||
|
||||
return { router, naviTo };
|
||||
};
|
||||
32
src/utils/hooks/useNotice.ts
Normal file
32
src/utils/hooks/useNotice.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import NoticeModal from '@/components/LoginModal/widget/notice.vue';
|
||||
import { usePopup } from './usePopup';
|
||||
import { useRouter } from 'vue-router';
|
||||
import isPhone from '@/utils/isPhone';
|
||||
|
||||
function clearNotice() {
|
||||
if (localStorage.getItem('validator_email')) {
|
||||
localStorage.removeItem('validator_email');
|
||||
}
|
||||
}
|
||||
|
||||
export function useNotification() {
|
||||
const { mount, unMount } = usePopup(isPhone() ? 'global-notification global-notification-mobile' : 'global-notification global-notification-pc', document.body);
|
||||
const router = useRouter();
|
||||
const notice = () => {
|
||||
mount(NoticeModal, {
|
||||
modelValue: true,
|
||||
onClose: () => {
|
||||
clearNotice();
|
||||
unMount();
|
||||
},
|
||||
onConfirm: () => {
|
||||
clearNotice();
|
||||
unMount();
|
||||
router.push('/setting/email');
|
||||
}
|
||||
});
|
||||
};
|
||||
return {
|
||||
notice
|
||||
};
|
||||
}
|
||||
14
src/utils/hooks/useOrgId.ts
Normal file
14
src/utils/hooks/useOrgId.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { ref, toValue, watchEffect, watch, computed } from 'vue';
|
||||
import router from '@/router';
|
||||
|
||||
export const useOrgId = (connector: string = '%2F', namespace: string | string[] = '') => {
|
||||
const orgId = computed(() => {
|
||||
const routeNamespace = namespace || router.currentRoute.value.params.namespace;
|
||||
if (!routeNamespace) return null;
|
||||
const orgPath = Array.isArray(routeNamespace)
|
||||
? routeNamespace.join(connector)
|
||||
: routeNamespace.split('/').join(connector);
|
||||
return orgPath;
|
||||
});
|
||||
return { orgId };
|
||||
};
|
||||
35
src/utils/hooks/usePageResize.ts
Normal file
35
src/utils/hooks/usePageResize.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { computed } from 'vue';
|
||||
import { useWindowSize } from '@vueuse/core';
|
||||
|
||||
const { width } = useWindowSize();
|
||||
|
||||
/**
|
||||
* 计算布局长度的代称
|
||||
* @returns
|
||||
*/
|
||||
export const usePageResize = () => {
|
||||
const widthConfig = {
|
||||
xxl: 1536,
|
||||
xl: 1280,
|
||||
md: 1024,
|
||||
lg: 768
|
||||
};
|
||||
const widthType = computed(() => {
|
||||
if (width.value > widthConfig.xxl) {
|
||||
return 'xxl';
|
||||
} else if (width.value <= widthConfig.xxl && width.value > widthConfig.xl) {
|
||||
return 'xl';
|
||||
} else if (width.value <= widthConfig.xl && width.value > widthConfig.md) {
|
||||
return 'md';
|
||||
} else if (width.value <= widthConfig.md && width.value > widthConfig.lg) {
|
||||
return 'lg';
|
||||
} else {
|
||||
return 'sm';
|
||||
}
|
||||
});
|
||||
const isMobile = computed(() => {
|
||||
return width.value <= widthConfig['md'];
|
||||
});
|
||||
|
||||
return { widthType, width, widthConfig, isMobile };
|
||||
};
|
||||
63
src/utils/hooks/usePagination.ts
Normal file
63
src/utils/hooks/usePagination.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { readonly, reactive, watch } from 'vue';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* @param storageKey 需要保存到 localStorage 设置的key
|
||||
*/
|
||||
interface IProsp {
|
||||
storageKey?: string;
|
||||
clientType?: 'pc'|'h5';
|
||||
}
|
||||
|
||||
export const usePagination = ({
|
||||
storageKey,
|
||||
clientType
|
||||
}:IProsp) => {
|
||||
const localPager = useSessionStorage('pager', { [storageKey + '']: 10 });
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: localPager.value[storageKey + ''] || 10,
|
||||
total: 0,
|
||||
pageCount: 0,
|
||||
loading: false,
|
||||
isFirstLoad: true, // 骨架屏和loading不能同时出现 所以需要有判断是否首次加载,当请求执行完之后置为false即可
|
||||
showLoading: false, // 有了首次状态之后loading这个属性已经不能用于直接控制是否显示loading了,增加一个属性来进行综合处理。
|
||||
showEmpty: false, // 是否显示为空 初始显示为骨架所以不为true
|
||||
});
|
||||
|
||||
const pageOptions = readonly({
|
||||
maxItems: 5,
|
||||
pageSizeOptions: [10, 20, 50],
|
||||
canJumpPage: false,
|
||||
totalItemText: '所有条目',
|
||||
canViewTotal: clientType !== 'h5',
|
||||
canChangePageSize: clientType !== 'h5',
|
||||
autoHide: true
|
||||
});
|
||||
|
||||
watch(() => pager.pageSize, (value) => {
|
||||
if (storageKey) {
|
||||
localPager.value[storageKey + ''] = value;
|
||||
}
|
||||
});
|
||||
// 增加监听 初次加载显示:骨架——数据
|
||||
// 切换时 如果总条数为0:骨架——数据
|
||||
// 其他情况 loading——数据
|
||||
watch(() => pager.loading, (value) => { // loading的变换过程为true——请求结束——false
|
||||
if (value) { // false —— true
|
||||
if (pager.isFirstLoad || pager.total === 0) { // 初次加载或者是表格原数量为0 使用骨架屏显示
|
||||
pager.showLoading = false // 首次加载不使用loading效果
|
||||
pager.isFirstLoad = true
|
||||
pager.showEmpty = false
|
||||
return false
|
||||
}
|
||||
pager.showLoading = true
|
||||
}
|
||||
if (!value) { // true —— false
|
||||
pager.showLoading = false
|
||||
pager.isFirstLoad = false
|
||||
pager.showEmpty = pager.total === 0
|
||||
}
|
||||
});
|
||||
return { pager, pageOptions };
|
||||
};
|
||||
58
src/utils/hooks/usePopup.ts
Normal file
58
src/utils/hooks/usePopup.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { h, render } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
export function usePopup(className?: string, rootElement?: HTMLElement) {
|
||||
const root = rootElement || document.getElementById('app') as HTMLElement;
|
||||
let cacheClass = 'popup-container';
|
||||
if (className) {
|
||||
cacheClass = className;
|
||||
};
|
||||
function mount<U>(component: Component, options: U = {} as U) {
|
||||
const el = document.createElement('div');
|
||||
el.className = cacheClass;
|
||||
root.appendChild(el);
|
||||
const vnode = h(component, Object.assign({
|
||||
onClose: () => {
|
||||
unMount();
|
||||
}
|
||||
}, options));
|
||||
render(vnode, el);
|
||||
}
|
||||
function unMount() {
|
||||
let firstClassName = '';
|
||||
if (cacheClass.split(' ').length) {
|
||||
firstClassName = cacheClass.split(' ')[0];
|
||||
}
|
||||
const el = root.querySelector(`.${firstClassName}`);
|
||||
if (el) { root.removeChild(el); };
|
||||
}
|
||||
function isMounted() {
|
||||
let firstClassName = '';
|
||||
if (cacheClass.split(' ').length) {
|
||||
firstClassName = cacheClass.split(' ')[0];
|
||||
}
|
||||
const el = root.querySelector(`.${firstClassName}`);
|
||||
return Boolean(el);
|
||||
}
|
||||
/** 关闭devui的modal弹窗 */
|
||||
function closeModal() {
|
||||
const el = document.body.querySelector('.devui-modal');
|
||||
const mask = document.body.querySelector('.devui-modal__overlay');
|
||||
if (el) { (el as HTMLElement).style.display = 'none'; }
|
||||
if (mask) { (mask as HTMLElement).style.display = 'none'; }
|
||||
}
|
||||
/** 恢复devui的modal弹窗 */
|
||||
function refreshModal() {
|
||||
const el = document.body.querySelector('.devui-modal');
|
||||
const mask = document.body.querySelector('.devui-modal__overlay');
|
||||
if (el) { (el as HTMLElement).style.display = 'block'; }
|
||||
if (mask) { (mask as HTMLElement).style.display = 'block'; }
|
||||
}
|
||||
return {
|
||||
mount,
|
||||
isMounted,
|
||||
unMount,
|
||||
closeModal,
|
||||
refreshModal
|
||||
};
|
||||
}
|
||||
168
src/utils/hooks/useRepoHeaderInit.ts
Normal file
168
src/utils/hooks/useRepoHeaderInit.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { useRepoId } from '@/utils/hooks/useRepoId';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { updateRepoNotice, getRepoNotice, getRepoTopic } from '@/api/repo';
|
||||
import { repoInfoStore } from '@/stores/Repo';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export const eventsTranslate = (events:any[]) => {
|
||||
events = events?.filter((val) => val.action_name && !val.filter_sensitive) || [];
|
||||
const result:any[] = events.map(({ title, ...others }) => ({
|
||||
...others,
|
||||
title: typeof title === 'string' && /^[\[\{/]/.test(title) ? JSON.parse(title) : title
|
||||
}));
|
||||
return result;
|
||||
};
|
||||
|
||||
const getAttr = (obj:object, str:string):any => {
|
||||
const keyArr:string[] = str.split('.');
|
||||
let result = obj;
|
||||
for (let i = 0; i < keyArr.length; ++i) {
|
||||
if (result) result = result[keyArr[i] as keyof typeof result];
|
||||
else return null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useRepoHeaderInit = (isLogin: boolean) => {
|
||||
const { repoId } = useRepoId();
|
||||
const repoInfo = reactive<any>({
|
||||
name: '',
|
||||
visibility: '',
|
||||
readme_url: '',
|
||||
tag_count: 0,
|
||||
watch_count: 0,
|
||||
forks_count: 0,
|
||||
star_count: 0,
|
||||
branch_count: 0,
|
||||
open_merge_requests_count: 0,
|
||||
http_url_to_repo: '',
|
||||
ssh_url_to_repo: '',
|
||||
description: '',
|
||||
tag_list: [],
|
||||
web_url: '',
|
||||
empty_repo: false,
|
||||
default_branch: '',
|
||||
namespace: {}
|
||||
});
|
||||
const loadingStatus = reactive({ // 加载状态
|
||||
profileLoading: true,
|
||||
readmeLoading: true,
|
||||
eventsLoading: true,
|
||||
contributorLoading: true,
|
||||
releasesLoading: true,
|
||||
processLoading: true
|
||||
});
|
||||
const linkList = [// 此处调整需要相应调整下面的keyMap字段映射
|
||||
{ icon: 'gt-line-OSLicense', text: '', linkName: 'repoFile', params: { branchName: '', filePath: 'LICENSE' }, name: '协议' },
|
||||
{ icon: 'gt-line-language', text: '', name: '', linkName: 'repoDir', params: { branchName: '' }},
|
||||
{ icon: 'gt-line-star', text: 0, name: '关注', linkName: 'repoStar', suffix: 'Stars' },
|
||||
{ icon: 'gt-line-branches', text: 0, name: '分支数', linkName: 'repoBranch', suffix: '分支' },
|
||||
{ icon: 'gt-line-tag', text: 0, name: '标签', linkName: 'repoTags', suffix: 'Tags' },
|
||||
{ icon: 'gt-line-commit', text: 0, name: '提交', linkName: 'repoCommitByBranch', suffix: '提交', params: { branchName: '' }},
|
||||
{ icon: 'gt-line-fork', text: 0, name: 'Fork', linkName: 'repoFork', suffix: 'Forks' }
|
||||
];
|
||||
const noticeStatus = ref<string>('');
|
||||
const validLinks = ref<any[]>([]);
|
||||
const keyMap = ['license.key', 'main_repository_language[0]', 'star_count', 'branch_count', 'tag_count', 'statistics.commit_count', 'forks_count'];// 简介字段映射
|
||||
const watchCount = ref(0);
|
||||
const topicData = reactive({
|
||||
userTopicList: [],
|
||||
officialTopicList: []
|
||||
});
|
||||
|
||||
const store = repoInfoStore();
|
||||
const initRepoData = async() => {
|
||||
Object.assign(repoInfo, store.repoInfo);
|
||||
|
||||
loadingStatus.profileLoading = false;
|
||||
|
||||
const linkArr:any[] = [];
|
||||
linkList.forEach((elem, idx) => {
|
||||
const obj = cloneDeep(elem);
|
||||
let val = get(repoInfo, keyMap[idx]);
|
||||
// 将string类型数字的转为number
|
||||
if (/^\d+$/.test(val)) {
|
||||
val = parseInt(val);
|
||||
}
|
||||
if (val) {
|
||||
if (obj.name !== 'readme文件') obj.text = val;
|
||||
if (typeof obj.params?.branchName !== 'undefined') obj.params.branchName = repoInfo.default_branch;
|
||||
linkArr.push(obj);
|
||||
}
|
||||
});
|
||||
validLinks.value = linkArr;
|
||||
};
|
||||
|
||||
const initNotice = async() => {
|
||||
if (!isLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoNoticeRes = await getRepoNotice({ repoId: repoId.value as string });
|
||||
if (repoNoticeRes.data) {
|
||||
const res = repoNoticeRes.data.data;
|
||||
noticeStatus.value = res.watch_type;
|
||||
watchCount.value = res.count;
|
||||
}
|
||||
};
|
||||
|
||||
const initTopic = async() => {
|
||||
const res = await getRepoTopic(repoId.value as string);
|
||||
if (!res.error) {
|
||||
const userTopicList = [] as any;
|
||||
const officialTopicList = [] as any;
|
||||
res.data.data.forEach(item => {
|
||||
item.name && (item.suffix = '# ' + item.name);
|
||||
// item.linkUrl = `/topic/${item.name}?type=${item.type}`;
|
||||
if (item.type === 1) {
|
||||
userTopicList.push(item);
|
||||
} else {
|
||||
officialTopicList.push(item);
|
||||
}
|
||||
});
|
||||
topicData.officialTopicList = officialTopicList;
|
||||
topicData.userTopicList = userTopicList;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化header数据
|
||||
const initRepoHeader = async() => {
|
||||
// 项目信息
|
||||
initRepoData();
|
||||
|
||||
await initNotice();
|
||||
};
|
||||
// 初始repo首页数据
|
||||
const initRepoDashboard = async() => {
|
||||
initRepoData();
|
||||
|
||||
initTopic(); // 获取首页topic
|
||||
};
|
||||
|
||||
const updateNotice = async(val:string) => { // 更新通知状态
|
||||
const res = await updateRepoNotice({ watch_type: val, repoId: repoId.value });
|
||||
if (res?.data) {
|
||||
Message({ type: 'success', message: '修改成功' });
|
||||
watchCount.value = res.data?.data?.count || 0;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => store.repoInfo, () => {
|
||||
initRepoData();
|
||||
}, { deep: true });
|
||||
|
||||
return {
|
||||
repoId,
|
||||
watchCount,
|
||||
loadingStatus,
|
||||
noticeStatus,
|
||||
repoInfo,
|
||||
validLinks,
|
||||
topicData,
|
||||
initRepoHeader,
|
||||
initRepoDashboard,
|
||||
updateNotice
|
||||
};
|
||||
};
|
||||
20
src/utils/hooks/useRepoId.ts
Normal file
20
src/utils/hooks/useRepoId.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ref, toValue, watchEffect, watch, computed } from 'vue';
|
||||
import router from '@/router';
|
||||
|
||||
export const useRepoId = (
|
||||
connector: string = '%2F',
|
||||
namespace: string | string[] = '',
|
||||
routeName: string | string[] = ''
|
||||
) => {
|
||||
const repoId = computed(() => {
|
||||
const routeParams = router.currentRoute.value.params;
|
||||
let routeNamespace = namespace || routeParams.namespace;
|
||||
const routeRepoName = routeName || routeParams.repoName;
|
||||
if (!routeNamespace || !routeRepoName) return null;
|
||||
routeNamespace = Array.isArray(routeNamespace)
|
||||
? routeNamespace.join(connector)
|
||||
: routeNamespace.split('/').join(connector);
|
||||
return `${routeNamespace}${connector}${routeRepoName}`;
|
||||
});
|
||||
return { repoId };
|
||||
};
|
||||
354
src/utils/hooks/useRepoInit.ts
Normal file
354
src/utils/hooks/useRepoInit.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import utf8 from 'crypto-js/enc-utf8';
|
||||
import { useRepoId } from '@/utils/hooks/useRepoId';
|
||||
import Base64 from 'crypto-js/enc-base64';
|
||||
import { getReleases } from '@/api/release';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { updateRepoNotice, getRepoEvents, getRepoReadme, getRepoContributors, getRepoNotice, getRepoTopic } from '@/api/repo';
|
||||
import { repoInfoStore } from '@/stores/Repo';
|
||||
import { useAccountStore } from '@/stores/user';
|
||||
|
||||
const { isLogin } = useAccountStore();
|
||||
|
||||
export const eventsTranslate = (events:any[]) => {
|
||||
events = events?.filter((val) => val.action_name && !val.filter_sensitive) || [];
|
||||
const result:any[] = events.map(({ title, ...others }) => ({
|
||||
...others,
|
||||
title: typeof title === 'string' && /^[\[\{/]/.test(title) ? JSON.parse(title) : title
|
||||
}));
|
||||
return result;
|
||||
};
|
||||
|
||||
const getAttr = (obj:object, str:string):any => {
|
||||
const keyArr:string[] = str.split('.');
|
||||
let result = obj;
|
||||
for (let i = 0; i < keyArr.length; ++i) {
|
||||
if (result) result = result[keyArr[i] as keyof typeof result];
|
||||
else return null;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useRepoInit = () => {
|
||||
const { repoId } = useRepoId();
|
||||
const repoInfo = reactive<any>({
|
||||
name: '',
|
||||
visibility: '',
|
||||
readme_url: '',
|
||||
tag_count: 0,
|
||||
watch_count: 0,
|
||||
forks_count: 0,
|
||||
star_count: 0,
|
||||
branch_count: 0,
|
||||
open_merge_requests_count: 0,
|
||||
http_url_to_repo: '',
|
||||
ssh_url_to_repo: '',
|
||||
description: '',
|
||||
tag_list: [],
|
||||
web_url: '',
|
||||
empty_repo: false,
|
||||
default_branch: '',
|
||||
namespace: {}
|
||||
});
|
||||
const loadMoreConfig = reactive({
|
||||
loadMore: false,
|
||||
loadMoreText: '查看更多'
|
||||
});
|
||||
const loadingStatus = reactive({ // 加载状态
|
||||
profileLoading: true,
|
||||
readmeLoading: true,
|
||||
eventsLoading: true,
|
||||
contributorLoading: true,
|
||||
releasesLoading: true,
|
||||
processLoading: true
|
||||
});
|
||||
const showReadme = ref<boolean>(false);// 是否展示readme
|
||||
const linkList = [// 此处调整需要相应调整下面的keyMap字段映射
|
||||
{ icon: 'gt-file', text: 'README', linkName: 'repoFile', params: { branchName: '', filePath: 'README.md' }, name: 'readme文件' },
|
||||
{ icon: 'gt-license', text: '', linkName: 'repoFile', params: { branchName: '', filePath: 'LICENSE' }, name: '协议' },
|
||||
{ icon: 'gt-star', text: 0, name: '关注', linkName: 'repoStar', suffix: 'stars' },
|
||||
{ icon: 'gt-branches', text: 0, name: '分支数', linkName: 'repoBranch', suffix: 'branches' },
|
||||
{ icon: 'gt-tag', text: 0, name: '标签', linkName: 'repoTags', suffix: 'tags' },
|
||||
{ icon: 'gt-commit', text: 0, name: '提交', linkName: 'repoMerge', suffix: 'merge' },
|
||||
{ icon: 'gt-remind', text: 0, name: '浏览', suffix: 'watching' },
|
||||
{ icon: 'gt-fork', text: 0, name: 'Fork', linkName: 'repoFork', suffix: 'forks' },
|
||||
{ icon: 'gt-connect', linkName: 'repoDir', params: { branchName: '' }, text: '', name: '链接' }
|
||||
];
|
||||
const contributorList = ref<any[]>([]);
|
||||
const releasesList = ref<any[]>([]);
|
||||
const timeData = ref<any[]>([]);
|
||||
const readmeText = ref<string>('');
|
||||
const isHasReadme = ref(false);
|
||||
const noticeStatus = ref<string>('');
|
||||
const validLinks = ref<any[]>([]);
|
||||
const keyMap = ['readme_url', 'license.key', 'star_count', 'branch_count', 'tag_count', 'open_merge_requests_count', 'watch_count', 'forks_count', 'web_url'];// 简介字段映射
|
||||
const eventPage = ref<number>(20);
|
||||
const watchCount = ref(0);
|
||||
const topicList = ref<any[]>([]);
|
||||
|
||||
async function initEvents() {
|
||||
loadingStatus.eventsLoading = true;
|
||||
|
||||
const eventRes = await getRepoEvents({ repoId: repoId.value as string, per_page: eventPage.value });
|
||||
if (eventRes.data) {
|
||||
const { events, has_next_page } = eventRes.data.data;
|
||||
timeData.value = eventsTranslate(events);
|
||||
loadMoreConfig.loadMore = has_next_page;
|
||||
}
|
||||
|
||||
loadingStatus.eventsLoading = false;
|
||||
}
|
||||
|
||||
const initRepoData = async() => {
|
||||
const store = repoInfoStore();
|
||||
Object.assign(repoInfo, store.repoInfo);
|
||||
|
||||
loadingStatus.profileLoading = false;
|
||||
|
||||
const linkArr:any[] = [];
|
||||
linkList.forEach((elem, idx) => {
|
||||
const obj = cloneDeep(elem);
|
||||
const val = getAttr(repoInfo, keyMap[idx]);
|
||||
if (val) {
|
||||
if (obj.name !== 'readme文件') obj.text = val;
|
||||
if (typeof obj.params?.branchName !== 'undefined') obj.params.branchName = repoInfo.default_branch;
|
||||
linkArr.push(obj);
|
||||
}
|
||||
});
|
||||
validLinks.value = linkArr;
|
||||
};
|
||||
|
||||
const initReadme = async() => {
|
||||
loadingStatus.readmeLoading = true;
|
||||
|
||||
const res = await getRepoReadme({ repoId: repoId.value as string });
|
||||
showReadme.value = false;
|
||||
isHasReadme.value = !!res.data?.data?.file_path;
|
||||
if (res.data?.data?.content) {
|
||||
showReadme.value = true;
|
||||
readmeText.value = utf8.stringify(Base64.parse(res.data.data.content));
|
||||
}
|
||||
loadingStatus.readmeLoading = false;
|
||||
};
|
||||
|
||||
const initContributors = async() => {
|
||||
loadingStatus.contributorLoading = true;
|
||||
|
||||
const contributorParam = { page: 1, per_page: 100, repoId: repoId.value };
|
||||
const contributorRes = await getRepoContributors(contributorParam);
|
||||
loadingStatus.contributorLoading = false;
|
||||
|
||||
if (contributorRes.data) {
|
||||
const { content } = contributorRes.data.data;
|
||||
contributorList.value = content;
|
||||
}
|
||||
};
|
||||
|
||||
const initRelease = async() => {
|
||||
loadingStatus.releasesLoading = true;
|
||||
const releaseRes = await getReleases({ repoId: repoId.value as string });
|
||||
loadingStatus.releasesLoading = false;
|
||||
if (releaseRes.data) {
|
||||
const { content } = releaseRes.data.data;
|
||||
releasesList.value = content;
|
||||
}
|
||||
};
|
||||
|
||||
const initNotice = async() => {
|
||||
if (!isLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repoNoticeRes = await getRepoNotice({ repoId: repoId.value as string });
|
||||
if (repoNoticeRes.data) {
|
||||
const res = repoNoticeRes.data.data;
|
||||
noticeStatus.value = res.watch_type;
|
||||
watchCount.value = res.count;
|
||||
}
|
||||
};
|
||||
|
||||
const initTopic = async() => {
|
||||
const res = await getRepoTopic(repoId.value as string);
|
||||
if (!res.error) {
|
||||
topicList.value = [...res.data.data];
|
||||
} else {
|
||||
topicList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化header数据
|
||||
const initRepoHeader = async() => {
|
||||
// 项目信息
|
||||
initRepoData();
|
||||
|
||||
await initNotice();
|
||||
};
|
||||
// 初始repo首页数据
|
||||
const initRepoDashboard = () => {
|
||||
initRepoData();
|
||||
|
||||
initEvents(); // 获取项目动态信息
|
||||
initReadme(); // 项目readme文件
|
||||
initContributors();
|
||||
initRelease();
|
||||
// initTopic(); // 获取首页topic
|
||||
};
|
||||
|
||||
const onLoadMore = () => {
|
||||
eventPage.value += 10;
|
||||
initEvents();
|
||||
};
|
||||
|
||||
watch(() => repoId, () => {
|
||||
eventPage.value = 20;// 页码重置
|
||||
});
|
||||
|
||||
const updateNotice = async(val:string) => { // 更新通知状态
|
||||
const res = await updateRepoNotice({ watch_type: val, repoId: repoId.value });
|
||||
if (res?.data) {
|
||||
Message({ type: 'success', message: '修改成功' });
|
||||
watchCount.value = res.data?.data?.count || 0;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
repoId,
|
||||
watchCount,
|
||||
showReadme,
|
||||
loadingStatus,
|
||||
noticeStatus,
|
||||
contributorList,
|
||||
repoInfo,
|
||||
timeData,
|
||||
readmeText,
|
||||
isHasReadme,
|
||||
validLinks,
|
||||
releasesList,
|
||||
loadMoreConfig,
|
||||
topicList,
|
||||
initRepoHeader,
|
||||
initRepoDashboard,
|
||||
updateNotice,
|
||||
onLoadMore
|
||||
};
|
||||
};
|
||||
|
||||
export function useAssets() {
|
||||
const iconPaper = new URL('@/assets/imgs/icon/icon-paper.svg', import.meta.url).href;
|
||||
const iconPen = new URL('@/assets/imgs/icon/icon-pen.svg', import.meta.url).href;
|
||||
return { iconPaper, iconPen };
|
||||
}
|
||||
|
||||
export const numRend = (num:number, suffix = '个') => num ? `${num}${suffix}` : '';
|
||||
|
||||
export const eventsDic = { // 事件动态的字典
|
||||
$: 'target_type',
|
||||
MergeRequest: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}Pull Request`,
|
||||
opened: (num = 0) => `重新打开了${numRend(num)}Pull Request`,
|
||||
accepted: (num = 0) => `合入了${numRend(num)}Pull Request`,
|
||||
closed: (num = 0) => `关闭了${numRend(num)}Pull Request`
|
||||
},
|
||||
Issue: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}Issue`,
|
||||
opened: (num = 0) => `重新打开了${numRend(num)}Issue`,
|
||||
closed: (num = 0) => `关闭了${numRend(num)}Issue`
|
||||
},
|
||||
Label: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}Label`,
|
||||
changed: (num = 0) => `修改了${numRend(num)}Label`,
|
||||
destroyed: (num = 0) => `删除了${numRend(num)}Label`,
|
||||
imported: (num = 0) => `导入了${numRend(num)}Label`
|
||||
},
|
||||
Note: {
|
||||
$: 'action_name',
|
||||
'commented on': {
|
||||
$: 'note.noteable_type',
|
||||
MergeRequest: {
|
||||
$: 'note.type',
|
||||
Note: (num = 0) => `评论了${numRend(num)}Pull Request`,
|
||||
DiscussionNote: '对Pull Request提评审意见',
|
||||
DiffNote: '对合并提交代码评审意见',
|
||||
'null': (num = 0) => `评论了${numRend(num)}Pull Request`
|
||||
},
|
||||
Issue: {
|
||||
$: 'note.type',
|
||||
DiscussionNote: (num = 0) => `评论了${numRend(num)}Issue`,
|
||||
'null': (num = 0) => `评论了${numRend(num)}Issue`
|
||||
},
|
||||
Commit: {
|
||||
$: 'note.type',
|
||||
DiffNote: (num = 0) => `对commit提交了${numRend(num)}代码评审意见`,
|
||||
DiscussionNote: (num = 0) => `评论了${numRend(num)}Commit`,
|
||||
'null': (num = 0) => `评论了${numRend(num)}Commit`
|
||||
},
|
||||
'null': (num = 0) => `删除了${numRend(num)}评论`
|
||||
}
|
||||
},
|
||||
Milestone: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}里程碑`,
|
||||
closed: (num = 0) => `关闭了${numRend(num)}里程碑`,
|
||||
opened: (num = 0) => `重新打开了${numRend(num)}里程碑`, // may not be use now
|
||||
destroyed: (num = 0) => `删除了${numRend(num)}里程碑`
|
||||
},
|
||||
Project: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}项目`,
|
||||
remove: (num = 0) => `删除了${numRend(num)}项目`,
|
||||
changed: (num = 0) => `修改了${numRend(num)}项目`,
|
||||
transfer: (num = 0) => `转移了${numRend(num)}项目`,
|
||||
'null': () => '修改仓库'
|
||||
},
|
||||
Group: {
|
||||
$: 'action_name',
|
||||
created: (num = 0) => `创建了${numRend(num)}组织`,
|
||||
destroyed: (num = 0) => `删除了${numRend(num)}组织`
|
||||
},
|
||||
'null': {
|
||||
$: 'action_name',
|
||||
'pushed to': {
|
||||
$: 'title.force_push',
|
||||
'false': () => '推送代码',
|
||||
'true': () => '强制推送代码',
|
||||
'null': () => '推送代码'
|
||||
},
|
||||
'pushed new': {
|
||||
$: 'push_data.ref_type',
|
||||
branch: (num = 0) => `新建了${numRend(num)}分支`,
|
||||
tag: (num = 0) => `新建了${numRend(num)}Tag`
|
||||
},
|
||||
deleted: {
|
||||
$: 'push_data.ref_type',
|
||||
branch: (num = 0) => `删除了${numRend(num)}分支`,
|
||||
tag: (num = 0) => `删除了${numRend(num)}Tag`
|
||||
},
|
||||
created: (num = 0) => `创建了${numRend(num)}项目`,
|
||||
'batch delete branches': (num = 0) => `批量删除了${numRend(num)}分支`,
|
||||
joined: (num = 0) => `加入了${numRend(num)}组织或项目`,
|
||||
left: (num = 0) => `退出了${numRend(num)}组织或项目`,
|
||||
imported: (num = 0) => `导入了${numRend(num)}项目`,
|
||||
opened: () => '被邀请加入项目',
|
||||
'change member': () => '项目成员角色权限被调整',
|
||||
'removed due to membership expiration from': () => '由于有效期到期已自动退出项目'
|
||||
}
|
||||
};
|
||||
|
||||
export const mapJson = (mapper:any) => computed(() => (data:any, ...params:any[]):any => {
|
||||
let obj:any = mapper;
|
||||
const prt = new Map();
|
||||
for (;;) {
|
||||
const key = getAttr(data, obj['$']);
|
||||
obj = obj[key];
|
||||
if (typeof obj === 'string') return obj;
|
||||
if (typeof obj === 'function') return obj(...params);
|
||||
if (typeof obj === 'undefined' || prt.get(obj)) return null;// prevent not found or circular
|
||||
prt.set(obj, true);
|
||||
}
|
||||
});
|
||||
|
||||
export const rdEvent = mapJson(eventsDic);
|
||||
3
src/utils/hooks/useRepoPathValidate.ts
Normal file
3
src/utils/hooks/useRepoPathValidate.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function useRepoPathValidate() {
|
||||
|
||||
}
|
||||
10
src/utils/hooks/useReport.ts
Normal file
10
src/utils/hooks/useReport.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { report } from '@/api/report';
|
||||
|
||||
export const useReport = (eventID: string, eventParams: object, headers?: object) => {
|
||||
// 百度pv手动上报
|
||||
if (eventID === 'pageview' && window._hmt) {
|
||||
const path = window.location.pathname + window.location.search;
|
||||
window._hmt.push(['_trackPageview', path]);
|
||||
}
|
||||
return report(eventID, eventParams, headers);
|
||||
};
|
||||
44
src/utils/hooks/useReq.ts
Normal file
44
src/utils/hooks/useReq.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { escapeResData } from '@/utils/index';
|
||||
const alwaysTrue = () => true;
|
||||
|
||||
/**
|
||||
* fn:
|
||||
* params:
|
||||
* pre:
|
||||
* map:
|
||||
*/
|
||||
export const useAsync = (fn, params, precondition, map) => {
|
||||
precondition = precondition || alwaysTrue;
|
||||
const data = ref(null);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
const _ = ref(null);
|
||||
const mutate = () => {
|
||||
_.value = (+new Date());
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (!precondition()) return;
|
||||
loading.value = true;
|
||||
fn(({ _: _.value, ...params })).then(res => {
|
||||
data.value = map ? map(res) : res;
|
||||
})
|
||||
.catch(e => error.value = e)
|
||||
.finally(() => loading.value = false);
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
mutate
|
||||
};
|
||||
};
|
||||
|
||||
export const useReq = (fn, params, precondition?: Function, map?: Function) => useAsync(fn, params, precondition, !map ? escapeResData : x => map(escapeResData(x)));
|
||||
|
||||
export const reqLoading = (fn, params, loading) => new Promise((resolve, reject) => {
|
||||
loading.value = true;
|
||||
fn(params).then(resolve).catch(reject).finally(() => loading.value = false);
|
||||
});
|
||||
23
src/utils/hooks/useShowMore.ts
Normal file
23
src/utils/hooks/useShowMore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useResizeObserver } from '@vueuse/core';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export const useShowMore = (eleRef) => {
|
||||
const isOver = ref(false);
|
||||
const showMore = ref(false);
|
||||
|
||||
const checkRange = () => {
|
||||
if (!eleRef.value) return false;
|
||||
isOver.value = eleRef.value.scrollHeight > eleRef.value.clientHeight;
|
||||
};
|
||||
|
||||
const onShowMore = () => {
|
||||
showMore.value = true;
|
||||
stop();
|
||||
};
|
||||
|
||||
const { stop } = useResizeObserver(eleRef, () => {
|
||||
checkRange();
|
||||
});
|
||||
|
||||
return { stop, isOver, onShowMore, showMore };
|
||||
};
|
||||
20
src/utils/hooks/useTagMessage.ts
Normal file
20
src/utils/hooks/useTagMessage.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
const broadCast = new BroadcastChannel('tagMessage');
|
||||
|
||||
export function sendMsg(type:string, content:string) {
|
||||
broadCast.postMessage({
|
||||
type,
|
||||
content
|
||||
});
|
||||
};
|
||||
|
||||
export function listenMsg(callback:Function) {
|
||||
const handler = (e:any) => {
|
||||
callback && callback(e.data);
|
||||
};
|
||||
broadCast.addEventListener('message', handler);
|
||||
return () => {
|
||||
broadCast.removeEventListener('message', handler);
|
||||
broadCast.close();
|
||||
};
|
||||
};
|
||||
33
src/utils/hooks/useTimeFormat.ts
Normal file
33
src/utils/hooks/useTimeFormat.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import locale from 'dayjs/esm/locale/zh-cn';
|
||||
import tz from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
|
||||
dayjs.locale(locale);
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(tz);
|
||||
dayjs.extend(utc);
|
||||
|
||||
dayjs.tz.setDefault('Asia/Shanghai'); // 设置中国时区
|
||||
|
||||
type outputData = {
|
||||
// 将时间点转化成距离现在的时间差
|
||||
formatTimeFromNow: (time: string) => string;
|
||||
formatTime: (time: string, format: string) => string;
|
||||
}
|
||||
|
||||
export const useTimeFormat = (): outputData => {
|
||||
const formatTimeFromNow = (time: string): string => {
|
||||
if (!time) return '-';
|
||||
return dayjs(time).fromNow(); // .format('YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
const formatTime = (time: string, format: string): string => {
|
||||
if (!time) return '-';
|
||||
return dayjs(time).format(format);
|
||||
};
|
||||
return {
|
||||
formatTimeFromNow,
|
||||
formatTime
|
||||
};
|
||||
};
|
||||
16
src/utils/hooks/useTitle.ts
Normal file
16
src/utils/hooks/useTitle.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
import { useTitle } from '@vueuse/core';
|
||||
|
||||
const pageTitle = useTitle();
|
||||
const projectName = 'GitCode';
|
||||
|
||||
export const usePageTitle = (title?: string, name?:string) => {
|
||||
if (title && name) {
|
||||
// pageTitle.value = `${title} - ${name} - ${projectName}`;
|
||||
pageTitle.value = `${title}`;
|
||||
} else {
|
||||
// pageTitle.value = title ? `${title} - ${projectName}` : (name ? `${name} - ${projectName}` : '可信开源代码库');
|
||||
pageTitle.value = title ? `${title}` : (name ? `${name} - ${projectName}` : '可信开源代码库');
|
||||
}
|
||||
return pageTitle.value;
|
||||
};
|
||||
55
src/utils/hooks/useUserAccessLevel.ts
Normal file
55
src/utils/hooks/useUserAccessLevel.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
getOrgPermission,
|
||||
getRepoPermission
|
||||
} from '@/api/common';
|
||||
import setting from '@/setting';
|
||||
import { useUserInfo } from '@/utils/hooks/useUserInfo';
|
||||
|
||||
/**
|
||||
* 获取当前用户权限
|
||||
* @param type 组织 org | 项目 repo
|
||||
* @param targetId 组织或项目id
|
||||
*/
|
||||
export function useUserAccessLevel({ type = 'org', targetId = '' } = {}) {
|
||||
const { visitor, developer, admin } = setting.role;
|
||||
const access_level = ref(0); // 当前用户-项目角色权限,访问级别 管理员 50,开发 30,浏览者 10
|
||||
const { userInfo } = useUserInfo();
|
||||
|
||||
async function getPermission(id = '') {
|
||||
if (type === 'org') {
|
||||
// 组织权限查询
|
||||
const res = await getOrgPermission({ group_id: id });
|
||||
if (!res.error) {
|
||||
const resData = res?.data?.data;
|
||||
// 未登录|非组织成员,my_role 为 null
|
||||
if (resData.my_role && resData.my_role?.access_level) {
|
||||
// 已登录
|
||||
access_level.value = resData.my_role.access_level;
|
||||
}
|
||||
return resData;
|
||||
}
|
||||
} else {
|
||||
// 项目权限查询,根据当前用户token查询
|
||||
const res = await getRepoPermission({ repo_id: id });
|
||||
if (!res.error) {
|
||||
const resData = res?.data?.data;
|
||||
access_level.value = resData.access_level || 0;
|
||||
return resData;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (targetId) {
|
||||
userInfo.username && getPermission(targetId); // 登录后才调用接口
|
||||
}
|
||||
const isAdmin = computed(() => {
|
||||
return access_level?.value >= admin;
|
||||
});
|
||||
const isDeveloper = computed(() => {
|
||||
return access_level?.value >= developer;
|
||||
});
|
||||
const isVisitor = computed(() => {
|
||||
return access_level?.value >= visitor;
|
||||
});
|
||||
return { access_level, getPermission, isAdmin, isDeveloper, isVisitor };
|
||||
}
|
||||
17
src/utils/hooks/useUserInfo.ts
Normal file
17
src/utils/hooks/useUserInfo.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
type UserInfo = {
|
||||
domain_id?: string;
|
||||
email?: string;
|
||||
id?: string;
|
||||
mobile?: string;
|
||||
nickname?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export const useUserInfo = (): { userInfo: UserInfo; } => {
|
||||
const userInfoStr = localStorage.getItem('userInfo');
|
||||
const userInfoStr2 = sessionStorage.getItem('userInfo');
|
||||
const userInfo: UserInfo = userInfoStr ? JSON.parse(userInfoStr) : userInfoStr2 ? JSON.parse(userInfoStr2) : {};
|
||||
return {
|
||||
userInfo
|
||||
};
|
||||
};
|
||||
299
src/utils/hooks/useWikiInit.ts
Normal file
299
src/utils/hooks/useWikiInit.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { ref, isRef, reactive, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import utf8 from 'crypto-js/enc-utf8';
|
||||
import Base64 from 'crypto-js/enc-base64';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import { addEventListener, offEvent } from '@/utils/eventBus';
|
||||
import type { WikiFileResType } from '@/api/repo/types';
|
||||
import { getWikiList, getWikiDetail, createdWiki, wikiCommitHistory, deleteWiki, updateWiki } from '@/api/repo/index';
|
||||
// Home是wiki首页 _Sidebar是侧边栏 _Footer是页脚
|
||||
const hasExt = /(\.[^.\/]+)$/;// 是否为带后缀名的路径
|
||||
const splitExt = /^(.*?)(\.[^.\/]+)?$/;// 分离路径和后缀
|
||||
export async function getWikiFiles(file_path:string, repo_path:string, parse:boolean = true) { // 请求wiki文件详情
|
||||
const reqWikiFilesParams = { file_path, repo_path };
|
||||
const wikiFilesRes = await getWikiDetail(reqWikiFilesParams);
|
||||
if (wikiFilesRes.data) {
|
||||
const res = wikiFilesRes.data.data;
|
||||
return {
|
||||
...res,
|
||||
content: res.content && parse ? utf8.stringify(Base64.parse(res.content)) : res.content
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export const useWikiHome = () => {
|
||||
const repoPath = ref<string>('');
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const [, namespace, repoName] = route.path.split('/');
|
||||
const showList = ref<boolean>(false);
|
||||
repoPath.value = `${namespace}/${repoName}`;
|
||||
const pager = reactive({
|
||||
total: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 1000
|
||||
});
|
||||
const loadingStatus = reactive({ // 加载状态
|
||||
wikiListLoading: true,
|
||||
pageInitLoading: true
|
||||
});
|
||||
const wikiList = ref<any[]>([]);
|
||||
const wikiHome = ref<string>('');
|
||||
const wikiPath = ref<any[]>([]);
|
||||
function handleError(e:any) { // 处理非法进入的错误
|
||||
if (e && e.error_code === 4109 && e.error_code_name === 'WIKE_PRIVATE_REPO_ERROR') router.push({ name: 'repoDashboard' });
|
||||
}
|
||||
addEventListener('responseError', handleError);
|
||||
async function getWikiData() { // 请求wiki列表数据
|
||||
const reqWikiListParams = {
|
||||
page: pager.pageIndex,
|
||||
per_page: pager.pageSize,
|
||||
repo_path: repoPath.value,
|
||||
filePath: wikiPath.value.join('/')
|
||||
};
|
||||
loadingStatus.wikiListLoading = true;
|
||||
const wikiListRes = await getWikiList(reqWikiListParams);
|
||||
loadingStatus.wikiListLoading = false;
|
||||
if (wikiListRes.data) {
|
||||
const { content, total } = wikiListRes.data.data;
|
||||
wikiList.value = content || [];
|
||||
pager.total = total;
|
||||
}
|
||||
}
|
||||
async function initPage() { // 页面初始化
|
||||
loadingStatus.pageInitLoading = true;
|
||||
const fileHome = await getWikiFiles('Home.md', repoPath.value);
|
||||
wikiHome.value = fileHome?.content || '';
|
||||
await getWikiData();
|
||||
loadingStatus.pageInitLoading = false;
|
||||
}
|
||||
onUnmounted(() => offEvent('responseError', handleError));
|
||||
watch(() => route.query, (query) => { if (query) showList.value = query.showList === '1'; }, { immediate: true });
|
||||
return { initPage, getWikiData, showList, loadingStatus, pager, wikiPath, wikiList, wikiHome };
|
||||
};
|
||||
export const useWikiCreate = () => { // 创建wiki
|
||||
const repoPath = ref<string>('');
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const suffix = ref<string>('');// 扩展名
|
||||
const mode = ref<number>(0);// 0=create,1=edit
|
||||
const [, namespace, repoName,, type, ...encodeName] = route.path.split('/');
|
||||
repoPath.value = `${namespace}/${repoName}`;
|
||||
const formData = reactive<Record<keyof any, string>>({
|
||||
title: '', // 页面标题
|
||||
mdContent: '', // MarkDown内容
|
||||
submitInfo: ''// 提交信息
|
||||
});
|
||||
const loadingStatus = reactive<Record<keyof any, boolean>>({
|
||||
createLoading: false,
|
||||
updateLoading: false
|
||||
});
|
||||
const decodeFileName = ref<string>('');
|
||||
if (type === 'edit') { // 编辑模式,需要回显页面内容和标题
|
||||
mode.value = 1;
|
||||
editInit();
|
||||
} else { // 创建模式,可以指定标题
|
||||
if (route.query.fileName) {
|
||||
decodeFileName.value = decodeURIComponent(route.query.fileName as string);
|
||||
const [, path, ext] = Array.prototype.slice.call(splitExt.exec(decodeFileName.value));
|
||||
formData.title = path;
|
||||
suffix.value = ext || '';
|
||||
}
|
||||
formData.submitInfo = `创建 ${decodeFileName.value || '新'}页面`;
|
||||
}
|
||||
async function editInit() {
|
||||
decodeFileName.value = decodeURIComponent(encodeName.join('/'));
|
||||
const res = await getWikiFiles(`${decodeFileName.value}`, repoPath.value);
|
||||
if (res) {
|
||||
formData.mdContent = res?.content || '';
|
||||
const [, path, ext] = Array.prototype.slice.call(splitExt.exec(res.name || decodeFileName.value));
|
||||
formData.title = path;
|
||||
formData.submitInfo = `修改 ${decodeFileName.value}`;
|
||||
suffix.value = ext || '';
|
||||
}
|
||||
}
|
||||
async function createWikiFile(...args:any) { // 创建wiki文件
|
||||
decodeFileName.value = `${formData.title}${hasExt.test(formData.title as string) ? '' : suffix.value}`;
|
||||
const reqWikiFilesParams = {
|
||||
repo_path: repoPath.value,
|
||||
name: `${formData.title}`,
|
||||
file_path: `${decodeFileName.value}`,
|
||||
commit_message: formData.submitInfo,
|
||||
content: formData.mdContent,
|
||||
currUserId: ''
|
||||
};
|
||||
loadingStatus.createLoading = true;
|
||||
const wikiCreateRes = await createdWiki(reqWikiFilesParams);
|
||||
loadingStatus.createLoading = false;
|
||||
if (wikiCreateRes.data) {
|
||||
const res = wikiCreateRes.data.data;
|
||||
const retPath = res.replace(/(\.[^.\/]+)?$/, '');
|
||||
if (~['Home', '_Sidebar', '_Footer'].indexOf(retPath)) router.back();
|
||||
else router.push({ name: 'repoWikiDetail', params: { wikiPath: res.split('/') }});
|
||||
Message.success('创建成功');
|
||||
}
|
||||
}
|
||||
async function deleteFile() { // 删除文件
|
||||
const result = await deleteWiki({
|
||||
repo_path: repoPath.value,
|
||||
file_path: `${decodeFileName.value}`
|
||||
});
|
||||
if (result.data) {
|
||||
Message.success('删除成功');
|
||||
router.push({ name: 'repoWiki' });
|
||||
}
|
||||
}
|
||||
async function updateFile() { // 更新文件
|
||||
const reqWikiFilesParams = {
|
||||
repo_path: repoPath.value,
|
||||
name: `${formData.title}`,
|
||||
file_path: `${decodeFileName.value}`,
|
||||
commit_message: formData.submitInfo,
|
||||
content: formData.mdContent
|
||||
};
|
||||
loadingStatus.updateLoading = true;
|
||||
const result = await updateWiki(reqWikiFilesParams);
|
||||
loadingStatus.updateLoading = false;
|
||||
if (result.data) {
|
||||
const res = result.data.data;
|
||||
const retPath = res.replace(/(\.[^.\/]+)?$/, '');
|
||||
if (~['Home', '_Sidebar', '_Footer'].indexOf(retPath)) router.back();
|
||||
else router.push({ name: 'repoWikiDetail', params: { wikiPath: res.split('/') }});
|
||||
Message.success('更新成功');
|
||||
}
|
||||
}
|
||||
return {
|
||||
mode,
|
||||
formData,
|
||||
repoPath,
|
||||
loadingStatus,
|
||||
decodeFileName,
|
||||
deleteFile,
|
||||
updateFile,
|
||||
createWikiFile
|
||||
};
|
||||
};
|
||||
export const useWikiDetail = () => { // wiki详情
|
||||
const repoPath = ref<string>('');
|
||||
const fileName = ref<string>('Home.md');
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loadingStatus = reactive({ // 加载状态
|
||||
homeLoading: true,
|
||||
sidebarLoading: true,
|
||||
footerLoading: true
|
||||
});
|
||||
const [, namespace, repoName,, ...encodeName] = route.path.split('/');
|
||||
repoPath.value = `${namespace}/${repoName}`;
|
||||
const wikiContent = ref<string>('');
|
||||
const pageProfile = reactive<Partial<WikiFileResType>>({});
|
||||
const wikiSidebar = ref<string>('');
|
||||
const wikiFooter = ref<string>('');
|
||||
if (encodeName.length) fileName.value = decodeURIComponent(encodeName.join('/'));
|
||||
const orn = location.hostname === 'localhost' ? 'https://test.gitcode.net' : location.origin;
|
||||
const wiki_url_to_repo = ref<string>(`${orn}/${namespace}/${repoName}.wiki.git`);// git
|
||||
async function initPage() { // 获取页面 侧边栏 页脚的内容
|
||||
loadingStatus.homeLoading = true;
|
||||
const homeRes = await getWikiFiles(`${fileName.value}`, repoPath.value);
|
||||
if (homeRes && homeRes.content === null) {
|
||||
router.push({ name: 'repoWikiCreate', query: { fileName: fileName.value }});
|
||||
return;
|
||||
}
|
||||
wikiContent.value = homeRes?.content || '';
|
||||
if (homeRes) Object.assign(pageProfile, homeRes);
|
||||
loadingStatus.homeLoading = false;
|
||||
loadingStatus.sidebarLoading = true;
|
||||
const sideRes = await getWikiFiles('_Sidebar.md', repoPath.value);
|
||||
wikiSidebar.value = sideRes?.content || '';
|
||||
loadingStatus.sidebarLoading = false;
|
||||
loadingStatus.footerLoading = true;
|
||||
const footRes = await getWikiFiles('_Footer.md', repoPath.value);
|
||||
wikiFooter.value = footRes?.content || '';
|
||||
loadingStatus.footerLoading = false;
|
||||
}
|
||||
return {
|
||||
route,
|
||||
repoPath,
|
||||
fileName,
|
||||
wikiContent,
|
||||
wikiSidebar,
|
||||
wikiFooter,
|
||||
pageProfile,
|
||||
loadingStatus,
|
||||
wiki_url_to_repo,
|
||||
initPage
|
||||
};
|
||||
};
|
||||
export const useWikiHistory = () => { // wiki历史版本
|
||||
const repoPath = ref<string>('');
|
||||
const route = useRoute();
|
||||
const historyList = ref<any[]>([]);
|
||||
const decodeFileName = ref<string>('');
|
||||
const loadingStatus = reactive({ // 加载状态
|
||||
wikiHistoryLoading: true
|
||||
});
|
||||
const [, namespace, repoName,,, ...encodeName] = route.path.split('/');
|
||||
repoPath.value = `${namespace}/${repoName}`;
|
||||
decodeFileName.value = decodeURIComponent(encodeName.join('/'));
|
||||
const pager = reactive({
|
||||
total: 0,
|
||||
pageIndex: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 30, 40, 50]
|
||||
});
|
||||
async function getHistoryData() { // 获取历史版本列表
|
||||
const wikiHistoryParams = {
|
||||
page: pager.pageIndex,
|
||||
per_page: pager.pageSize,
|
||||
repo_path: repoPath.value,
|
||||
file_path: `${decodeFileName.value}`
|
||||
};
|
||||
loadingStatus.wikiHistoryLoading = true;
|
||||
const result = await wikiCommitHistory(wikiHistoryParams);
|
||||
loadingStatus.wikiHistoryLoading = false;
|
||||
if (result.data) {
|
||||
const { content, total } = result.data.data;
|
||||
historyList.value = content;
|
||||
pager.total = total;
|
||||
}
|
||||
}
|
||||
return {
|
||||
pager,
|
||||
historyList,
|
||||
loadingStatus,
|
||||
getHistoryData
|
||||
};
|
||||
};
|
||||
export const useMdHeightObserver = (element:any, options?:any) => { // 高度自适应的markDown编辑器,使用不当会出现页面一直延长的问题
|
||||
let handleObserve:Function|null = null;
|
||||
const minHeight = options?.minHeight || 250;
|
||||
const height = ref(minHeight);
|
||||
function observeCallback(entrys:any[]) {
|
||||
for (const entry of entrys) {
|
||||
const { borderBoxSize } = entry;
|
||||
height.value = borderBoxSize[0].blockSize < minHeight ? minHeight : borderBoxSize[0].blockSize;
|
||||
}
|
||||
}
|
||||
function addObserver(targetNode:HTMLElement, callback:any) { // 监听元素改变
|
||||
const observer = new ResizeObserver(callback);
|
||||
observer.observe(targetNode);
|
||||
return () => { // 消除副作用
|
||||
observer.unobserve(targetNode);
|
||||
};
|
||||
}
|
||||
onMounted(() => {
|
||||
const el = isRef(element) ? element.value : element;
|
||||
if (el instanceof HTMLElement) {
|
||||
handleObserve = addObserver(el, observeCallback);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => handleObserve && handleObserve());
|
||||
return {
|
||||
height
|
||||
};
|
||||
};
|
||||
export function useAssets() {
|
||||
const iconDelete = new URL('@/assets/imgs/icon/icon-delete.png', import.meta.url).href;
|
||||
const iconHistory = new URL('@/assets/imgs/icon/icon-history.png', import.meta.url).href;
|
||||
return { iconDelete, iconHistory };
|
||||
}
|
||||
25
src/utils/hooks/usebranchName.ts
Normal file
25
src/utils/hooks/usebranchName.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ref, toValue, watchEffect, watch, computed } from 'vue';
|
||||
import router from '@/router';
|
||||
export const useBranchName = (connector: string = '%2F', branch: string | string[] = '') => {
|
||||
const branchName = computed(() => {
|
||||
const routeNamespace = branch || router.currentRoute.value.params.namespace;
|
||||
if (!routeNamespace) return null;
|
||||
const branchName = Array.isArray(routeNamespace)
|
||||
? routeNamespace.join(connector)
|
||||
: routeNamespace.split('/').join(connector);
|
||||
return branchName;
|
||||
});
|
||||
return { branchName };
|
||||
};
|
||||
|
||||
export const useEscapeBranchName = (url: string) => {
|
||||
if (router.resolve(url).href) {
|
||||
const link = router.resolve(url).href.replace(/%2F/g, '/');
|
||||
let branchName = router.currentRoute.value.params.branchName.toString();
|
||||
branchName = branchName.replace(/%2F/g, '/');
|
||||
const branchNameEscape = branchName.replace(/\//g, '%2F');
|
||||
return link.replace(branchName, branchNameEscape);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
};
|
||||
462
src/utils/index.ts
Normal file
462
src/utils/index.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
/* 工具函数 */
|
||||
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('userInfo') || '{}');
|
||||
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 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 '';
|
||||
};
|
||||
4
src/utils/isPhone.ts
Normal file
4
src/utils/isPhone.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default function isPhone() {
|
||||
const flag = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
return flag;
|
||||
}
|
||||
18
src/utils/microAppConfig.ts
Normal file
18
src/utils/microAppConfig.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { addEventListener, clearEvent } from '@/utils/eventBus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import microApp from '@micro-zoe/micro-app';
|
||||
export const microEvents = () => {
|
||||
const router = useRouter();
|
||||
addEventListener('microRouterChange', (to) => {
|
||||
// console.log('proxy', to);
|
||||
router.push(to.fullPath);
|
||||
});
|
||||
router.beforeEach((to, from) => {
|
||||
// console.log('parent to', to);
|
||||
if (to.meta.micorApp) {
|
||||
to.meta.micorApp.forEach(appName => { // 子应用同步父应用的跳转
|
||||
microApp.router.replace({ name: appName, path: to.fullPath });
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
26
src/utils/modules.d.ts
vendored
Normal file
26
src/utils/modules.d.ts
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// markdown插件类型plantuml
|
||||
declare module 'markdown-it-plantuml' {
|
||||
import { PluginWithOptions } from 'markdown-it';
|
||||
import { RenderRule } from 'markdown-it/lib/renderer';
|
||||
declare namespace Plantuml {
|
||||
interface Options {
|
||||
closeMarker?: string;
|
||||
diagramName?: string;
|
||||
generateSource?: (umlCode: string, pluginOptions: Options) => string;
|
||||
imageFormat?: string;
|
||||
openMarker?: string;
|
||||
render?: RenderRule;
|
||||
server?: string;
|
||||
}
|
||||
}
|
||||
declare const markdownItPlantuml: PluginWithOptions<Plantuml.Options>;
|
||||
export = markdownItPlantuml;
|
||||
}
|
||||
// markdown插件类型katex
|
||||
declare module '@iktakahiro/markdown-it-katex' {
|
||||
import { KatexOptions } from 'katex';
|
||||
import { PluginWithOptions } from 'markdown-it';
|
||||
type Options = Pick<KatexOptions, 'errorColor' | 'throwOnError'>;
|
||||
declare const mathPlugin: PluginWithOptions<Options>;
|
||||
export = mathPlugin;
|
||||
}
|
||||
115
src/utils/regex.ts
Normal file
115
src/utils/regex.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 正则匹配邮箱
|
||||
*/
|
||||
export const emailRegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配密码
|
||||
*/
|
||||
export const passwordRegExp = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,30}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配验证码
|
||||
*/
|
||||
export const verifyCodeRegExp4 = /^\d{4}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配验证码
|
||||
*/
|
||||
export const verifyCodeRegExp6 = /^\d{6}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配身份证
|
||||
*/
|
||||
export const IDRegExp = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
|
||||
/**
|
||||
* 正则匹配手机号
|
||||
*/
|
||||
export const mobileRegExp = /^1[3-9]\d{9}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配用户名称
|
||||
*/
|
||||
export const usernameRegExp = /^(?!-)(?!.*-$)[a-zA-Z][a-zA-Z0-9_-]{1,18}[a-zA-Z0-9]$/;
|
||||
|
||||
/**
|
||||
* 官网地址(支持"?+参数",暂不支持"#+参数"后端做了验证)
|
||||
*/
|
||||
export const websiteRegExp = /^(((ht|f)tps?):\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&\/~+-]*[\w@?^=%&\/~+-])?$/;
|
||||
|
||||
/**
|
||||
* 网址(支持端口和"?+参数"和"#+参数")
|
||||
*/
|
||||
export const urlRegExp = /(http|https):\/\/[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.]+$/;
|
||||
|
||||
/**
|
||||
* 正则匹配tag名称-以“-”开头
|
||||
*/
|
||||
export const tagNameHyphenRegExp = /^[^-].*/;
|
||||
|
||||
/**
|
||||
* 正则匹配tag名称-以“refs/heads/”开头
|
||||
*/
|
||||
export const tagNamePrefixHeadsRegExp = /^(?!refs\/heads\/).*/;
|
||||
|
||||
/**
|
||||
* 正则匹配tag名称-以“refs/remotes/”开头
|
||||
*/
|
||||
export const tagNamePrefixRemotesRegExp = /^(?!refs\/remotes\/).*/;
|
||||
|
||||
/**
|
||||
* 正则匹配tag名称-以“/”或“.”或“.lock”结尾
|
||||
*/
|
||||
export const tagNameSuffixLockRegExp = /^(?!.*(\.|\/|\.lock)$).*$/;
|
||||
|
||||
/**
|
||||
* 正则匹配tag名称 名称由字母、数字_.-和/组成;且/不能连续,分支名称必须以字母或数字开头,分支名称最多可以包含 50 个字符
|
||||
*/
|
||||
export const tagNameRegExp = /^[\u4E00-\u9FA5a-zA-Z0-9](?!.*\/\/)[\u4E00-\u9FA5a-zA-Z0-9-_.\/]{0,49}$/;
|
||||
/**
|
||||
* 正则匹配分支名称
|
||||
*/
|
||||
export const branchNameRegExp = /^[a-zA-Z0-9](?!.*\/\/)[a-zA-Z0-9-_\/]{0,199}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配org名称
|
||||
*/
|
||||
export const orgNameRegExp = /^[a-zA-Z\d\u4e00-\u9fa5](?:[a-zA-Z\d\u4e00-\u9fa5]|(-|_)(?=[a-zA-Z\d\u4e00-\u9fa5])){0,50}$/;
|
||||
/**
|
||||
* 正则匹配orgPath名称
|
||||
*/
|
||||
export const orgPathRegExp = /^[a-zA-Z][a-zA-Z0-9-_]{1,48}[a-zA-Z0-9]$/;
|
||||
|
||||
/**
|
||||
* 正则匹配项目名称
|
||||
*/
|
||||
export const repoNameRegExp = /^[\u4e00-\u9fa5a-zA-Z\d_.][\u4e00-\u9fa5a-zA-Z\d_\-.]{0,99}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配项目topic名称
|
||||
*/
|
||||
export const repoTopicRegExp = /^[\u4e00-\u9fa5a-zA-Z\d_][\u4e00-\u9fa5a-zA-Z\d_\-.]{0,49}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配项目Path名称-正则
|
||||
*/
|
||||
export const repoPathRegExp = /^(?!-)[a-zA-Z0-9_.-]{1,100}$/;
|
||||
/**
|
||||
* 正则匹配项目Path名称-后缀(禁用某些结尾)
|
||||
*/
|
||||
export const repoPathSuffix = /^(?!.*(?:\.wiki|\.git|\.atom)$).*$/;
|
||||
/**
|
||||
* 正则匹配项目Path名称-全匹配(禁用某些path)
|
||||
*/
|
||||
export const repoPathMatch = /^(?!codehub$|dashboard$|users$|profile$|merge_requests$|issues$|milestones$|settings$|wiki$|home$|tree$|chart$|blob$|issues-create$|review$|release$|tags$|branches$|commits$|compare$|newmergefrom$|network$|memberlist$|labels$|setting$|files$).+/;
|
||||
/**
|
||||
* 正则匹配分支规则
|
||||
* 分支名称由字母、数字-_和/组成;分支名称必须以字母或数字开头和结尾,分支名称最多可以包含 200 个字符。
|
||||
*/
|
||||
export const branchRegex = /^[a-zA-Z\d](?:[a-zA-Z\d]|(-|_|\/)(?=[a-zA-Z\d])){0,200}$/;
|
||||
|
||||
/**
|
||||
* 正则匹配wiki标题(文件名)规则
|
||||
* wiki标题(文件名)由中文、字母、数字、短划线(-) 、下划线(_)和句点(.)字符
|
||||
*/
|
||||
export const wikiTitleReg = /^[a-zA-Z0-9_\-\u4e00-\u9fa5./]*$/;
|
||||
194
src/utils/request.ts
Normal file
194
src/utils/request.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/* 请求封装 */
|
||||
import axios, { type AxiosRequestConfig } from 'axios';
|
||||
import { dealWarning } from './status';
|
||||
import router from '@/router';
|
||||
import { repoInfoStore } from '@/stores/Repo';
|
||||
import { useGlobalInfoStore } from '@/stores/Global';
|
||||
import { cutParamsInUrl, getSignUtmSource } from './index';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import degradeInterceptor from './degradeInterceptor';
|
||||
|
||||
const handleError = dealWarning();
|
||||
|
||||
/** 设置用户中心相关api迁移时的api前缀 */
|
||||
const setPassportPrefix = (url: string, method: string) => {
|
||||
const prefix = (import.meta as any).env.VITE_PASSPORT_PREFIX;
|
||||
if (prefix) {
|
||||
if (url?.includes('/api/v1/user/')) {
|
||||
// 用户信息相关
|
||||
return `${prefix}${url}`;
|
||||
}
|
||||
if (url?.includes('/api/v1/oauth/')) {
|
||||
// 用户权限
|
||||
return `${prefix}${url}`;
|
||||
}
|
||||
if (url?.includes('/api/v1/internal/messages')) {
|
||||
// 消息通知
|
||||
return `${prefix}${url}`;
|
||||
}
|
||||
if (url?.includes('/api/v1/follow')) {
|
||||
// 关注状态
|
||||
return `${prefix}${url}`;
|
||||
}
|
||||
if (url?.includes('/api/v1/obs') && method === 'get') {
|
||||
// 图片相关
|
||||
return `${prefix}${url}`;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const baseURL = (import.meta as any).env.VITE_API_HOST;
|
||||
export const baseDevURL = (import.meta as any).env.VITE_DEV_API_HOST;
|
||||
|
||||
type CustomConfigs = {
|
||||
// 是否使用自定义的错误处理
|
||||
customError?: boolean;
|
||||
};
|
||||
|
||||
interface PendingTask {
|
||||
config: AxiosRequestConfig;
|
||||
resolve: Function;
|
||||
}
|
||||
let refreshing = false;
|
||||
const queue: PendingTask[] = [];
|
||||
|
||||
const refreshToken = async () => axios.request({
|
||||
url: baseURL + `${(import.meta as any).env.VITE_PASSPORT_PREFIX || ''}` + '/api/v1/user/token/refresh',
|
||||
method: 'post',
|
||||
data: {
|
||||
refresh_token: localStorage.getItem('refresh_token')
|
||||
},
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
const proxyService = (params: any, customConfigs?: CustomConfigs) => {
|
||||
const service = axios.create({
|
||||
baseURL: params.apiType === 'devApi' ? baseDevURL : baseURL,
|
||||
timeout: 15000,
|
||||
withCredentials: true
|
||||
});
|
||||
/** user api相关请求白名单 */
|
||||
const whiteList = ['/login', 'login/mobile', '/sendVeriCode', '/register', '/quickLogin', '/quickRegister', '/forgetCode', '/forgetPassword', '/checkSameUser', '/token'];
|
||||
|
||||
const notAuthorization = (url: string = '') => {
|
||||
return whiteList.some((str) => url.includes(str));
|
||||
};
|
||||
|
||||
// request 拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 配置请求头
|
||||
const globalStore = useGlobalInfoStore();
|
||||
const repoStore = repoInfoStore();
|
||||
const currentRoute = router?.currentRoute?.value;
|
||||
const repoId = currentRoute?.meta?.type === 'repo' ? repoStore.repoInfo?.id : undefined;
|
||||
const title = config.headers['homeweb-page-title'] || currentRoute?.meta?.reportTitle;
|
||||
delete config.headers['homeweb-page-title'];
|
||||
if (title && params.apiType !== 'devApi') {
|
||||
// devpress接口不需要上报
|
||||
config.headers['page-title'] = encodeURIComponent(title);
|
||||
}
|
||||
if (currentRoute.name === 'homepage' && params.apiType !== 'devApi') {
|
||||
const homeTitleConfig = {
|
||||
0: '个人主页',
|
||||
1: '组织主页'
|
||||
};
|
||||
const pageTitle = homeTitleConfig[globalStore.namespaceType as 0 | 1];
|
||||
config.headers['page-title'] = encodeURIComponent(pageTitle);
|
||||
}
|
||||
if (repoId && params.apiType !== 'devApi') {
|
||||
config.headers['page-repo-id'] = repoId;
|
||||
}
|
||||
|
||||
if (params.apiType !== 'devApi') {
|
||||
config.headers['page-ref'] = encodeURIComponent(cutParamsInUrl(window.page_ref || ''));
|
||||
config.headers['page-uri'] = encodeURIComponent(cutParamsInUrl(window.location.href || ''));
|
||||
config.headers['gitcode-utm-source'] = encodeURIComponent(getSignUtmSource());
|
||||
}
|
||||
|
||||
const access_token = window.localStorage.getItem('access_token');
|
||||
const { url } = config;
|
||||
if (access_token && !notAuthorization(url)) {
|
||||
config.headers.Authorization = `Bearer ${access_token}`;
|
||||
}
|
||||
/** 修改用户模块前缀名称 */
|
||||
config.url = setPassportPrefix(config.url as string, config.method as string);
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
service.interceptors.request.use(degradeInterceptor.onRequestFulfilled, degradeInterceptor.onRequestRejected);
|
||||
|
||||
// response 拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
const { response } = error;
|
||||
const { config } = response || {};
|
||||
if (refreshing) {
|
||||
return new Promise((resolve) => {
|
||||
queue.unshift({
|
||||
config,
|
||||
resolve
|
||||
});
|
||||
});
|
||||
}
|
||||
if (response?.status === 401 && localStorage.getItem('access_token') && !config?.url.includes('/token/refresh')) {
|
||||
refreshing = true;
|
||||
try {
|
||||
const res = await refreshToken();
|
||||
refreshing = false;
|
||||
if (res.status === 200) {
|
||||
const { access_token, refresh_token } = res.data;
|
||||
localStorage.setItem('access_token', access_token);
|
||||
localStorage.setItem('refresh_token', refresh_token);
|
||||
queue.forEach(({ config, resolve }) => {
|
||||
if (config.headers) {
|
||||
config.headers.Authorization = `Bearer ${access_token}`;
|
||||
}
|
||||
resolve(axios(config));
|
||||
});
|
||||
queue.splice(0);
|
||||
config.headers.Authorization = `Bearer ${access_token}`;
|
||||
return axios(config);
|
||||
} else {
|
||||
handleError({}, true);
|
||||
return Promise.reject(res.data);
|
||||
}
|
||||
} catch (e) {
|
||||
handleError({}, true);
|
||||
}
|
||||
} else if (response) {
|
||||
// 请求已发出,但是不在2xx的范围
|
||||
// showMessage(response);
|
||||
response.data.http_status = response.status;
|
||||
if (!customConfigs || !customConfigs.customError) {
|
||||
handleError(response);
|
||||
}
|
||||
return Promise.reject(response.data);
|
||||
} else {
|
||||
// 网络连接超时
|
||||
const timeoutRegx = /\btimeout\b/;
|
||||
if (error?.message && timeoutRegx.test(error.message)) {
|
||||
Message.error('网络连接超时');
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
service.interceptors.response.use(degradeInterceptor.onResponseFulfilled, degradeInterceptor.onResponseRejected);
|
||||
|
||||
return service(params);
|
||||
};
|
||||
|
||||
export default proxyService;
|
||||
9
src/utils/setFurionConfig.ts
Normal file
9
src/utils/setFurionConfig.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* 埋点统计设置用户信息
|
||||
* @param config
|
||||
*/
|
||||
export default function furionSetConfig(config) {
|
||||
try {
|
||||
window.__fr.setConfig(config);
|
||||
} catch (e) {}
|
||||
}
|
||||
75
src/utils/status.ts
Normal file
75
src/utils/status.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { emitEvent } from './eventBus';
|
||||
import { Message } from 'vue-devui/message';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export function dealWarning() {
|
||||
return debounce((res: Record<string, any>, logout: boolean = false) => {
|
||||
const msg = showMessage(res, logout);
|
||||
if (msg) {
|
||||
Message.error(showMessage(res));
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
/**
|
||||
* 400010: 用户名密码错误
|
||||
* 400008: 登录被锁定
|
||||
* 400009: 登录惩罚升级
|
||||
* 400007: 验证码错误
|
||||
*/
|
||||
const failCode = ['400010', '400008', '400009', '400007'];
|
||||
|
||||
export function showMessage(res: Record<string, any>, logout: boolean = false) : string {
|
||||
if (logout) {
|
||||
emitEvent('logout', true);
|
||||
return '';
|
||||
}
|
||||
let message:string = '';
|
||||
const { status } = res;
|
||||
const error_code = res.data.error_code?.toString() || '';
|
||||
switch (status) {
|
||||
case 400: // 参数错误直接返回错误信息
|
||||
if (!failCode.includes(error_code)) {
|
||||
message = error_code === '400013' ? '账号异常,请联系客服' : res.data.error_message;
|
||||
}
|
||||
break;
|
||||
case 401:
|
||||
// if (!failCode.includes(res.data.error_code.toString())) {
|
||||
// message = res.data.error_message;
|
||||
// }
|
||||
emitEvent('logout', Boolean(localStorage.getItem('access_token')));
|
||||
break;
|
||||
case 403:
|
||||
if (localStorage.getItem('access_token')) {
|
||||
emitEvent('forbiddenRefresh');
|
||||
} else {
|
||||
message = res.data.error_message;
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
break;
|
||||
case 408:
|
||||
message = '请求超时';
|
||||
break;
|
||||
case 500:
|
||||
message = '服务器错误';
|
||||
break;
|
||||
case 501:
|
||||
message = '服务未实现';
|
||||
break;
|
||||
case 502:
|
||||
message = '网络错误';
|
||||
break;
|
||||
// case 503:
|
||||
// location.href = '/503';
|
||||
// break;
|
||||
case 504:
|
||||
message = '网络超时';
|
||||
break;
|
||||
case 505:
|
||||
message = 'HTTP版本不受支持';
|
||||
break;
|
||||
default:
|
||||
message = res?.data?.error_message || `连接出错`;
|
||||
}
|
||||
return message;
|
||||
};
|
||||
16
src/utils/storage.ts
Normal file
16
src/utils/storage.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const setStore = (key, object) => {
|
||||
try {
|
||||
localStorage.setItem(key, JSON.stringify(object));
|
||||
} catch (e) {
|
||||
// console.warn(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getStore = (key) => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(key) || '');
|
||||
} catch (e) {
|
||||
// console.warn(e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
78
src/utils/theme/gitcodeColorMap.ts
Normal file
78
src/utils/theme/gitcodeColorMap.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export const colorMap = {
|
||||
'devui-text': '#000000',
|
||||
'devui-aide-text': '#3B3E55',
|
||||
'devui-placeholder': '#9C9DB4',
|
||||
'devui-disabled-text': '#BCBCD0',
|
||||
'devui-light-text': '#FFFFFF',
|
||||
'devui-icon-fill': '#000000',
|
||||
'devui-icon-fill-weak': '#3B3E55',
|
||||
'devui-icon-fill-hover': '#3B3E55',
|
||||
'devui-icon-disabled-color': '#BCBCD0',
|
||||
'devui-icon-bg': '#FFFFFF',
|
||||
'devui-brand': '#2951E0',
|
||||
'devui-brand-foil': '#4D6EE5',
|
||||
'devui-shape-icon-fill': '#BCBCD0',
|
||||
'devui-shape-icon-fill-hover': '#BCBCD0',
|
||||
'devui-shape-icon-fill-disabled': '#BCBCD0',
|
||||
'devui-dividing-line': '#F1F1F8',
|
||||
'devui-line': '#F1F1F8',
|
||||
'devui-disabled-line': '#D1D1E0',
|
||||
'devui-form-control-line': '#F1F1F8',
|
||||
'devui-form-control-line-hover': '#E3E3EE',
|
||||
'devui-form-control-line-active': '#2951E0',
|
||||
'devui-primary': '#000000',
|
||||
'devui-primary-hover': '#333333',
|
||||
'devui-primary-active': '#000000',
|
||||
'devui-primary-disabled': '#cccccc',
|
||||
'devui-btn-common-bg': '#FFFFFF',
|
||||
'devui-btn-common-bg-hover': '#F9F9FB',
|
||||
'devui-disabled-bg': '#F9F9FB',
|
||||
'devui-btn-common-border-color': '#E3E3EE',
|
||||
'devui-btn-common-border-color-hover': '#8B8CA7',
|
||||
'devui-btn-common-border-disabled': '#F1F1F8',
|
||||
'devui-gray-form-control-bg': '#F9F9FB',
|
||||
'devui-gray-form-control-hover-bg': '#F1F1F8',
|
||||
'devui-link': '#2951E0',
|
||||
'devui-link-light': '#94A8F0',
|
||||
'devui-link-disabled': '#F1F1F8',
|
||||
'devui-icon-hover-bg': '#F1F1F8',
|
||||
'devui-icon-active-bg': '#E3E3EE',
|
||||
'devui-list-item-active-text': '#252b3a',
|
||||
'devui-list-item-active-bg': '#F1F1F8',
|
||||
'devui-global-bg': '#F9F9FB',
|
||||
'devui-connected-overlay-b': '#FFFFFF',
|
||||
'devui-list-item-hover-text': '#252b3a',
|
||||
'devui-list-item-hover-bg': '#F1F1F8',
|
||||
'devui-feedback-overlay-bg': '#3F425A',
|
||||
'devui-danger-bg': '#F9DADF',
|
||||
'devui-danger': '#DA203E',
|
||||
'devui-contrast': '#DA203E',
|
||||
'devui-contrast-hover': '#E0455E',
|
||||
'devui-contrast-active': '#A203E',
|
||||
'devui-danger-text': '#DA203E',
|
||||
'devui-warning-bg': '#FCE4DA',
|
||||
'devui-warning': '#ED5F22',
|
||||
'devui-warning-text': '#F07A47',
|
||||
'devui-success-bg': '#D6ECE2',
|
||||
'devui-success': '#10A35C',
|
||||
'devui-success-text': '#31A26C'
|
||||
};
|
||||
|
||||
export const fontMap = {
|
||||
'devui-font-size': '14px',
|
||||
'devui-font-size-card-title': '16px',
|
||||
'devui-font-size-page-title': '16px',
|
||||
'devui-font-size-modal-title': '18px',
|
||||
'devui-font-size-price': '20px',
|
||||
'devui-font-size-data-overview': '24px',
|
||||
'devui-font-size-icon': '16px',
|
||||
'devui-font-size-sm': '12px',
|
||||
'devui-font-size-md': '14px',
|
||||
'devui-font-size-lg': '14px',
|
||||
'devui-font-content-weight': 'normal',
|
||||
'devui-line-height-base': '1.5'
|
||||
}
|
||||
|
||||
export const borderMap = {
|
||||
'devui-border-radius': '4px'
|
||||
}
|
||||
9
src/utils/theme/gitcodeTheme.ts
Normal file
9
src/utils/theme/gitcodeTheme.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Theme, devuiLightTheme } from 'devui-theme';
|
||||
import { colorMap, fontMap, borderMap } from './gitcodeColorMap';
|
||||
export const gitCodeTheme: Theme = new Theme({
|
||||
id: 'gitcode-theme',
|
||||
name: 'gitcode-theme',
|
||||
cnName: 'GitCode主题',
|
||||
data: Object.assign({}, devuiLightTheme.data, colorMap, fontMap, borderMap),
|
||||
isDark: false
|
||||
});
|
||||
39
src/utils/types.ts
Normal file
39
src/utils/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// 设置接口部分属性可选
|
||||
export type PartPartial<T, D extends keyof T> = Partial<Pick<T, D>> & Omit<T, D>;
|
||||
// 创建一个接口所有属性都为某一类型
|
||||
export type TypeAll<T extends keyof any, D> = {[k in T]:D};
|
||||
// 带页码的返回体
|
||||
export interface CommonWithPageReqType<T>{
|
||||
content:T;
|
||||
page_count:number;
|
||||
page_num:number;
|
||||
page_size:number;
|
||||
total:number;
|
||||
}
|
||||
export type RepoItemResData = {
|
||||
id: number;
|
||||
name: string;
|
||||
namespace: string;
|
||||
path: string;
|
||||
develop_mode: string;
|
||||
tag_list: any[];
|
||||
visibility: string;
|
||||
star_count: number;
|
||||
forks_count: number;
|
||||
open_issues_count: number;
|
||||
open_merge_requests_count: number;
|
||||
open_change_requests_count: number;
|
||||
starred: boolean;
|
||||
name_with_namespace: string;
|
||||
web_url: string;
|
||||
last_activity_at: Date;
|
||||
main_repository_language: null[];
|
||||
forked_from_project: null;
|
||||
permissions: null;
|
||||
archived: boolean;
|
||||
member_count: number;
|
||||
description: null;
|
||||
repository_size: number;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
130
src/utils/validator.ts
Normal file
130
src/utils/validator.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { repoPathRegExp, repoPathSuffix, repoPathMatch, repoNameRegExp, tagNameHyphenRegExp, tagNamePrefixHeadsRegExp, tagNamePrefixRemotesRegExp, tagNameSuffixLockRegExp, tagNameRegExp } from '@/utils/regex';
|
||||
import { checkRepoName } from '@/api/repo';
|
||||
import { reqCatch } from '@/utils/catch';
|
||||
function asyncDebounce(fn:Function, wait:number) { // 异步函数版debounce,直接使用lodash的有问题
|
||||
let timerId:any = null;
|
||||
return function(this:any, ...params:any[]) {
|
||||
return new Promise((resolve, reject) => {
|
||||
clearTimeout(timerId);
|
||||
timerId = setTimeout(async() => {
|
||||
try {
|
||||
const res = await fn.apply(this, params);
|
||||
resolve(res);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, wait);
|
||||
});
|
||||
};
|
||||
}
|
||||
interface DynamicValidator{
|
||||
(validateRules:Record<string, any[]>):{
|
||||
expected:number, // 期望值
|
||||
validVector:Ref<number> // 当前根据验证情况获取的值
|
||||
}
|
||||
}
|
||||
// 动态的表单验证,能实时监控表单的验证情况,每个字段规则不能超过32条
|
||||
export const useDynamicValidator:DynamicValidator = (validateRules, initValue?) => {
|
||||
const keyBitMap = new Map();
|
||||
const validVector = ref(0);
|
||||
// hack:监听校验情况按钮置灰
|
||||
const suffix = (rule: any, value: string, callback: Function) => {
|
||||
const keySeq = keyBitMap.get(rule.field);
|
||||
if (typeof keySeq !== 'undefined') validVector.value |= keySeq;
|
||||
return callback();
|
||||
};
|
||||
const prefix = (rule: any, value: string, callback: Function) => {
|
||||
const keySeq = keyBitMap.get(rule.field);
|
||||
if (typeof keySeq !== 'undefined') validVector.value &= ~keySeq;
|
||||
return callback();
|
||||
};
|
||||
let index = 0; let expected = 0;
|
||||
for (const name in validateRules) {
|
||||
const arr = validateRules[name];
|
||||
keyBitMap.set(name, 1 << index);
|
||||
expected += 1 << index;
|
||||
++index;
|
||||
arr.unshift({ validator: prefix });
|
||||
arr.push({ validator: suffix });
|
||||
}
|
||||
!initValue ? validVector.value = expected : '';// 初始化不置灰
|
||||
return {
|
||||
expected,
|
||||
validateRules,
|
||||
validVector
|
||||
};
|
||||
};
|
||||
// 项目名称的校验
|
||||
export const repoNameValidator = () => [
|
||||
{ required: true, message: '项目名称不能为空' },
|
||||
{ pattern: repoNameRegExp, message: '项目名称应当由中文、字母、数字、下划线(_)、点(.)和连字符(-)组成,必须以字母、数字、中文、点(.)或下划线(_)开头,且长度不得超过 100 个字符。' },
|
||||
{ pattern: repoPathSuffix, message: '项目名称不能以.wiki,.git,.atom结尾' }
|
||||
];
|
||||
interface RepoPathParams{
|
||||
repoData:any,
|
||||
id:string|undefined,
|
||||
[propName: string]: any,
|
||||
}
|
||||
|
||||
|
||||
export const repoNamePathValidator = ({ repoData, id: selfId, repoId }:RepoPathParams) => {
|
||||
const { namepath } = repoData;
|
||||
const emptyCheck = (rule: object, value: string, callback: Function) =>
|
||||
callback(namepath.id && namepath.repoPath ? undefined : new Error('项目路径不能为空'));
|
||||
|
||||
const repoPathValidate = async (rule: object, value: object) => {
|
||||
|
||||
const { repoPath, id } = value;
|
||||
|
||||
if(!repoPathRegExp.test(repoPath)){
|
||||
return Promise.reject(new Error('Path的长度必须在1到100个字符之间,只能包含字母(a-z,A-Z)、数字(0-9)、连字符(-)、下划线(_)和点(.),不区分大小写,不能以连字符和(.)开头'));
|
||||
}
|
||||
if(!repoPathSuffix.test(repoPath)){
|
||||
return Promise.reject(new Error('Path不能以.wiki,.git,.atom结尾'));
|
||||
}
|
||||
if(!repoPathMatch.test(repoPath)){
|
||||
return Promise.reject(new Error('Path不能包含如下关键字:codehub,dashboard,users,profile,merge_requests,issues,milestones,settings,wiki,home,tree,chart,blob,issues-create,review,release,tags,branches,commits,compare,newmergefrom,network,memberlist,labels,setting,files'))
|
||||
}
|
||||
return Promise.resolve('');
|
||||
|
||||
}
|
||||
|
||||
return [
|
||||
{ required: true, validator: emptyCheck },
|
||||
{ asyncValidator: asyncDebounce(repoPathValidate, 200) }
|
||||
];
|
||||
};
|
||||
|
||||
// 项目路径的校验
|
||||
export const repoPathValidator = ({ repoData, id }:RepoPathParams) => {
|
||||
const emptyCheck = (rule: object, value: string, callback: Function) =>
|
||||
callback(repoData.namespace.id && repoData.repoPath ? undefined : new Error('项目路径不能为空'));
|
||||
async function isPathUsed(rule:any, value:string) { // 项目路径是否重复验证
|
||||
const { namespace } = repoData;
|
||||
const params = {
|
||||
name: value,
|
||||
namespace_id: namespace.id === id ? null : namespace.id
|
||||
};
|
||||
const { data } = await reqCatch(checkRepoName, params);
|
||||
return (data && data.data.status) ? Promise.resolve('') : Promise.reject('path已被使用,请重新输入!');
|
||||
}
|
||||
return [
|
||||
{ required: true, validator: emptyCheck },
|
||||
{ pattern: repoPathRegExp, message: 'Path的长度必须在1到100个字符之间,只能包含字母(a-z,A-Z)、数字(0-9)、连字符(-)、下划线(_)和点(.),不区分大小写,不能以连字符和(.)开头' },
|
||||
{ pattern: repoPathSuffix, message: 'Path不能以.wiki,.git,.atom结尾' },
|
||||
{ pattern: repoPathMatch, message: 'Path不能包含如下关键字:codehub,dashboard,users,profile,merge_requests,issues,milestones,settings,wiki,home,tree,chart,blob,issues-create,review,release,tags,branches,commits,compare,newmergefrom,network,memberlist,labels,setting,files' },
|
||||
{ asyncValidator: asyncDebounce(isPathUsed, 100) }
|
||||
];
|
||||
};
|
||||
// tag名称的校验
|
||||
export const tagNameValidator = () => [
|
||||
{ required: true, message: 'tag名称不能为空' },
|
||||
{ pattern: tagNameHyphenRegExp, message: '格式错误,请勿以"-"开头' },
|
||||
{ pattern: tagNamePrefixHeadsRegExp, message: '格式错误,请勿以"refs/heads/"开头' },
|
||||
{ pattern: tagNamePrefixRemotesRegExp, message: '格式错误,请勿以"refs/remotes/"开头' },
|
||||
{ pattern: tagNameSuffixLockRegExp, message: '格式错误,请勿以"/"或"."或".lock"结尾' },
|
||||
{ pattern: tagNameRegExp, message: '格式错误,名称由中文、字母、数字_.-和/组成;且/不能连续,最多可以包含 50 个字符' }
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user