diff --git a/.eslintrc b/.eslintrc index a9c312d..0fe7251 100644 --- a/.eslintrc +++ b/.eslintrc @@ -60,8 +60,11 @@ "varsIgnorePattern": "^_" } ], + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/ban-types": "off" + "@typescript-eslint/ban-types": "off", + "class-methods-use-this": "off" // 因为AxiosCancel必须实例化而能静态化所以加的规则,如果有办法解决可以取消 }, "overrides": [ { diff --git a/package.json b/package.json index e1c852a..b05932c 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "axios": "^0.27.2", "dayjs": "^1.10.6", "echarts": "~5.1.2", + "lodash": "^4.17.21", "nprogress": "^0.2.0", "pinia": "^2.0.11", "qrcode.vue": "^3.2.2", + "qs": "^6.10.5", "tdesign-icons-vue-next": "^0.1.1", "tdesign-vue-next": "^0.16.0", "tvision-color": "^1.3.1", @@ -35,6 +37,8 @@ "@commitlint/cli": "^16.2.1", "@commitlint/config-conventional": "^16.2.1", "@types/echarts": "^4.9.10", + "@types/lodash": "^4.14.182", + "@types/qs": "^6.9.7", "@types/ws": "^8.2.2", "@typescript-eslint/eslint-plugin": "^4.29.3", "@typescript-eslint/parser": "^4.29.3", diff --git a/src/config/proxy.ts b/src/config/proxy.ts index 42284bb..80d6ae1 100644 --- a/src/config/proxy.ts +++ b/src/config/proxy.ts @@ -1,4 +1,5 @@ export default { + isRequestProxy: true, development: { // 开发环境接口请求 host: 'https://service-exndqyuk-1257786608.gz.apigw.tencentcs.com', diff --git a/src/pages/detail/advanced/index.vue b/src/pages/detail/advanced/index.vue index cdfcf93..7da16be 100644 --- a/src/pages/detail/advanced/index.vue +++ b/src/pages/detail/advanced/index.vue @@ -120,7 +120,7 @@ export default { import { ref, onMounted } from 'vue'; import { prefix } from '@/config/global'; import { BASE_INFO_DATA, TABLE_COLUMNS_DATA as columns, PRODUCT_LIST } from './constants'; -import request from '@/utils/request'; +import { request } from '@/utils/request'; import { ResDataType } from '@/types/interface'; import Product from './components/Product.vue'; @@ -145,7 +145,7 @@ const stepUpdate = () => { const fetchData = async () => { try { - const res: ResDataType = await request.get('/api/get-purchase-list'); + const res: ResDataType = await request.get({ url: '/api/get-purchase-list' }); if (res.code === 0) { const { list = [] } = res.data; data.value = list; diff --git a/src/pages/detail/deploy/index.vue b/src/pages/detail/deploy/index.vue index 4b0ae3b..9b26352 100644 --- a/src/pages/detail/deploy/index.vue +++ b/src/pages/detail/deploy/index.vue @@ -90,7 +90,7 @@ import { changeChartsTheme } from '@/utils/color'; import { prefix } from '@/config/global'; import { ResDataType } from '@/types/interface'; -import request from '@/utils/request'; +import { request } from '@/utils/request'; echarts.use([ TitleComponent, @@ -115,7 +115,7 @@ const pagination = ref({ const fetchData = async () => { try { - const res: ResDataType = await request.get('/api/get-project-list'); + const res: ResDataType = await request.get({ url: '/api/get-project-list' }); if (res.code === 0) { const { list = [] } = res.data; data.value = list; diff --git a/src/pages/list/base/index.vue b/src/pages/list/base/index.vue index 1fb4b45..d0c3ea9 100644 --- a/src/pages/list/base/index.vue +++ b/src/pages/list/base/index.vue @@ -83,7 +83,7 @@ import { MessagePlugin } from 'tdesign-vue-next'; import { CONTRACT_STATUS, CONTRACT_TYPES, CONTRACT_PAYMENT_TYPES } from '@/constants'; import Trend from '@/components/trend/index.vue'; import { ResDataType } from '@/types/interface'; -import request from '@/utils/request'; +import { request } from '@/utils/request'; import { useSettingStore } from '@/store'; import { COLUMNS } from './constants'; @@ -103,7 +103,7 @@ const dataLoading = ref(false); const fetchData = async () => { dataLoading.value = true; try { - const res: ResDataType = await request.get('/api/get-list'); + const res: ResDataType = await request.get({ url: '/api/get-list' }); if (res.code === 0) { const { list = [] } = res.data; data.value = list; diff --git a/src/pages/list/card/index.vue b/src/pages/list/card/index.vue index 603a9e0..beef005 100644 --- a/src/pages/list/card/index.vue +++ b/src/pages/list/card/index.vue @@ -73,7 +73,7 @@ import { SearchIcon } from 'tdesign-icons-vue-next'; import { MessagePlugin } from 'tdesign-vue-next'; import ProductCard from '@/components/product-card/index.vue'; import DialogForm from './components/DialogForm.vue'; -import request from '@/utils/request'; +import { request } from '@/utils/request'; import { ResDataType } from '@/types/interface'; const INITIAL_DATA = { @@ -93,7 +93,7 @@ const dataLoading = ref(true); const fetchData = async () => { try { - const res: ResDataType = await request.get('/api/get-card-list'); + const res: ResDataType = await request.get({ url: '/api/get-card-list' }); if (res.code === 0) { const { list = [] } = res.data; productList.value = list; diff --git a/src/pages/list/components/CommonTable.vue b/src/pages/list/components/CommonTable.vue index df55f78..bba1402 100644 --- a/src/pages/list/components/CommonTable.vue +++ b/src/pages/list/components/CommonTable.vue @@ -117,7 +117,7 @@ import { ref, computed, onMounted } from 'vue'; import { MessagePlugin } from 'tdesign-vue-next'; import Trend from '@/components/trend/index.vue'; -import request from '@/utils/request'; +import { request } from '@/utils/request'; import { ResDataType } from '@/types/interface'; import { useSettingStore } from '@/store'; @@ -199,7 +199,7 @@ const dataLoading = ref(false); const fetchData = async () => { dataLoading.value = true; try { - const res: ResDataType = await request.get('/api/get-list'); + const res: ResDataType = await request.get({ url: '/api/get-list' }); if (res.code === 0) { const { list = [] } = res.data; data.value = list; diff --git a/src/types/axios.d.ts b/src/types/axios.d.ts new file mode 100644 index 0000000..0a28f0c --- /dev/null +++ b/src/types/axios.d.ts @@ -0,0 +1,29 @@ +import { AxiosRequestConfig } from 'axios'; + +export interface RequestOptions { + apiUrl?: string; + isJoinPrefix?: boolean; + urlPrefix?: string; + joinParamsToUrl?: boolean; + formatDate?: boolean; + isTransformResponse?: boolean; + isReturnNativeResponse?: boolean; + ignoreRepeatRequest?: boolean; + joinTime?: boolean; + withToken?: boolean; + retry?: { + count: number; + delay: number; + }; +} + +export interface Result { + code: number; + type: 'success' | 'error' | 'warning'; + message: string; + result: T; +} + +export interface AxiosRequestConfigRetry extends AxiosRequestConfig { + retryCount?: number; +} diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts index 048a228..7a5db4b 100644 --- a/src/types/globals.d.ts +++ b/src/types/globals.d.ts @@ -21,3 +21,5 @@ declare module '*.svg' { const CONTENT: string; export default CONTENT; } + +declare type Recordable = Record; diff --git a/src/utils/request.ts b/src/utils/request.ts deleted file mode 100644 index a2a664b..0000000 --- a/src/utils/request.ts +++ /dev/null @@ -1,53 +0,0 @@ -import axios, { AxiosInstance } from 'axios'; -import proxy from '../config/proxy'; - -const env = import.meta.env.MODE || 'development'; - -const host = env === 'mock' ? '/' : proxy[env].host; // 如果是mock模式 就不配置host 会走本地Mock拦截 - -const CODE = { - LOGIN_TIMEOUT: 1000, - REQUEST_SUCCESS: 0, - REQUEST_FAILED: 1001, -}; - -const instance: AxiosInstance = axios.create({ - baseURL: host, - timeout: 5000, - withCredentials: true, -}); - -instance.interceptors.response.use( - (response) => { - if (response.status === 200) { - const { data } = response; - if (data.code === CODE.REQUEST_SUCCESS) { - return data; - } - } - return response; - }, - (err) => { - const { config } = err; - - if (!config || !config.retry) return Promise.reject(err); - - config.retryCount = config.retryCount || 0; - - if (config.retryCount >= config.retry) { - return Promise.reject(err); - } - - config.retryCount += 1; - - const backoff = new Promise((resolve) => { - setTimeout(() => { - resolve(null); - }, config.retryDelay || 1); - }); - - return backoff.then(() => instance(config)); - }, -); - -export default instance; diff --git a/src/utils/request/Axios.ts b/src/utils/request/Axios.ts new file mode 100644 index 0000000..ca7c1ed --- /dev/null +++ b/src/utils/request/Axios.ts @@ -0,0 +1,182 @@ +import axios, { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from 'axios'; +import { stringify } from 'qs'; +import isFunction from 'lodash/isFunction'; +import cloneDeep from 'lodash/cloneDeep'; +import { CreateAxiosOptions } from './AxiosTransform'; +import { AxiosCanceler } from './AxiosCancel'; +import { AxiosRequestConfigRetry, RequestOptions, Result } from '@/types/axios'; + +// Axios模块 +export class VAxios { + // axios句柄 + private instance: AxiosInstance; + + // axios选项 + private readonly options: CreateAxiosOptions; + + constructor(options: CreateAxiosOptions) { + this.options = options; + this.instance = axios.create(options); + this.setupInterceptors(); + } + + // 创建axios句柄 + private createAxios(config: CreateAxiosOptions): void { + this.instance = axios.create(config); + } + + // 获取数据处理 + private getTransform() { + const { transform } = this.options; + return transform; + } + + // 获取句柄 + getAxios(): AxiosInstance { + return this.instance; + } + + // 配置 axios + configAxios(config: CreateAxiosOptions) { + if (!this.instance) { + return; + } + this.createAxios(config); + } + + // 设置通用头信息 + setHeader(headers: Record): void { + if (!this.instance) { + return; + } + Object.assign(this.instance.defaults.headers, headers); + } + + // 设置拦截器 + private setupInterceptors() { + const transform = this.getTransform(); + if (!transform) { + return; + } + const { requestInterceptors, requestInterceptorsCatch, responseInterceptors, responseInterceptorsCatch } = + transform; + const axiosCanceler = new AxiosCanceler(); + + // 请求配置处理 + this.instance.interceptors.request.use((config: AxiosRequestConfig) => { + const { + headers: { ignoreRepeatRequest }, + } = config; + const ignoreRepeat = ignoreRepeatRequest ?? this.options.requestOptions?.ignoreRepeatRequest; + if (!ignoreRepeat) axiosCanceler.addPending(config); + + if (requestInterceptors && isFunction(requestInterceptors)) { + config = requestInterceptors(config, this.options); + } + return config; + }, undefined); + + // 请求错误处理 + if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) { + this.instance.interceptors.request.use(undefined, requestInterceptorsCatch); + } + + // 响应结果处理 + this.instance.interceptors.response.use((res: AxiosResponse) => { + if (res) axiosCanceler.removePending(res.config); + if (responseInterceptors && isFunction(responseInterceptors)) { + res = responseInterceptors(res); + } + return res; + }, undefined); + + // 响应错误处理 + if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) { + this.instance.interceptors.response.use(undefined, responseInterceptorsCatch); + } + } + + // 支持Form Data + supportFormData(config: AxiosRequestConfig) { + const headers = config.headers || this.options.headers; + const contentType = headers?.['Content-Type'] || headers?.['content-type']; + + if ( + contentType !== 'application/x-www-form-urlencoded;charset=UTF-8' || + !Reflect.has(config, 'data') || + config.method?.toUpperCase() === 'GET' + ) { + return config; + } + + return { + ...config, + data: stringify(config.data, { arrayFormat: 'brackets' }), + }; + } + + get(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'GET' }, options); + } + + post(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'POST' }, options); + } + + put(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'PUT' }, options); + } + + delete(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'DELETE' }, options); + } + + patch(config: AxiosRequestConfig, options?: RequestOptions): Promise { + return this.request({ ...config, method: 'PATCH' }, options); + } + + // 请求 + async request(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise { + let conf: CreateAxiosOptions = cloneDeep(config); + const transform = this.getTransform(); + + const { requestOptions } = this.options; + + const opt: RequestOptions = { ...requestOptions, ...options }; + + const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {}; + if (beforeRequestHook && isFunction(beforeRequestHook)) { + conf = beforeRequestHook(conf, opt); + } + conf.requestOptions = opt; + + conf = this.supportFormData(conf); + + return new Promise((resolve, reject) => { + this.instance + .request>(!config.retryCount ? conf : config) + .then((res: AxiosResponse) => { + if (transformRequestHook && isFunction(transformRequestHook)) { + try { + const ret = transformRequestHook(res, opt); + resolve(ret); + } catch (err) { + reject(err || new Error('请求错误!')); + } + return; + } + resolve(res as unknown as Promise); + }) + .catch((e: Error | AxiosError) => { + if (requestCatchHook && isFunction(requestCatchHook)) { + reject(requestCatchHook(e, opt)); + return; + } + if (axios.isAxiosError(e)) { + // 在这里重写Axios的错误信息 + } + reject(e); + }); + }); + } +} diff --git a/src/utils/request/AxiosCancel.ts b/src/utils/request/AxiosCancel.ts new file mode 100644 index 0000000..b50d279 --- /dev/null +++ b/src/utils/request/AxiosCancel.ts @@ -0,0 +1,49 @@ +import type { AxiosRequestConfig, Canceler } from 'axios'; +import axios from 'axios'; +import isFunction from 'lodash/isFunction'; + +// 存储请求与取消令牌的键值对列表 +let pendingMap = new Map(); + +export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&'); + +export class AxiosCanceler { + // 添加请求到列表 + addPending(config: AxiosRequestConfig) { + this.removePending(config); + const url = getPendingUrl(config); + config.cancelToken = + config.cancelToken || + new axios.CancelToken((cancel) => { + if (!pendingMap.has(url)) { + // 如果当前没有相同请求就添加 + pendingMap.set(url, cancel); + } + }); + } + + // 清空所有请求 + removeAllPending() { + pendingMap.forEach((cancel) => { + if (cancel && isFunction(cancel)) cancel(); + }); + pendingMap.clear(); + } + + // 移除某个请求 + removePending(config: AxiosRequestConfig) { + const url = getPendingUrl(config); + + if (pendingMap.has(url)) { + // If there is a current request identifier in pending, + // the current request needs to be cancelled and removed + const cancel = pendingMap.get(url); + if (cancel) cancel(url); + pendingMap.delete(url); + } + } + + reset() { + pendingMap = new Map(); + } +} diff --git a/src/utils/request/AxiosTransform.ts b/src/utils/request/AxiosTransform.ts new file mode 100644 index 0000000..2505aaf --- /dev/null +++ b/src/utils/request/AxiosTransform.ts @@ -0,0 +1,37 @@ +import type { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { AxiosError } from 'axios'; +import type { RequestOptions, Result } from '@/types/axios'; + +// 创建Axios选项 +export interface CreateAxiosOptions extends AxiosRequestConfig { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes + authenticationScheme?: string; + // 数据处理 + transform?: AxiosTransform; + // 请求选项 + requestOptions?: RequestOptions; +} + +// Axios 数据处理 +export abstract class AxiosTransform { + // 请求前Hook + beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; + + // 转换前Hook + transformRequestHook?: (res: AxiosResponse, options: RequestOptions) => any; + + // 请求失败处理 + requestCatchHook?: (e: Error | AxiosError, options: RequestOptions) => Promise; + + // 请求前的拦截器 + requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig; + + // 请求后的拦截器 + responseInterceptors?: (res: AxiosResponse) => AxiosResponse; + + // 请求前的拦截器错误处理 + requestInterceptorsCatch?: (error: AxiosError) => void; + + // 请求后的拦截器错误处理 + responseInterceptorsCatch?: (error: AxiosError) => void; +} diff --git a/src/utils/request/index.ts b/src/utils/request/index.ts new file mode 100644 index 0000000..fa87cb9 --- /dev/null +++ b/src/utils/request/index.ts @@ -0,0 +1,195 @@ +// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动 +import isString from 'lodash/isFunction'; +import merge from 'lodash/merge'; +import type { AxiosTransform, CreateAxiosOptions } from './AxiosTransform'; +import { VAxios } from './Axios'; +import proxy from '@/config/proxy'; +import { joinTimestamp, formatRequestDate, setObjToUrlParams } from './utils'; +import { TOKEN_NAME } from '@/config/global'; + +const env = import.meta.env.MODE || 'development'; + +// 如果是mock模式 或 没启用直连代理 就不配置host 会走本地Mock拦截 或 Vite 代理 +const host = env === 'mock' || !proxy.isRequestProxy ? '' : proxy[env].host; + +// 数据处理,方便区分多种处理方式 +const transform: AxiosTransform = { + // 处理请求数据。如果数据不是预期格式,可直接抛出错误 + transformRequestHook: (res, options) => { + const { isTransformResponse, isReturnNativeResponse } = options; + + // 如果204无内容直接返回 + const method = res.config.method?.toLowerCase(); + if (res.status === 204 || method === 'put' || method === 'patch') { + return res; + } + + // 是否返回原生响应头 比如:需要获取响应头时使用该属性 + if (isReturnNativeResponse) { + return res; + } + // 不进行任何处理,直接返回 + // 用于页面代码可能需要直接获取code,data,message这些信息时开启 + if (!isTransformResponse) { + return res.data; + } + + // 错误的时候返回 + const { data } = res; + if (!data) { + throw new Error('请求接口错误'); + } + + // 这里 message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式 + const { message } = data; + + // 这里逻辑可以根据项目进行修改 + const hasSuccess = data && !Reflect.has(data, 'error'); + if (hasSuccess) { + return data; + } + + throw new Error(message); + }, + + // 请求前处理配置 + beforeRequestHook: (config, options) => { + const { apiUrl, isJoinPrefix, urlPrefix, joinParamsToUrl, formatDate, joinTime = true } = options; + + // 添加接口前缀 + if (isJoinPrefix) { + config.url = `${urlPrefix}${config.url}`; + } + + // 将baseUrl拼接 + if (apiUrl && isString(apiUrl)) { + config.url = `${apiUrl}${config.url}`; + } + const params = config.params || {}; + const data = config.data || false; + + if (formatDate && data && !isString(data)) { + formatRequestDate(data); + } + if (config.method?.toUpperCase() === 'GET') { + if (!isString(params)) { + // 给 get 请求加上时间戳参数,避免从缓存中拿数据。 + config.params = Object.assign(params || {}, joinTimestamp(joinTime, false)); + } else { + // 兼容restful风格 + config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`; + config.params = undefined; + } + } else if (!isString(params)) { + if (formatDate) { + formatRequestDate(params); + } + if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) { + config.data = data; + config.params = params; + } else { + // 非GET请求如果没有提供data,则将params视为data + config.data = params; + config.params = undefined; + } + if (joinParamsToUrl) { + config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data }); + } + } else { + // 兼容restful风格 + config.url += params; + config.params = undefined; + } + return config; + }, + + // 请求拦截器处理 + requestInterceptors: (config, options) => { + // 请求之前处理config + const token = localStorage.getItem(TOKEN_NAME); + if (token && (config as Recordable)?.requestOptions?.withToken !== false) { + // jwt token + (config as Recordable).headers.Authorization = options.authenticationScheme + ? `${options.authenticationScheme} ${token}` + : token; + } + return config; + }, + + // 响应拦截器处理 + responseInterceptors: (res) => { + return res; + }, + + // 响应错误处理 + responseInterceptorsCatch: (error: any) => { + const { config } = error; + if (!config || !config.requestOptions.retry) return Promise.reject(error); + + config.retryCount = config.retryCount || 0; + + if (config.retryCount >= config.requestOptions.retry.count) return Promise.reject(error); + + config.retryCount += 1; + + const backoff = new Promise((resolve) => { + setTimeout(() => { + resolve(config); + }, config.requestOptions.retry.delay || 1); + }); + + return backoff.then((config) => request.request(config)); + }, +}; + +function createAxios(opt?: Partial) { + return new VAxios( + merge( + { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes + // 例如: authenticationScheme: 'Bearer' + authenticationScheme: '', + // 超时 + timeout: 10 * 1000, + // 携带Cookie + withCredentials: true, + // 头信息 + headers: { 'Content-Type': 'application/json;charset=UTF-8' }, + // 数据处理方式 + transform, + // 配置项,下面的选项都可以在独立的接口请求中覆盖 + requestOptions: { + // 接口地址 + apiUrl: host, + // 是否自动添加接口前缀 + isJoinPrefix: false, + // 接口前缀 + // 例如: https://www.baidu.com/api + // urlPrefix: '/api' + urlPrefix: '/api', + // 是否返回原生响应头 比如:需要获取响应头时使用该属性 + isReturnNativeResponse: false, + // 需要对返回数据进行处理 + isTransformResponse: true, + // post请求的时候添加参数到url + joinParamsToUrl: false, + // 格式化提交参数时间 + formatDate: true, + // 是否加入时间戳 + joinTime: true, + // 忽略重复请求 + ignoreRepeatRequest: true, + // 是否携带token + withToken: true, + // 重试 + retry: { + count: 3, + delay: 1000, + }, + }, + }, + opt || {}, + ), + ); +} +export const request = createAxios(); diff --git a/src/utils/request/utils.ts b/src/utils/request/utils.ts new file mode 100644 index 0000000..9c88203 --- /dev/null +++ b/src/utils/request/utils.ts @@ -0,0 +1,54 @@ +import isString from 'lodash/isString'; +import isObject from 'lodash/isObject'; + +const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export function joinTimestamp(join: boolean, restful: T): T extends true ? string : object; + +export function joinTimestamp(join: boolean, restful = false): string | object { + if (!join) { + return restful ? '' : {}; + } + const now = new Date().getTime(); + if (restful) { + return `?_t=${now}`; + } + return { _t: now }; +} + +// 格式化提交参数时间 +export function formatRequestDate(params: Recordable) { + if (Object.prototype.toString.call(params) !== '[object Object]') { + return; + } + + for (const key in params) { + // eslint-disable-next-line no-underscore-dangle + if (params[key] && params[key]._isAMomentObject) { + params[key] = params[key].format(DATE_TIME_FORMAT); + } + if (isString(key)) { + const value = params[key]; + if (value) { + try { + params[key] = isString(value) ? value.trim() : value; + } catch (error: any) { + throw new Error(error); + } + } + } + if (isObject(params[key])) { + formatRequestDate(params[key]); + } + } +} + +// 将对象转为Url参数 +export function setObjToUrlParams(baseUrl: string, obj: object): string { + let parameters = ''; + for (const key in obj) { + parameters += `${key}=${encodeURIComponent(obj[key])}&`; + } + parameters = parameters.replace(/&$/, ''); + return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters; +}