feat: initial commit

This commit is contained in:
gin
2026-05-07 18:39:00 +08:00
commit cdee21ee8e
653 changed files with 63946 additions and 0 deletions
+90
View File
@@ -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";
}
/** 格式化tokenjwt格式) */
export const formatToken = (token: string): string => {
return "Bearer " + token;
};
+141
View File
@@ -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() {}
}
+53
View File
@@ -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 {};
+311
View File
@@ -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
View File
@@ -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>;
}
+85
View File
@@ -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 };
+13
View File
@@ -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>();
+214
View File
@@ -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;
+17
View File
@@ -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;
+39
View File
@@ -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
});
}
}
+37
View File
@@ -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 });
};
+22
View File
@@ -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;
}
+59
View File
@@ -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;
}
})();
+243
View File
@@ -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]);
}