feat: initial commit
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import Cookies from "js-cookie";
|
||||
import { storageSession } from "@pureadmin/utils";
|
||||
import { useUserStoreHook } from "@/store/modules/user";
|
||||
import { aesEncrypt, aesDecrypt } from "@/utils/crypt";
|
||||
import { TokenDTO } from "@/api/common/login";
|
||||
|
||||
/**
|
||||
* 原版前端token实现
|
||||
*/
|
||||
export interface DataInfo<T> {
|
||||
/** token */
|
||||
accessToken: string;
|
||||
/** `accessToken`的过期时间(时间戳) */
|
||||
expires: T;
|
||||
/** 用于调用刷新accessToken的接口时所需的token */
|
||||
refreshToken: string;
|
||||
/** 用户名 */
|
||||
username?: string;
|
||||
/** 当前登陆用户的角色 */
|
||||
roles?: Array<string>;
|
||||
}
|
||||
|
||||
export const sessionKey = "user-info";
|
||||
export const tokenKey = "authorized-token";
|
||||
export const isRememberMeKey = "ag-is-remember-me";
|
||||
export const passwordKey = "ag-password";
|
||||
|
||||
/** 获取`token` */
|
||||
export function getToken(): TokenDTO {
|
||||
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
|
||||
return Cookies.get(tokenKey)
|
||||
? JSON.parse(Cookies.get(tokenKey))
|
||||
: storageSession().getItem<TokenDTO>(sessionKey)?.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后端处理token
|
||||
*/
|
||||
export function setTokenFromBackend(data: TokenDTO): void {
|
||||
const cookieString = JSON.stringify(data);
|
||||
Cookies.set(tokenKey, cookieString);
|
||||
|
||||
useUserStoreHook().SET_USERNAME(data.currentUser.userInfo.username);
|
||||
useUserStoreHook().SET_ROLES([data.currentUser.roleKey]);
|
||||
storageSession().setItem(sessionKey, data);
|
||||
}
|
||||
|
||||
/** 删除`token`以及key值为`user-info`的session信息 */
|
||||
export function removeToken() {
|
||||
Cookies.remove(tokenKey);
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
/** 将密码加密后 存入cookies中 */
|
||||
export function savePassword(password: string) {
|
||||
const encryptPassword = aesEncrypt(password);
|
||||
Cookies.set(passwordKey, encryptPassword);
|
||||
}
|
||||
|
||||
/** 将密码中cookies中删除 */
|
||||
export function removePassword() {
|
||||
Cookies.remove(passwordKey);
|
||||
}
|
||||
|
||||
/** 获取密码 并解密 */
|
||||
export function getPassword(): string {
|
||||
const encryptPassword = Cookies.get(passwordKey);
|
||||
if (
|
||||
encryptPassword !== null &&
|
||||
encryptPassword !== undefined &&
|
||||
encryptPassword.trim() !== ""
|
||||
) {
|
||||
return aesDecrypt(encryptPassword);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function saveIsRememberMe(isRememberMe: boolean) {
|
||||
Cookies.set(isRememberMeKey, isRememberMe.toString());
|
||||
}
|
||||
|
||||
export function getIsRememberMe() {
|
||||
const value = Cookies.get(isRememberMeKey);
|
||||
return value === "true";
|
||||
}
|
||||
|
||||
/** 格式化token(jwt格式) */
|
||||
export const formatToken = (token: string): string => {
|
||||
return "Bearer " + token;
|
||||
};
|
||||
@@ -0,0 +1,141 @@
|
||||
import { PaginationProps, TableColumn } from "@pureadmin/table";
|
||||
import { Sort } from "element-plus";
|
||||
import { utils, writeFile } from "xlsx";
|
||||
import { message } from "./message";
|
||||
import { pinyin } from "pinyin-pro";
|
||||
|
||||
export class CommonUtils {
|
||||
static getBeginTimeSafely(timeRange: string[]): string {
|
||||
if (timeRange == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (timeRange.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (timeRange[0] == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return timeRange[0];
|
||||
}
|
||||
|
||||
static getEndTimeSafely(timeRange: string[]): string {
|
||||
if (timeRange == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (timeRange.length <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (timeRange[1] == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return timeRange[1];
|
||||
}
|
||||
|
||||
static fillPaginationParams(
|
||||
baseQuery: BasePageQuery,
|
||||
pagination: PaginationProps
|
||||
) {
|
||||
baseQuery.pageNum = pagination.currentPage;
|
||||
baseQuery.pageSize = pagination.pageSize;
|
||||
}
|
||||
|
||||
static fillSortParams(baseQuery: BasePageQuery, sort: Sort) {
|
||||
if (sort == null) {
|
||||
return;
|
||||
}
|
||||
baseQuery.orderColumn = sort.prop;
|
||||
baseQuery.orderDirection = sort.order;
|
||||
}
|
||||
|
||||
/** 适用于BaseQuery中固定的时间参数 beginTime和endTime参数 */
|
||||
static fillTimeRangeParams(baseQuery: any, timeRange: string[]) {
|
||||
if (timeRange == null || timeRange.length == 0 || timeRange === undefined) {
|
||||
baseQuery["beginTime"] = undefined;
|
||||
baseQuery["endTime"] = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (baseQuery == null || baseQuery === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
baseQuery["beginTime"] = CommonUtils.getBeginTimeSafely(timeRange);
|
||||
baseQuery["endTime"] = CommonUtils.getEndTimeSafely(timeRange);
|
||||
}
|
||||
|
||||
static exportExcel(
|
||||
columns: TableColumnList,
|
||||
originalDataList: any[],
|
||||
excelName: string
|
||||
) {
|
||||
if (
|
||||
!Array.isArray(columns) ||
|
||||
!Array.isArray(originalDataList) ||
|
||||
typeof excelName !== "string"
|
||||
) {
|
||||
message("参数异常,导出失败", { type: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
// columns和dataList为空的话 弹出提示 不执行导出
|
||||
if (columns.length === 0 || originalDataList.length === 0) {
|
||||
message("无法导出空列表", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
|
||||
const titleList: string[] = [];
|
||||
const dataKeyList: string[] = [];
|
||||
// 把columns里面的label取出来作为excel的列标题,把prop取出来等下从dataList里面根据作为key取对象中的值
|
||||
columns.forEach((column: TableColumn) => {
|
||||
if (column.label && column.prop) {
|
||||
titleList.push(column.label);
|
||||
dataKeyList.push(column.prop as string);
|
||||
}
|
||||
});
|
||||
|
||||
const excelDataList: string[][] = originalDataList.map(item => {
|
||||
const arr = [];
|
||||
dataKeyList.forEach(dataKey => {
|
||||
arr.push(item[dataKey]);
|
||||
});
|
||||
return arr;
|
||||
});
|
||||
|
||||
excelDataList.unshift(titleList);
|
||||
|
||||
const workSheet = utils.aoa_to_sheet(excelDataList);
|
||||
const workBook = utils.book_new();
|
||||
utils.book_append_sheet(workBook, workSheet, excelName);
|
||||
writeFile(workBook, `${excelName}.xlsx`);
|
||||
}
|
||||
|
||||
static paginateList(dataList: any[], pagination: PaginationProps): any[] {
|
||||
// 计算起始索引
|
||||
const startIndex = (pagination.currentPage - 1) * pagination.pageSize;
|
||||
|
||||
// 截取数组
|
||||
const endIndex = startIndex + pagination.pageSize;
|
||||
const paginatedList = dataList.slice(startIndex, endIndex);
|
||||
|
||||
// 返回截取后的数组
|
||||
return paginatedList;
|
||||
}
|
||||
|
||||
static toPinyin(chineseStr: string): string {
|
||||
if (chineseStr == null || chineseStr === undefined || chineseStr === "") {
|
||||
return chineseStr;
|
||||
}
|
||||
|
||||
const pinyinStr = pinyin(chineseStr, { toneType: "none" });
|
||||
return pinyinStr.replace(/\s/g, "");
|
||||
}
|
||||
|
||||
// 私有构造函数,防止类被实例化
|
||||
private constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { JSEncrypt } from "jsencrypt";
|
||||
import * as CryptoJS from "crypto-js";
|
||||
import { isEmpty } from "@pureadmin/utils";
|
||||
|
||||
// 密钥对生成 http://web.chacuo.net/netrsakeypair
|
||||
// RSA 公钥 对应的私钥放在后端项目的application-basic.yml文件下
|
||||
const publicKey =
|
||||
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCh6HkK+rCM37FAzCHVythTc6pxvr551K07CRhdX/NjCddHAuQMOd/57R5fiIwgVNEfCsD1cIyS6A8IWj4DtJLR2t29JehPpqiFSJ4hNtDcLNxNJiYRcCQvyMQeyQIPE5Ljc35c72YwDtQAsIJChsauyLrc+E6HC3gn1JDm18HNXwIDAQAB";
|
||||
|
||||
// 加密
|
||||
export function rsaEncrypt(txt): string {
|
||||
const encryptor = new JSEncrypt();
|
||||
// 设置公钥
|
||||
encryptor.setPublicKey(publicKey);
|
||||
// 对数据进行加密
|
||||
const encryptedValue = encryptor.encrypt(txt);
|
||||
|
||||
// Check if the encrypted value is a boolean
|
||||
if (typeof encryptedValue === "boolean") {
|
||||
throw new Error("Encryption failed: Encrypted value returned a boolean");
|
||||
}
|
||||
|
||||
return encryptedValue;
|
||||
}
|
||||
|
||||
const aesKey = "agileboot1234567";
|
||||
|
||||
export function aesEncrypt(txt): string {
|
||||
if (isEmpty(txt)) {
|
||||
return null;
|
||||
}
|
||||
const message = CryptoJS.enc.Utf8.parse(txt);
|
||||
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
|
||||
const iv = CryptoJS.enc.Utf8.parse(aesKey);
|
||||
|
||||
const encrypted = CryptoJS.AES.encrypt(message, secretPassphrase, {
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
iv
|
||||
}).toString();
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
export function aesDecrypt(txtEncrypt): string {
|
||||
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
|
||||
const iv = CryptoJS.enc.Utf8.parse(aesKey);
|
||||
const decrypted = CryptoJS.AES.decrypt(txtEncrypt, secretPassphrase, {
|
||||
mode: CryptoJS.mode.CBC,
|
||||
padding: CryptoJS.pad.Pkcs7,
|
||||
iv
|
||||
}).toString(CryptoJS.enc.Utf8);
|
||||
return decrypted;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 如果项目出现 `global is not defined` 报错,可能是您引入某个库的问题,比如 aws-sdk-js https://github.com/aws/aws-sdk-js
|
||||
// 解决办法就是将该文件引入 src/main.ts 即可 import "@/utils/globalPolyfills";
|
||||
if (typeof (window as any).global === "undefined") {
|
||||
(window as any).global = window;
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,311 @@
|
||||
import Axios, {
|
||||
AxiosInstance,
|
||||
AxiosRequestConfig,
|
||||
CustomParamsSerializer
|
||||
} from "axios";
|
||||
import {
|
||||
PureHttpError,
|
||||
RequestMethods,
|
||||
PureHttpResponse,
|
||||
PureHttpRequestConfig
|
||||
} from "./types.d";
|
||||
import { stringify } from "qs";
|
||||
import NProgress from "../progress";
|
||||
import { getToken, formatToken } from "@/utils/auth";
|
||||
import { message } from "../message";
|
||||
import { ElMessageBox } from "element-plus";
|
||||
import { router } from "@/router";
|
||||
import { removeToken } from "@/utils/auth";
|
||||
import { downloadByData } from "@pureadmin/utils";
|
||||
// console.log("Utils:" + router);
|
||||
|
||||
const { VITE_APP_BASE_API } = import.meta.env;
|
||||
// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
|
||||
const defaultConfig: AxiosRequestConfig = {
|
||||
// 请求超时时间
|
||||
timeout: 10000,
|
||||
// 后端请求地址
|
||||
baseURL: VITE_APP_BASE_API,
|
||||
headers: {
|
||||
Accept: "application/json, text/plain, */*",
|
||||
"Content-Type": "application/json",
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
},
|
||||
// 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
|
||||
paramsSerializer: {
|
||||
serialize: stringify as unknown as CustomParamsSerializer
|
||||
}
|
||||
};
|
||||
|
||||
class PureHttp {
|
||||
constructor() {
|
||||
this.httpInterceptorsRequest();
|
||||
this.httpInterceptorsResponse();
|
||||
}
|
||||
|
||||
/** token过期后,暂存待执行的请求 */
|
||||
private static requests = [];
|
||||
|
||||
/** 防止重复刷新token */
|
||||
private static isRefreshing = false;
|
||||
|
||||
/** 初始化配置对象 */
|
||||
private static initConfig: PureHttpRequestConfig = {};
|
||||
|
||||
/** 保存当前Axios实例对象 */
|
||||
private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);
|
||||
|
||||
/** 重连原始请求 */
|
||||
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
||||
return new Promise(resolve => {
|
||||
PureHttp.requests.push((token: string) => {
|
||||
config.headers["Authorization"] = formatToken(token);
|
||||
resolve(config);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 请求拦截 */
|
||||
private httpInterceptorsRequest(): void {
|
||||
PureHttp.axiosInstance.interceptors.request.use(
|
||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||
// 开启进度条动画
|
||||
NProgress.start();
|
||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||
if (typeof config.beforeRequestCallback === "function") {
|
||||
config.beforeRequestCallback(config);
|
||||
return config;
|
||||
}
|
||||
if (PureHttp.initConfig.beforeRequestCallback) {
|
||||
PureHttp.initConfig.beforeRequestCallback(config);
|
||||
return config;
|
||||
}
|
||||
/** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
|
||||
const whiteList = [
|
||||
"/refreshToken",
|
||||
"/login",
|
||||
"/captchaImage",
|
||||
"/getConfig"
|
||||
];
|
||||
return whiteList.some(v => config.url.endsWith(v))
|
||||
? config
|
||||
: new Promise(resolve => {
|
||||
const data = getToken();
|
||||
config.headers["Authorization"] = formatToken(data.token);
|
||||
resolve(config);
|
||||
});
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 响应拦截 */
|
||||
private httpInterceptorsResponse(): void {
|
||||
const instance = PureHttp.axiosInstance;
|
||||
instance.interceptors.response.use(
|
||||
async (response: PureHttpResponse) => {
|
||||
let code = undefined;
|
||||
let msg = undefined;
|
||||
|
||||
// 后台返回的二进制流
|
||||
if (response.data instanceof Blob) {
|
||||
// 返回二进制流的时候 可能出错 这时候返回的错误是Json格式
|
||||
if (response.data.type === "application/json") {
|
||||
const text = await this.readBlobAsText(response.data);
|
||||
const json = JSON.parse(text);
|
||||
// 提取错误消息中的code和msg
|
||||
code = json.code;
|
||||
msg = json.msg;
|
||||
} else {
|
||||
NProgress.done();
|
||||
return response.data;
|
||||
}
|
||||
// 正常的返回类型 直接获取code和msg字段
|
||||
} else {
|
||||
code = response.data.code;
|
||||
msg = response.data.msg;
|
||||
}
|
||||
|
||||
// 如果不存在code说明后端格式有问题
|
||||
if (!code) {
|
||||
msg = "服务器返回数据结构有误";
|
||||
}
|
||||
|
||||
// 请求返回失败时,有业务错误时,弹出错误提示
|
||||
if (response.data.code !== 0) {
|
||||
// token失效时弹出过期提示
|
||||
if (response.data.code === 106) {
|
||||
ElMessageBox.confirm(
|
||||
"登录状态已过期,您可以继续留在该页面,或者重新登录",
|
||||
"系统提示",
|
||||
{
|
||||
confirmButtonText: "重新登录",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
removeToken();
|
||||
router.push("/login");
|
||||
})
|
||||
.catch(() => {
|
||||
message("取消重新登录", { type: "info" });
|
||||
});
|
||||
NProgress.done();
|
||||
return Promise.reject(msg);
|
||||
} else {
|
||||
// 其余情况弹出错误提示框
|
||||
message(msg, { type: "error" });
|
||||
NProgress.done();
|
||||
return Promise.reject(msg);
|
||||
}
|
||||
}
|
||||
|
||||
const $config = response.config;
|
||||
// 关闭进度条动画
|
||||
NProgress.done();
|
||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||
if (typeof $config.beforeResponseCallback === "function") {
|
||||
$config.beforeResponseCallback(response);
|
||||
return response.data;
|
||||
}
|
||||
if (PureHttp.initConfig.beforeResponseCallback) {
|
||||
PureHttp.initConfig.beforeResponseCallback(response);
|
||||
return response.data;
|
||||
}
|
||||
return response.data;
|
||||
},
|
||||
(error: PureHttpError) => {
|
||||
const $error = error;
|
||||
$error.isCancelRequest = Axios.isCancel($error);
|
||||
// 关闭进度条动画
|
||||
NProgress.done();
|
||||
// 所有的响应异常 区分来源为取消请求/非取消请求
|
||||
return Promise.reject($error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** 通用请求工具函数 */
|
||||
public request<T>(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
param?: AxiosRequestConfig,
|
||||
axiosConfig?: PureHttpRequestConfig
|
||||
): Promise<T> {
|
||||
const config = {
|
||||
method,
|
||||
url,
|
||||
...param,
|
||||
...axiosConfig
|
||||
} as PureHttpRequestConfig;
|
||||
|
||||
// 单独处理自定义请求/响应回调
|
||||
return new Promise((resolve, reject) => {
|
||||
PureHttp.axiosInstance
|
||||
.request(config)
|
||||
.then((response: undefined) => {
|
||||
resolve(response);
|
||||
})
|
||||
.catch(error => {
|
||||
// 某些情况网络失效,此时直接进入error流程,所以在这边也进行拦截
|
||||
if (error.response && error.response.status >= 500) {
|
||||
message("网络异常", { type: "error" });
|
||||
}
|
||||
|
||||
if (
|
||||
error.response &&
|
||||
error.response.status >= 400 &&
|
||||
error.response.status < 500
|
||||
) {
|
||||
message("请求接口不存在", { type: "error" });
|
||||
}
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 从二进制流中读取文本 */
|
||||
async readBlobAsText(blob: Blob): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const text = reader.result as string;
|
||||
resolve(text);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsText(blob, "UTF-8");
|
||||
});
|
||||
}
|
||||
|
||||
/** 单独抽离的post工具函数 */
|
||||
public post<T, P>(
|
||||
url: string,
|
||||
params?: AxiosRequestConfig<T>,
|
||||
config?: PureHttpRequestConfig
|
||||
): Promise<P> {
|
||||
return this.request<P>("post", url, params, config);
|
||||
}
|
||||
|
||||
/** 单独抽离的get工具函数 */
|
||||
public get<T, P>(
|
||||
url: string,
|
||||
params?: AxiosRequestConfig<T>,
|
||||
config?: PureHttpRequestConfig
|
||||
): Promise<P> {
|
||||
return this.request<P>("get", url, params, config);
|
||||
}
|
||||
|
||||
/** download文件方法 从后端获取文件流 */
|
||||
public download(
|
||||
url: string,
|
||||
fileName: string,
|
||||
params?: AxiosRequestConfig
|
||||
): void {
|
||||
this.get(url, params, {
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
responseType: "blob"
|
||||
}).then((data: Blob) => {
|
||||
downloadByData(data, fileName);
|
||||
});
|
||||
}
|
||||
|
||||
// .post(url, params, {
|
||||
// transformRequest: [params => encodeURIParams(params)],
|
||||
// headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
// responseType: "blob"
|
||||
// })
|
||||
// .then(async data => {
|
||||
// const isLogin = await isBlobData(data);
|
||||
// if (isLogin) {
|
||||
// const blob = new Blob([data]);
|
||||
// saveAs(blob, filename);
|
||||
// } else {
|
||||
// const resText = await data.text();
|
||||
// const rspObj = JSON.parse(resText);
|
||||
// const errMsg =
|
||||
// errorCode[rspObj.code] || rspObj.msg || errorCode.default;
|
||||
// ElMessage.error(errMsg);
|
||||
// }
|
||||
// downloadLoadingInstance.close();
|
||||
// })
|
||||
// .catch(r => {
|
||||
// console.error(r);
|
||||
// ElMessage.error("下载文件出现错误,请联系管理员!");
|
||||
// downloadLoadingInstance.close();
|
||||
// });
|
||||
|
||||
// axios
|
||||
// .get("https://pure-admin.github.io/pure-admin-doc/img/pure.png", {
|
||||
// responseType: "blob"
|
||||
// })
|
||||
// .then(({ data }) => {
|
||||
// downloadByData(data, "test-data.png");
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
export const http = new PureHttp();
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import Axios, {
|
||||
Method,
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
AxiosRequestConfig
|
||||
} from "axios";
|
||||
|
||||
export type resultType = {
|
||||
accessToken?: string;
|
||||
};
|
||||
|
||||
export type RequestMethods = Extract<
|
||||
Method,
|
||||
"get" | "post" | "put" | "delete" | "patch" | "option" | "head"
|
||||
>;
|
||||
|
||||
export interface PureHttpError extends AxiosError {
|
||||
isCancelRequest?: boolean;
|
||||
}
|
||||
|
||||
export interface PureHttpResponse extends AxiosResponse {
|
||||
config: PureHttpRequestConfig;
|
||||
}
|
||||
|
||||
export interface PureHttpRequestConfig extends AxiosRequestConfig {
|
||||
beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
|
||||
beforeResponseCallback?: (response: PureHttpResponse) => void;
|
||||
}
|
||||
|
||||
export default class PureHttp {
|
||||
request<T>(
|
||||
method: RequestMethods,
|
||||
url: string,
|
||||
param?: AxiosRequestConfig,
|
||||
axiosConfig?: PureHttpRequestConfig
|
||||
): Promise<T>;
|
||||
post<T, P>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: PureHttpRequestConfig
|
||||
): Promise<P>;
|
||||
get<T, P>(
|
||||
url: string,
|
||||
params?: T,
|
||||
config?: PureHttpRequestConfig
|
||||
): Promise<P>;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { type VNode } from "vue";
|
||||
import { isFunction } from "@pureadmin/utils";
|
||||
import { type MessageHandler, ElMessage } from "element-plus";
|
||||
|
||||
type messageStyle = "el" | "antd";
|
||||
type messageTypes = "info" | "success" | "warning" | "error";
|
||||
|
||||
interface MessageParams {
|
||||
/** 消息类型,可选 `info` 、`success` 、`warning` 、`error` ,默认 `info` */
|
||||
type?: messageTypes;
|
||||
/** 自定义图标,该属性会覆盖 `type` 的图标 */
|
||||
icon?: any;
|
||||
/** 是否将 `message` 属性作为 `HTML` 片段处理,默认 `false` */
|
||||
dangerouslyUseHTMLString?: boolean;
|
||||
/** 消息风格,可选 `el` 、`antd` ,默认 `antd` */
|
||||
customClass?: messageStyle;
|
||||
/** 显示时间,单位为毫秒。设为 `0` 则不会自动关闭,`element-plus` 默认是 `3000` ,平台改成默认 `2000` */
|
||||
duration?: number;
|
||||
/** 是否显示关闭按钮,默认值 `false` */
|
||||
showClose?: boolean;
|
||||
/** 文字是否居中,默认值 `false` */
|
||||
center?: boolean;
|
||||
/** `Message` 距离窗口顶部的偏移量,默认 `20` */
|
||||
offset?: number;
|
||||
/** 设置组件的根元素,默认 `document.body` */
|
||||
appendTo?: string | HTMLElement;
|
||||
/** 合并内容相同的消息,不支持 `VNode` 类型的消息,默认值 `false` */
|
||||
grouping?: boolean;
|
||||
/** 关闭时的回调函数, 参数为被关闭的 `message` 实例 */
|
||||
onClose?: Function | null;
|
||||
}
|
||||
|
||||
/** 用法非常简单,参考 src/views/components/message/index.vue 文件 */
|
||||
|
||||
/**
|
||||
* `Message` 消息提示函数
|
||||
*/
|
||||
const message = (
|
||||
message: string | VNode | (() => VNode),
|
||||
params?: MessageParams
|
||||
): MessageHandler => {
|
||||
if (!params) {
|
||||
return ElMessage({
|
||||
message,
|
||||
customClass: "pure-message"
|
||||
});
|
||||
} else {
|
||||
const {
|
||||
icon,
|
||||
type = "info",
|
||||
dangerouslyUseHTMLString = false,
|
||||
customClass = "antd",
|
||||
duration = 2000,
|
||||
showClose = false,
|
||||
center = false,
|
||||
offset = 20,
|
||||
appendTo = document.body,
|
||||
grouping = false,
|
||||
onClose
|
||||
} = params;
|
||||
|
||||
return ElMessage({
|
||||
message,
|
||||
type,
|
||||
icon,
|
||||
dangerouslyUseHTMLString,
|
||||
duration,
|
||||
showClose,
|
||||
center,
|
||||
offset,
|
||||
appendTo,
|
||||
grouping,
|
||||
// 全局搜 pure-message 即可知道该类的样式位置
|
||||
customClass: customClass === "antd" ? "pure-message" : "",
|
||||
onClose: () => (isFunction(onClose) ? onClose() : null)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭所有 `Message` 消息提示函数
|
||||
*/
|
||||
const closeAllMessage = (): void => ElMessage.closeAll();
|
||||
|
||||
export { message, closeAllMessage };
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Emitter } from "mitt";
|
||||
import mitt from "mitt";
|
||||
|
||||
/** 全局公共事件需要在此处添加类型 */
|
||||
type Events = {
|
||||
openPanel: string;
|
||||
tagViewsChange: string;
|
||||
tagViewsShowModel: string;
|
||||
logoChange: boolean;
|
||||
changLayoutRoute: string;
|
||||
};
|
||||
|
||||
export const emitter: Emitter<Events> = mitt<Events>();
|
||||
@@ -0,0 +1,214 @@
|
||||
interface PrintFunction {
|
||||
extendOptions: Function;
|
||||
getStyle: Function;
|
||||
setDomHeight: Function;
|
||||
toPrint: Function;
|
||||
}
|
||||
|
||||
const Print = function (dom, options?: object): PrintFunction {
|
||||
options = options || {};
|
||||
// @ts-expect-error
|
||||
if (!(this instanceof Print)) return new Print(dom, options);
|
||||
this.conf = {
|
||||
styleStr: "",
|
||||
// Elements that need to dynamically get and set the height
|
||||
setDomHeightArr: [],
|
||||
// Callback before printing
|
||||
printBeforeFn: null,
|
||||
// Callback after printing
|
||||
printDoneCallBack: null
|
||||
};
|
||||
for (const key in this.conf) {
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (key && options.hasOwnProperty(key)) {
|
||||
this.conf[key] = options[key];
|
||||
}
|
||||
}
|
||||
if (typeof dom === "string") {
|
||||
this.dom = document.querySelector(dom);
|
||||
} else {
|
||||
this.dom = this.isDOM(dom) ? dom : dom.$el;
|
||||
}
|
||||
if (this.conf.setDomHeightArr && this.conf.setDomHeightArr.length) {
|
||||
this.setDomHeight(this.conf.setDomHeightArr);
|
||||
}
|
||||
this.init();
|
||||
};
|
||||
|
||||
Print.prototype = {
|
||||
/**
|
||||
* init
|
||||
*/
|
||||
init: function (): void {
|
||||
const content = this.getStyle() + this.getHtml();
|
||||
this.writeIframe(content);
|
||||
},
|
||||
/**
|
||||
* Configuration property extension
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj2
|
||||
*/
|
||||
extendOptions: function <T>(obj, obj2: T): T {
|
||||
for (const k in obj2) {
|
||||
obj[k] = obj2[k];
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
/**
|
||||
Copy all styles of the original page
|
||||
*/
|
||||
getStyle: function (): string {
|
||||
let str = "";
|
||||
const styles: NodeListOf<Element> = document.querySelectorAll("style,link");
|
||||
for (let i = 0; i < styles.length; i++) {
|
||||
str += styles[i].outerHTML;
|
||||
}
|
||||
str += `<style>.no-print{display:none;}${this.conf.styleStr}</style>`;
|
||||
return str;
|
||||
},
|
||||
// form assignment
|
||||
getHtml: function (): Element {
|
||||
const inputs = document.querySelectorAll("input");
|
||||
const selects = document.querySelectorAll("select");
|
||||
const textareas = document.querySelectorAll("textarea");
|
||||
const canvass = document.querySelectorAll("canvas");
|
||||
|
||||
for (let k = 0; k < inputs.length; k++) {
|
||||
if (inputs[k].type == "checkbox" || inputs[k].type == "radio") {
|
||||
if (inputs[k].checked == true) {
|
||||
inputs[k].setAttribute("checked", "checked");
|
||||
} else {
|
||||
inputs[k].removeAttribute("checked");
|
||||
}
|
||||
} else if (inputs[k].type == "text") {
|
||||
inputs[k].setAttribute("value", inputs[k].value);
|
||||
} else {
|
||||
inputs[k].setAttribute("value", inputs[k].value);
|
||||
}
|
||||
}
|
||||
|
||||
for (let k2 = 0; k2 < textareas.length; k2++) {
|
||||
if (textareas[k2].type == "textarea") {
|
||||
textareas[k2].innerHTML = textareas[k2].value;
|
||||
}
|
||||
}
|
||||
|
||||
for (let k3 = 0; k3 < selects.length; k3++) {
|
||||
if (selects[k3].type == "select-one") {
|
||||
const child = selects[k3].children;
|
||||
for (const i in child) {
|
||||
if (child[i].tagName == "OPTION") {
|
||||
if ((child[i] as any).selected == true) {
|
||||
child[i].setAttribute("selected", "selected");
|
||||
} else {
|
||||
child[i].removeAttribute("selected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let k4 = 0; k4 < canvass.length; k4++) {
|
||||
const imageURL = canvass[k4].toDataURL("image/png");
|
||||
const img = document.createElement("img");
|
||||
img.src = imageURL;
|
||||
img.setAttribute("style", "max-width: 100%;");
|
||||
img.className = "isNeedRemove";
|
||||
canvass[k4].parentNode.insertBefore(img, canvass[k4].nextElementSibling);
|
||||
}
|
||||
|
||||
return this.dom.outerHTML;
|
||||
},
|
||||
/**
|
||||
create iframe
|
||||
*/
|
||||
writeIframe: function (content) {
|
||||
let w: Document | Window;
|
||||
let doc: Document;
|
||||
const iframe: HTMLIFrameElement = document.createElement("iframe");
|
||||
const f: HTMLIFrameElement = document.body.appendChild(iframe);
|
||||
iframe.id = "myIframe";
|
||||
iframe.setAttribute(
|
||||
"style",
|
||||
"position:absolute;width:0;height:0;top:-10px;left:-10px;"
|
||||
);
|
||||
// eslint-disable-next-line prefer-const
|
||||
w = f.contentWindow || f.contentDocument;
|
||||
// eslint-disable-next-line prefer-const
|
||||
doc = f.contentDocument || f.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write(content);
|
||||
doc.close();
|
||||
|
||||
const removes = document.querySelectorAll(".isNeedRemove");
|
||||
for (let k = 0; k < removes.length; k++) {
|
||||
removes[k].parentNode.removeChild(removes[k]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
const _this = this;
|
||||
iframe.onload = function (): void {
|
||||
// Before popping, callback
|
||||
if (_this.conf.printBeforeFn) {
|
||||
_this.conf.printBeforeFn({ doc });
|
||||
}
|
||||
_this.toPrint(w);
|
||||
setTimeout(function () {
|
||||
document.body.removeChild(iframe);
|
||||
// After popup, callback
|
||||
if (_this.conf.printDoneCallBack) {
|
||||
_this.conf.printDoneCallBack();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
},
|
||||
/**
|
||||
Print
|
||||
*/
|
||||
toPrint: function (frameWindow): void {
|
||||
try {
|
||||
setTimeout(function () {
|
||||
frameWindow.focus();
|
||||
try {
|
||||
if (!frameWindow.document.execCommand("print", false, null)) {
|
||||
frameWindow.print();
|
||||
}
|
||||
} catch (e) {
|
||||
frameWindow.print();
|
||||
}
|
||||
frameWindow.close();
|
||||
}, 10);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
isDOM:
|
||||
typeof HTMLElement === "object"
|
||||
? function (obj) {
|
||||
return obj instanceof HTMLElement;
|
||||
}
|
||||
: function (obj) {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj === "object" &&
|
||||
obj.nodeType === 1 &&
|
||||
typeof obj.nodeName === "string"
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Set the height of the specified dom element by getting the existing height of the dom element and setting
|
||||
* @param {Array} arr
|
||||
*/
|
||||
setDomHeight(arr) {
|
||||
if (arr && arr.length) {
|
||||
arr.forEach(name => {
|
||||
const domArr = document.querySelectorAll(name);
|
||||
domArr.forEach(dom => {
|
||||
dom.style.height = dom.offsetHeight + "px";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Print;
|
||||
@@ -0,0 +1,17 @@
|
||||
import NProgress from "nprogress";
|
||||
import "nprogress/nprogress.css";
|
||||
|
||||
NProgress.configure({
|
||||
// 动画方式
|
||||
easing: "ease",
|
||||
// 递增进度条的速度
|
||||
speed: 500,
|
||||
// 是否显示加载ico
|
||||
showSpinner: false,
|
||||
// 自动递增间隔
|
||||
trickleSpeed: 200,
|
||||
// 初始化时的最小百分比
|
||||
minimum: 0.3
|
||||
});
|
||||
|
||||
export default NProgress;
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { CSSProperties, VNodeChild } from "vue";
|
||||
import {
|
||||
createTypes,
|
||||
toValidableType,
|
||||
VueTypesInterface,
|
||||
VueTypeValidableDef
|
||||
} from "vue-types";
|
||||
|
||||
export type VueNode = VNodeChild | JSX.Element;
|
||||
|
||||
type PropTypes = VueTypesInterface & {
|
||||
readonly style: VueTypeValidableDef<CSSProperties>;
|
||||
readonly VNodeChild: VueTypeValidableDef<VueNode>;
|
||||
};
|
||||
|
||||
const newPropTypes = createTypes({
|
||||
func: undefined,
|
||||
bool: undefined,
|
||||
string: undefined,
|
||||
number: undefined,
|
||||
object: undefined,
|
||||
integer: undefined
|
||||
}) as PropTypes;
|
||||
|
||||
// 从 vue-types v5.0 开始,extend()方法已经废弃,当前已改为官方推荐的ES6+方法 https://dwightjack.github.io/vue-types/advanced/extending-vue-types.html#the-extend-method
|
||||
export default class propTypes extends newPropTypes {
|
||||
// a native-like validator that supports the `.validable` method
|
||||
static get style() {
|
||||
return toValidableType("style", {
|
||||
type: [String, Object]
|
||||
});
|
||||
}
|
||||
|
||||
static get VNodeChild() {
|
||||
return toValidableType("VNodeChild", {
|
||||
type: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 响应式storage
|
||||
import { App } from "vue";
|
||||
import Storage from "responsive-storage";
|
||||
import { routerArrays } from "@/layout/types";
|
||||
import { responsiveStorageNameSpace } from "@/config";
|
||||
|
||||
export const injectResponsiveStorage = (app: App, config: ServerConfigs) => {
|
||||
const nameSpace = responsiveStorageNameSpace();
|
||||
const configObj = Object.assign(
|
||||
{
|
||||
// layout模式以及主题
|
||||
layout: Storage.getData("layout", nameSpace) ?? {
|
||||
layout: config.Layout ?? "vertical",
|
||||
theme: config.Theme ?? "default",
|
||||
darkMode: config.DarkMode ?? false,
|
||||
sidebarStatus: config.SidebarStatus ?? true,
|
||||
epThemeColor: config.EpThemeColor ?? "#409EFF"
|
||||
},
|
||||
configure: Storage.getData("configure", nameSpace) ?? {
|
||||
grey: config.Grey ?? false,
|
||||
weak: config.Weak ?? false,
|
||||
hideTabs: config.HideTabs ?? false,
|
||||
showLogo: config.ShowLogo ?? true,
|
||||
showModel: config.ShowModel ?? "smart",
|
||||
multiTagsCache: config.MultiTagsCache ?? false
|
||||
}
|
||||
},
|
||||
config.MultiTagsCache
|
||||
? {
|
||||
// 默认显示顶级菜单tag
|
||||
tags: Storage.getData("tags", nameSpace) ?? routerArrays
|
||||
}
|
||||
: {}
|
||||
);
|
||||
|
||||
app.use(Storage, { nameSpace, memory: configObj });
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const { VITE_PUBLIC_PATH } = import.meta.env;
|
||||
|
||||
export const configConver = () => {
|
||||
if (VITE_PUBLIC_PATH === "./") {
|
||||
return window.location.origin + "/";
|
||||
}
|
||||
return window.location.origin + processPath(VITE_PUBLIC_PATH);
|
||||
};
|
||||
|
||||
function processPath(str: string): string {
|
||||
if (str.startsWith("./")) {
|
||||
str = str.substring(1);
|
||||
} else if (!str.startsWith("/")) {
|
||||
str = "/" + str;
|
||||
}
|
||||
|
||||
if (!str.endsWith("/")) {
|
||||
str += "/";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { removeToken, setTokenFromBackend, type DataInfo } from "./auth";
|
||||
import { subBefore, getQueryMap } from "@pureadmin/utils";
|
||||
|
||||
/**
|
||||
* 简版前端单点登录,根据实际业务自行编写
|
||||
* 划重点:
|
||||
* 判断是否为单点登录,不为则直接返回不再进行任何逻辑处理,下面是单点登录后的逻辑处理
|
||||
* 1.清空本地旧信息;
|
||||
* 2.获取url中的重要参数信息,然后通过 setToken 保存在本地;
|
||||
* 3.删除不需要显示在 url 的参数
|
||||
* 4.使用 window.location.replace 跳转正确页面
|
||||
*/
|
||||
(function () {
|
||||
// 获取 url 中的参数
|
||||
const params = getQueryMap(location.href) as DataInfo<Date>;
|
||||
const must = ["username", "roles", "accessToken"];
|
||||
const mustLength = must.length;
|
||||
if (Object.keys(params).length !== mustLength) return;
|
||||
|
||||
// url 参数满足 must 里的全部值,才判定为单点登录,避免非单点登录时刷新页面无限循环
|
||||
let sso = [];
|
||||
let start = 0;
|
||||
|
||||
while (start < mustLength) {
|
||||
if (Object.keys(params).includes(must[start]) && sso.length <= mustLength) {
|
||||
sso.push(must[start]);
|
||||
} else {
|
||||
sso = [];
|
||||
}
|
||||
start++;
|
||||
}
|
||||
|
||||
if (sso.length === mustLength) {
|
||||
// 判定为单点登录
|
||||
|
||||
// 清空本地旧信息
|
||||
removeToken();
|
||||
|
||||
// 保存新信息到本地
|
||||
setTokenFromBackend(params as any);
|
||||
|
||||
// 删除不需要显示在 url 的参数
|
||||
delete params["roles"];
|
||||
delete params["accessToken"];
|
||||
|
||||
const newUrl = `${location.origin}${location.pathname}${subBefore(
|
||||
location.hash,
|
||||
"?"
|
||||
)}?${JSON.stringify(params)
|
||||
.replace(/["{}]/g, "")
|
||||
.replace(/:/g, "=")
|
||||
.replace(/,/g, "&")}`;
|
||||
|
||||
// 替换历史记录项
|
||||
window.location.replace(newUrl);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,243 @@
|
||||
import { RouteItem } from "@/api/common/login";
|
||||
|
||||
/**
|
||||
* @description 提取菜单树中的每一项uniqueId
|
||||
* @param tree 树
|
||||
* @returns 每一项uniqueId组成的数组
|
||||
*/
|
||||
export const extractPathList = (tree: any[]): any => {
|
||||
if (!Array.isArray(tree)) {
|
||||
console.warn("tree must be an array");
|
||||
return [];
|
||||
}
|
||||
if (!tree || tree.length === 0) return [];
|
||||
const expandedPaths: Array<number | string> = [];
|
||||
for (const node of tree) {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
if (hasChildren) {
|
||||
extractPathList(node.children);
|
||||
}
|
||||
expandedPaths.push(node.uniqueId);
|
||||
}
|
||||
return expandedPaths;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 如果父级下children的length为1,删除children并自动组建唯一uniqueId
|
||||
* @param tree 树
|
||||
* @param pathList 每一项的id组成的数组
|
||||
* @returns 组件唯一uniqueId后的树
|
||||
*/
|
||||
export const deleteChildren = (tree: any[], pathList = []): any => {
|
||||
if (!Array.isArray(tree)) {
|
||||
console.warn("menuTree must be an array");
|
||||
return [];
|
||||
}
|
||||
if (!tree || tree.length === 0) return [];
|
||||
for (const [key, node] of tree.entries()) {
|
||||
if (node.children && node.children.length === 1) delete node.children;
|
||||
node.id = key;
|
||||
node.parentId = pathList.length ? pathList[pathList.length - 1] : null;
|
||||
node.pathList = [...pathList, node.id];
|
||||
node.uniqueId =
|
||||
node.pathList.length > 1 ? node.pathList.join("-") : node.pathList[0];
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
if (hasChildren) {
|
||||
deleteChildren(node.children, node.pathList);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
// 定义扩展属性类型
|
||||
export interface HierarchyNodeExtra {
|
||||
id?: string;
|
||||
parentId?: string | null;
|
||||
pathList?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 创建层级关系
|
||||
* @param tree 树
|
||||
* @param pathList 每一项的id组成的数组
|
||||
* @returns 创建层级关系后的树
|
||||
*/
|
||||
export function buildHierarchyTree<T extends RouteItem & HierarchyNodeExtra>(
|
||||
tree: T[],
|
||||
pathList: string[] = []
|
||||
): T[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
console.warn("tree must be an array");
|
||||
return [];
|
||||
}
|
||||
if (!tree || tree.length === 0) return [];
|
||||
|
||||
for (const node of tree) {
|
||||
node.id = node.meta.id;
|
||||
node.parentId = pathList.at(-1) ?? null;
|
||||
node.pathList = [...pathList, node.id];
|
||||
|
||||
if (node?.children?.length) {
|
||||
buildHierarchyTree(node.children, node.pathList);
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 广度优先遍历,根据唯一uniqueId找当前节点信息
|
||||
* @param tree 树
|
||||
* @param uniqueId 唯一uniqueId
|
||||
* @returns 当前节点信息
|
||||
*/
|
||||
export const getNodeByUniqueId = (
|
||||
tree: any[],
|
||||
uniqueId: number | string
|
||||
): any => {
|
||||
if (!Array.isArray(tree)) {
|
||||
console.warn("menuTree must be an array");
|
||||
return [];
|
||||
}
|
||||
if (!tree || tree.length === 0) return [];
|
||||
const item = tree.find(node => node.uniqueId === uniqueId);
|
||||
if (item) return item;
|
||||
const childrenList = tree
|
||||
.filter(node => node.children)
|
||||
.map(i => i.children)
|
||||
.flat(1) as unknown;
|
||||
return getNodeByUniqueId(childrenList as any[], uniqueId);
|
||||
};
|
||||
|
||||
/**
|
||||
* @description 向当前唯一uniqueId节点中追加字段
|
||||
* @param tree 树
|
||||
* @param uniqueId 唯一uniqueId
|
||||
* @param fields 需要追加的字段
|
||||
* @returns 追加字段后的树
|
||||
*/
|
||||
export const appendFieldByUniqueId = (
|
||||
tree: any[],
|
||||
uniqueId: number | string,
|
||||
fields: object
|
||||
): any => {
|
||||
if (!Array.isArray(tree)) {
|
||||
console.warn("menuTree must be an array");
|
||||
return [];
|
||||
}
|
||||
if (!tree || tree.length === 0) return [];
|
||||
for (const node of tree) {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
if (
|
||||
node.uniqueId === uniqueId &&
|
||||
Object.prototype.toString.call(fields) === "[object Object]"
|
||||
)
|
||||
Object.assign(node, fields);
|
||||
if (hasChildren) {
|
||||
appendFieldByUniqueId(node.children, uniqueId, fields);
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据返回数据的status字段值判断追加是否禁用disabled字段,返回处理后的树结构,用于上级部门级联选择器的展示
|
||||
*(实际开发中也是如此,不可能前端需要的每个字段后端都会返回,这时需要前端自行根据后端返回的某些字段做逻辑处理)
|
||||
* 这个是pure作者留下的例子, 也可以通过设置disabled 对应的字段来实现 比如disabled: 'status' (需要后端的字段为true/false)
|
||||
* @param treeList
|
||||
* @param field 根据哪个字段来设置disabled
|
||||
* @returns
|
||||
*/
|
||||
export function setDisabledForTreeOptions(treeList, field) {
|
||||
if (!treeList || !treeList.length) return;
|
||||
const newTreeList = [];
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
treeList[i].disabled = treeList[i][field] === 0 ? true : false;
|
||||
setDisabledForTreeOptions(treeList[i].children, field);
|
||||
newTreeList.push(treeList[i]);
|
||||
}
|
||||
return newTreeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 构造树型结构数据
|
||||
* @param data 数据源
|
||||
* @param id id字段 默认id
|
||||
* @param parentId 父节点字段,默认parentId
|
||||
* @param children 子节点字段,默认children
|
||||
* @returns 追加字段后的树
|
||||
*/
|
||||
export const handleTree = (
|
||||
data: any[],
|
||||
id?: string,
|
||||
parentId?: string,
|
||||
children?: string
|
||||
): any => {
|
||||
if (!Array.isArray(data)) {
|
||||
console.warn("data must be an array");
|
||||
return [];
|
||||
}
|
||||
const config = {
|
||||
id: id || "id",
|
||||
parentId: parentId || "parentId",
|
||||
childrenList: children || "children"
|
||||
};
|
||||
|
||||
const childrenListMap: any = {};
|
||||
const nodeIds: any = {};
|
||||
const tree = [];
|
||||
|
||||
for (const d of data) {
|
||||
const parentId = d[config.parentId];
|
||||
if (childrenListMap[parentId] == null) {
|
||||
childrenListMap[parentId] = [];
|
||||
}
|
||||
nodeIds[d[config.id]] = d;
|
||||
childrenListMap[parentId].push(d);
|
||||
}
|
||||
|
||||
for (const d of data) {
|
||||
const parentId = d[config.parentId];
|
||||
if (nodeIds[parentId] == null) {
|
||||
tree.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of tree) {
|
||||
adaptToChildrenList(t);
|
||||
}
|
||||
|
||||
function adaptToChildrenList(o: Record<string, any>) {
|
||||
if (childrenListMap[o[config.id]] !== null) {
|
||||
o[config.childrenList] = childrenListMap[o[config.id]];
|
||||
}
|
||||
if (o[config.childrenList]) {
|
||||
for (const c of o[config.childrenList]) {
|
||||
adaptToChildrenList(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
export interface Tree {
|
||||
children?: this[];
|
||||
}
|
||||
|
||||
export function toTree<T extends Tree>(
|
||||
src: T[],
|
||||
keyField: keyof T,
|
||||
parentField: keyof T
|
||||
): T[] {
|
||||
const map = new Map<unknown, T>(src.map(it => [it[keyField], it]));
|
||||
src.forEach(it => {
|
||||
if (map.has(it[parentField])) {
|
||||
const parent = map.get(it[parentField])!;
|
||||
if (!parent.children) {
|
||||
parent.children = [];
|
||||
}
|
||||
parent.children.push(it);
|
||||
}
|
||||
});
|
||||
return src.filter(it => !it[parentField]);
|
||||
}
|
||||
Reference in New Issue
Block a user