搜索结果列表页面开发

This commit is contained in:
付民康
2025-03-12 18:41:20 +08:00
commit 7cebe8fc00
739 changed files with 88149 additions and 0 deletions

92
src/utils/apiStorage.ts Normal file
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,77 @@
/** 混入颜色透明度 */
/**
*
* @param color #000 #000000
* @param opacity 01
* @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;
}

View 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
View 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
View 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
View 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;
}

View 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
};
}

View 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
};
}

View 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
View 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
View 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
};
}

View 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
}
}

View 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();
}
});
}

View 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
};
}

View 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
};
}

View 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
View 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 };
};

View 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
};
}

View 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 };
};

View 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 };
};

View 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 };
};

View 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
};
}

View 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
};
};

View 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 };
};

View 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);

View File

@@ -0,0 +1,3 @@
export function useRepoPathValidate() {
}

View 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
View 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);
});

View 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 };
};

View 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();
};
};

View 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
};
};

View 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;
};

View 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 };
}

View 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
};
};

View 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 };
}

View 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
View 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
View 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;
}

View 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
View 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
View 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
View 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;

View 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
View 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
View 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;
}
};

View 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'
}

View 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
View 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
View 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-zA-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不能包含如下关键字codehubdashboardusersprofilemerge_requestsissuesmilestonessettingswikihometreechartblobissues-createreviewreleasetagsbranchescommitscomparenewmergefromnetworkmemberlistlabelssettingfiles'))
}
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-zA-Z、数字0-9、连字符-、下划线_和点.),不区分大小写,不能以连字符和(.)开头' },
{ pattern: repoPathSuffix, message: 'Path不能以.wiki.git.atom结尾' },
{ pattern: repoPathMatch, message: 'Path不能包含如下关键字codehubdashboardusersprofilemerge_requestsissuesmilestonessettingswikihometreechartblobissues-createreviewreleasetagsbranchescommitscomparenewmergefromnetworkmemberlistlabelssettingfiles' },
{ 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 个字符' }
];