chore(config): 添加项目配置文件和隐私协议
- 添加 .editorconfig 文件统一代码风格 - 添加 .env.development 和 .env.example 环境配置文件 - 添加 .eslintignore 和 .eslintrc.js 代码检查配置 - 添加 .gitignore 版本控制忽略文件配置 - 添加 .prettierignore 格式化忽略配置 - 添加隐私协议HTML文件 - 添加API密钥管理组件基础结构
This commit is contained in:
437
src/utils/cache-manager.ts
Normal file
437
src/utils/cache-manager.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 缓存管理工具
|
||||
*/
|
||||
|
||||
// 缓存项接口
|
||||
interface CacheItem<T = any> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
expiry: number;
|
||||
version?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
// 缓存配置
|
||||
interface CacheConfig {
|
||||
maxSize?: number;
|
||||
defaultExpiry?: number;
|
||||
version?: string;
|
||||
enableCompression?: boolean;
|
||||
}
|
||||
|
||||
// 内存缓存管理器
|
||||
export class MemoryCache {
|
||||
private cache = new Map<string, CacheItem>();
|
||||
private config: Required<CacheConfig>;
|
||||
private accessOrder = new Map<string, number>();
|
||||
private accessCounter = 0;
|
||||
|
||||
constructor(config: CacheConfig = {}) {
|
||||
this.config = {
|
||||
maxSize: config.maxSize || 100,
|
||||
defaultExpiry: config.defaultExpiry || 5 * 60 * 1000, // 5分钟
|
||||
version: config.version || '1.0.0',
|
||||
enableCompression: config.enableCompression || false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
set<T>(key: string, data: T, expiry?: number, tags?: string[]): void {
|
||||
const item: CacheItem<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expiry: expiry || this.config.defaultExpiry,
|
||||
version: this.config.version,
|
||||
tags
|
||||
};
|
||||
|
||||
// 检查缓存大小限制
|
||||
if (this.cache.size >= this.config.maxSize && !this.cache.has(key)) {
|
||||
this.evictLRU();
|
||||
}
|
||||
|
||||
this.cache.set(key, item);
|
||||
this.updateAccessOrder(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (this.isExpired(item)) {
|
||||
this.cache.delete(key);
|
||||
this.accessOrder.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查版本
|
||||
if (item.version !== this.config.version) {
|
||||
this.cache.delete(key);
|
||||
this.accessOrder.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.updateAccessOrder(key);
|
||||
return item.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
this.accessOrder.delete(key);
|
||||
return this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
this.accessOrder.clear();
|
||||
this.accessCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签清除缓存
|
||||
*/
|
||||
clearByTags(tags: string[]): void {
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (item.tags && item.tags.some((tag) => tags.includes(tag))) {
|
||||
this.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在且有效
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
return this.get(key) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存大小
|
||||
*/
|
||||
size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
getStats() {
|
||||
let totalSize = 0;
|
||||
let expiredCount = 0;
|
||||
|
||||
for (const item of this.cache.values()) {
|
||||
totalSize += JSON.stringify(item.data).length;
|
||||
if (this.isExpired(item)) {
|
||||
expiredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalItems: this.cache.size,
|
||||
totalSize,
|
||||
expiredCount,
|
||||
maxSize: this.config.maxSize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
cleanup(): number {
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (this.isExpired(item)) {
|
||||
this.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
private isExpired(item: CacheItem): boolean {
|
||||
return Date.now() - item.timestamp > item.expiry;
|
||||
}
|
||||
|
||||
private updateAccessOrder(key: string): void {
|
||||
this.accessOrder.set(key, ++this.accessCounter);
|
||||
}
|
||||
|
||||
private evictLRU(): void {
|
||||
let lruKey = '';
|
||||
let lruAccess = Infinity;
|
||||
|
||||
for (const [key, access] of this.accessOrder.entries()) {
|
||||
if (access < lruAccess) {
|
||||
lruAccess = access;
|
||||
lruKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (lruKey) {
|
||||
this.delete(lruKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化缓存管理器
|
||||
export class PersistentCache {
|
||||
private prefix: string;
|
||||
private config: Required<CacheConfig>;
|
||||
|
||||
constructor(prefix = 'app_cache', config: CacheConfig = {}) {
|
||||
this.prefix = prefix;
|
||||
this.config = {
|
||||
maxSize: config.maxSize || 50,
|
||||
defaultExpiry: config.defaultExpiry || 24 * 60 * 60 * 1000, // 24小时
|
||||
version: config.version || '1.0.0',
|
||||
enableCompression: config.enableCompression || true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置持久化缓存
|
||||
*/
|
||||
set<T>(key: string, data: T, expiry?: number, tags?: string[]): void {
|
||||
try {
|
||||
const item: CacheItem<T> = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expiry: expiry || this.config.defaultExpiry,
|
||||
version: this.config.version,
|
||||
tags
|
||||
};
|
||||
|
||||
const serialized = JSON.stringify(item);
|
||||
localStorage.setItem(this.getKey(key), serialized);
|
||||
|
||||
// 更新索引
|
||||
this.updateIndex(key);
|
||||
} catch (error) {
|
||||
console.warn('Failed to set persistent cache:', error);
|
||||
// 如果存储失败,尝试清理一些空间
|
||||
this.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取持久化缓存
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
try {
|
||||
const serialized = localStorage.getItem(this.getKey(key));
|
||||
|
||||
if (!serialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item: CacheItem<T> = JSON.parse(serialized);
|
||||
|
||||
// 检查是否过期
|
||||
if (this.isExpired(item)) {
|
||||
this.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查版本
|
||||
if (item.version !== this.config.version) {
|
||||
this.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.data;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get persistent cache:', error);
|
||||
this.delete(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除持久化缓存
|
||||
*/
|
||||
delete(key: string): void {
|
||||
localStorage.removeItem(this.getKey(key));
|
||||
this.removeFromIndex(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
clear(): void {
|
||||
const keys = this.getAllKeys();
|
||||
keys.forEach((key) => localStorage.removeItem(key));
|
||||
localStorage.removeItem(this.getIndexKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签清除缓存
|
||||
*/
|
||||
clearByTags(tags: string[]): void {
|
||||
const keys = this.getAllKeys();
|
||||
|
||||
keys.forEach((fullKey) => {
|
||||
try {
|
||||
const serialized = localStorage.getItem(fullKey);
|
||||
if (serialized) {
|
||||
const item: CacheItem = JSON.parse(serialized);
|
||||
if (item.tags && item.tags.some((tag) => tags.includes(tag))) {
|
||||
const key = fullKey.replace(this.prefix + '_', '');
|
||||
this.delete(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略解析错误,直接删除
|
||||
localStorage.removeItem(fullKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期缓存
|
||||
*/
|
||||
cleanup(): number {
|
||||
const keys = this.getAllKeys();
|
||||
let cleanedCount = 0;
|
||||
|
||||
keys.forEach((fullKey) => {
|
||||
try {
|
||||
const serialized = localStorage.getItem(fullKey);
|
||||
if (serialized) {
|
||||
const item: CacheItem = JSON.parse(serialized);
|
||||
if (this.isExpired(item)) {
|
||||
const key = fullKey.replace(this.prefix + '_', '');
|
||||
this.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果解析失败,也删除这个项
|
||||
localStorage.removeItem(fullKey);
|
||||
cleanedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
private getKey(key: string): string {
|
||||
return `${this.prefix}_${key}`;
|
||||
}
|
||||
|
||||
private getIndexKey(): string {
|
||||
return `${this.prefix}_index`;
|
||||
}
|
||||
|
||||
private getAllKeys(): string[] {
|
||||
const keys: string[] = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (
|
||||
key &&
|
||||
key.startsWith(this.prefix + '_') &&
|
||||
key !== this.getIndexKey()
|
||||
) {
|
||||
keys.push(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private updateIndex(key: string): void {
|
||||
try {
|
||||
const indexKey = this.getIndexKey();
|
||||
const index = JSON.parse(localStorage.getItem(indexKey) || '[]');
|
||||
|
||||
if (!index.includes(key)) {
|
||||
index.push(key);
|
||||
|
||||
// 限制索引大小
|
||||
if (index.length > this.config.maxSize) {
|
||||
const removedKey = index.shift();
|
||||
this.delete(removedKey);
|
||||
}
|
||||
|
||||
localStorage.setItem(indexKey, JSON.stringify(index));
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to update cache index:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private removeFromIndex(key: string): void {
|
||||
try {
|
||||
const indexKey = this.getIndexKey();
|
||||
const index = JSON.parse(localStorage.getItem(indexKey) || '[]');
|
||||
const newIndex = index.filter((k: string) => k !== key);
|
||||
localStorage.setItem(indexKey, JSON.stringify(newIndex));
|
||||
} catch (error) {
|
||||
console.warn('Failed to remove from cache index:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private isExpired(item: CacheItem): boolean {
|
||||
return Date.now() - item.timestamp > item.expiry;
|
||||
}
|
||||
}
|
||||
|
||||
// 全局缓存实例
|
||||
export const memoryCache = new MemoryCache({
|
||||
maxSize: 200,
|
||||
defaultExpiry: 5 * 60 * 1000 // 5分钟
|
||||
});
|
||||
|
||||
export const persistentCache = new PersistentCache('app_cache', {
|
||||
maxSize: 100,
|
||||
defaultExpiry: 24 * 60 * 60 * 1000 // 24小时
|
||||
});
|
||||
|
||||
// 缓存装饰器
|
||||
export function cached(
|
||||
expiry: number = 5 * 60 * 1000,
|
||||
keyGenerator?: (...args: any[]) => string
|
||||
) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const key = keyGenerator
|
||||
? keyGenerator(...args)
|
||||
: `${target.constructor.name}_${propertyKey}_${JSON.stringify(args)}`;
|
||||
|
||||
// 尝试从缓存获取
|
||||
let result = memoryCache.get(key);
|
||||
|
||||
if (result === null) {
|
||||
// 缓存未命中,执行原方法
|
||||
result = await originalMethod.apply(this, args);
|
||||
|
||||
// 存入缓存
|
||||
memoryCache.set(key, result, expiry);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
598
src/utils/common.ts
Normal file
598
src/utils/common.ts
Normal file
@@ -0,0 +1,598 @@
|
||||
import { message, SelectProps } from 'ant-design-vue';
|
||||
import { isExternalLink, random, toDateString } from 'ele-admin-pro';
|
||||
import router from '@/router';
|
||||
import { listDictionaryData } from '@/api/system/dictionary-data';
|
||||
import { ref, unref } from 'vue';
|
||||
import { APP_SECRET, FILE_SERVER } from '@/config/setting';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import CryptoJS from 'crypto-js';
|
||||
// import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getSiteDomain, getTenantId } from '@/utils/domain';
|
||||
import { uuid } from 'ele-admin-pro';
|
||||
import mitt from 'mitt';
|
||||
import { ChatMessage } from '@/api/system/chat/model';
|
||||
type Events = {
|
||||
message: ChatMessage;
|
||||
};
|
||||
export const emitter = mitt<Events>();
|
||||
/**
|
||||
* 常用函数封装
|
||||
*/
|
||||
|
||||
// 生成编号
|
||||
export function createCode(): string {
|
||||
const data = new Date();
|
||||
const code = `${data.getFullYear()}${data.getMonth()}${data.getDate()}${data.getHours()}${data.getMilliseconds()}`;
|
||||
return code.slice(0);
|
||||
}
|
||||
|
||||
// 生成商户编号
|
||||
export function createMerchantCode(): string {
|
||||
const data = new Date();
|
||||
const code = `${data.getFullYear()}${data.getMonth()}${data.getDate()}${data.getSeconds()}`;
|
||||
return code.slice(3);
|
||||
}
|
||||
|
||||
// 生成订单编号
|
||||
export function createOrderNo(): string {
|
||||
const data = new Date();
|
||||
const code = `${data.getFullYear()}${data.getMonth()}${data.getDate()}${data.getHours()}${data.getMilliseconds()}${random(
|
||||
8000,
|
||||
12000
|
||||
)}`;
|
||||
return code.slice(0);
|
||||
}
|
||||
|
||||
// 跳转页面函数
|
||||
export function openUrl(url: string, params?: any): void {
|
||||
const isExternal = isExternalLink(url);
|
||||
if (isExternal) {
|
||||
window.open(url);
|
||||
} else {
|
||||
if (params) {
|
||||
router.push({ path: url, query: params });
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转页面函数
|
||||
* 携带用于统计用户行为的参数
|
||||
* @param path /product/detail.html
|
||||
* @param id 128
|
||||
* @param d 项目数据
|
||||
* 拼接规则: {域名}{path}?spm={模型}.{租户ID}.{商户ID}.{父栏目ID}.{栏目ID}.{详情页ID}.{用户ID}.{timestamp}&token={token}
|
||||
* @return https:///websoft.top/product/detail/128.html?spm=c.5.3057.10005.undefined&token=DDkr1PpE9DouIVMjLEMt9733QsgG7oNV
|
||||
*/
|
||||
export function openSpmUrl(path?: string, d?: any, id = 0): void {
|
||||
// const domain = getSiteDomain();
|
||||
const domain = localStorage.getItem('Domain');
|
||||
let spm = '';
|
||||
const tid = d?.tenantId || 0;
|
||||
const mid = localStorage.getItem('MerchantId') || 0;
|
||||
const pid = d?.parentId || 0;
|
||||
let cid = d?.categoryId || 0;
|
||||
const uid = localStorage.getItem('UserId') || 0;
|
||||
const timestamp = ref(Date.now() / 1000);
|
||||
|
||||
if (d?.navigationId > 0) {
|
||||
cid = d.navigationId;
|
||||
}
|
||||
if (d?.itemId > 0) {
|
||||
id = d.itemId;
|
||||
}
|
||||
|
||||
// TODO 封装租户ID和店铺ID
|
||||
spm = `?spm=${d?.model}.${tid}.${mid}.${pid}.${cid}.${id}.${uid}.${
|
||||
timestamp.value
|
||||
}&token=${uuid()}`;
|
||||
|
||||
// TODO 含http直接跳转
|
||||
if (path?.startsWith('http')) {
|
||||
window.open(`${path}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO 开发环境
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('开发环境', getTenantId());
|
||||
window.open(`http://localhost:${getTenantId()}${path}${spm}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO 跳转网站预览地址
|
||||
if (domain && domain.length > 0) {
|
||||
window.open(`https://${domain}${path}${spm}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SpmUrl
|
||||
* 拼接规则: {域名}{path}?spm={模型}.{租户ID}.{商户ID}.{父栏目ID}.{栏目ID}.{详情页ID}.{用户ID}.{timestamp}&token={token}
|
||||
* @param d
|
||||
*/
|
||||
export function getSpmUrl(d?: any): string {
|
||||
let domain = localStorage.getItem('Domain');
|
||||
let path = d?.model;
|
||||
const tid = d?.tenantId || 0;
|
||||
const mid = d?.merchantId || 0;
|
||||
const pid = d?.parentId || 0;
|
||||
let cid = d?.navigationId;
|
||||
let id = d?.itemId;
|
||||
const uid = localStorage.getItem('UserId') || 0;
|
||||
const timestamp = ref(Date.now() / 1000);
|
||||
|
||||
// TODO 配置cid
|
||||
if (!cid) {
|
||||
cid = d?.categoryId;
|
||||
}
|
||||
if (!id) {
|
||||
id = d?.articleId || 0;
|
||||
}
|
||||
path = d?.model + '/' + d?.navigationId;
|
||||
// TODO 首页
|
||||
if (d?.model == 'index') {
|
||||
path = '';
|
||||
}
|
||||
if (!domain?.startsWith('https:')) {
|
||||
domain = `https://${domain}`;
|
||||
}
|
||||
// 开发环境
|
||||
if (import.meta.env.DEV) {
|
||||
domain = `http://localhost:${getTenantId()}`;
|
||||
}
|
||||
|
||||
// TODO 顶级栏目则默认跳转到第一个子栏目
|
||||
if (d?.parentId == 0 && d?.children && d?.children.length > 0) {
|
||||
cid = d?.children?.[0]?.navigationId;
|
||||
}
|
||||
|
||||
// 文章后缀
|
||||
if (d?.suffix) {
|
||||
path = path + d?.suffix;
|
||||
}
|
||||
|
||||
// TODO 封装spm
|
||||
return `${domain}/${path}?spm=${d?.model}.${tid}.${mid}.${pid}.${cid}.${id}.${uid}.${timestamp.value}`;
|
||||
}
|
||||
|
||||
// export function getSpmUrl(path: string, d?: any, id = 0): string {
|
||||
// let domain = localStorage.getItem('Domain');
|
||||
// let spm = '';
|
||||
// let tid = localStorage.getItem('TenantId') || 0;
|
||||
// let mid = localStorage.getItem('MerchantId') || 0;
|
||||
// let pid = d?.parentId || 0;
|
||||
// let cid = d?.navigationId || d?.categoryId;
|
||||
// let uid = localStorage.getItem('UserId') || 0;
|
||||
// let timestamp = ref(Date.now() / 1000);
|
||||
//
|
||||
// // 跳转网站预览地址
|
||||
// if(d?.model == 'links'){
|
||||
// return d?.path;
|
||||
// }
|
||||
// // 详情ID
|
||||
// if(d?.itemId > 0){
|
||||
// id = d?.itemId;
|
||||
// }
|
||||
// // 顶级栏目则默认跳转到第一个子栏目
|
||||
// if(d?.children && d?.children.length > 0){
|
||||
// id = d?.children[0]?.navigationId;
|
||||
// }
|
||||
// // TODO 封装spm
|
||||
// spm = `?spm=${d?.model}.${tid}.${mid}.${pid}.${cid}.${id}.${uid}.${timestamp.value}`;
|
||||
//
|
||||
// // 开发环境
|
||||
// if (import.meta.env.DEV) {
|
||||
// return `http://localhost:10317${path}${spm}`;
|
||||
// }
|
||||
// return `https://${domain}${path}${spm}`
|
||||
// }
|
||||
|
||||
/**
|
||||
* 弹出新窗口
|
||||
* @param url
|
||||
* @constructor
|
||||
*/
|
||||
export function openNew(url: string) {
|
||||
if (url.slice(0, 4) == 'http') {
|
||||
return window.open(url);
|
||||
}
|
||||
window.open(`http://${url}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览地址
|
||||
* @param url
|
||||
* @constructor
|
||||
*/
|
||||
export function openPreview(url: string) {
|
||||
if (url.slice(0, 4) == 'http') {
|
||||
return window.open(url);
|
||||
}
|
||||
return window.open(`${getSiteDomain()}${url}`);
|
||||
}
|
||||
/**
|
||||
* 获取网站域名
|
||||
* @param path
|
||||
*/
|
||||
export const getDomainPath = (path: string) => {
|
||||
const domain = localStorage.getItem('Domain');
|
||||
return domain + path;
|
||||
};
|
||||
|
||||
export const getLang = () => {
|
||||
if (localStorage.getItem('i18n-lang')) {
|
||||
return `${localStorage.getItem('i18n-lang')}`;
|
||||
}
|
||||
// const { locale } = useI18n();
|
||||
// return `${locale.value}`;
|
||||
};
|
||||
// 预览云存储文件
|
||||
export function getUrl(url: string) {
|
||||
const isExternal = isExternalLink(url);
|
||||
// const uploadMethod = localStorage.getItem('UploadMethod');
|
||||
// const bucketDomain = localStorage.getItem('BucketDomain');
|
||||
// if (uploadMethod == 'oss') {
|
||||
// return bucketDomain + url;
|
||||
// }
|
||||
if (!isExternal) {
|
||||
return FILE_SERVER + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
// 跳转页面(不弹窗)
|
||||
export function navTo(d?: any, path?: string, spm?: boolean): string {
|
||||
let domain = localStorage.getItem('Domain');
|
||||
|
||||
if (!domain?.startsWith('https:')) {
|
||||
domain = `https://${domain}`;
|
||||
}
|
||||
// 开发环境
|
||||
if (import.meta.env.DEV) {
|
||||
domain = `http://localhost:${getTenantId()}`;
|
||||
}
|
||||
if (d?.model == 'index') {
|
||||
return domain + '/';
|
||||
}
|
||||
if (!path) {
|
||||
path = d?.path;
|
||||
}
|
||||
// 国际化配置
|
||||
if (getLang()) {
|
||||
if (getLang() == 'en') {
|
||||
path = '/en' + path;
|
||||
}
|
||||
}
|
||||
// 是否移动端
|
||||
if (d?.isMobile) {
|
||||
path = '/m' + path;
|
||||
}
|
||||
path = domain + path;
|
||||
// 是否附加spm参数
|
||||
if (spm) {
|
||||
const uid = localStorage.getItem('UserId') || 0;
|
||||
const timestamp = ref(Date.now() / 1000);
|
||||
return `${path}?spm=${d?.tenantId || 0}.${d?.merchantId || 0}.${
|
||||
d?.parentId || 0
|
||||
}.${d?.navigationId || d?.categoryId}.${uid}.${timestamp.value}`;
|
||||
}
|
||||
return `${path}`;
|
||||
}
|
||||
|
||||
export function detail(d: any) {
|
||||
return navTo(d, `/${d.detail}/${d.articleId}.html`);
|
||||
}
|
||||
|
||||
export function push(path: string) {
|
||||
router.push(path);
|
||||
}
|
||||
|
||||
// 手机号脱敏
|
||||
export function getMobile(tel: string) {
|
||||
const reg = /^(\d{3})\d{4}(\d{4})$/;
|
||||
return tel.replace(reg, '$1****$2');
|
||||
}
|
||||
|
||||
// 复制文本
|
||||
export const copyText = (text) => {
|
||||
// 模拟 输入框
|
||||
const cInput = document.createElement('input');
|
||||
cInput.value = text;
|
||||
document.body.appendChild(cInput);
|
||||
cInput.select(); // 选取文本框内容
|
||||
|
||||
// 执行浏览器复制命令
|
||||
// 复制命令会将当前选中的内容复制到剪切板中(这里就是创建的input标签)
|
||||
// Input要在正常的编辑状态下原生复制方法才会生效
|
||||
message.success(`复制成功`);
|
||||
document.execCommand('copy');
|
||||
|
||||
// 复制成功后再将构造的标签 移除
|
||||
document.body.removeChild(cInput);
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算剩余时间
|
||||
* @param endTime
|
||||
*/
|
||||
export const getEndTime = (endTime) => {
|
||||
const setTime = new Date(endTime);
|
||||
const nowTime = new Date();
|
||||
const restSec = setTime.getTime() - nowTime.getTime();
|
||||
// 剩余天数
|
||||
const lastDay = parseInt(String(restSec / (60 * 60 * 24 * 1000) + 1));
|
||||
// let lastHour = parseInt(String((restSec / (60 * 60 * 1000)) % 24));
|
||||
// let lastMinu = parseInt(String((restSec / (60 * 1000)) % 60));
|
||||
// let lastSec = parseInt(String((restSec / 1000) % 60));
|
||||
|
||||
// 过期状态
|
||||
if (lastDay < 30 && lastDay > 0) {
|
||||
return `<div class="ele-text-warning">${toDateString(
|
||||
endTime,
|
||||
'yyyy-MM-dd'
|
||||
)}(${lastDay}天后过期)</div>`;
|
||||
}
|
||||
if (lastDay < 0) {
|
||||
return `<div class="ele-text-danger">${toDateString(
|
||||
endTime,
|
||||
'yyyy-MM-dd'
|
||||
)}(已过期)</div>`;
|
||||
}
|
||||
return `<div class="ele-text-info">${toDateString(
|
||||
endTime,
|
||||
'yyyy-MM-dd'
|
||||
)}</div>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断是否是移动设备
|
||||
*/
|
||||
export function isMobileDevice(): boolean {
|
||||
return (
|
||||
typeof window.orientation !== 'undefined' ||
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据作为下拉选项数据
|
||||
* @param dictCode
|
||||
*/
|
||||
export const getDictionaryOptions = (dictCode) => {
|
||||
const dictOptions = ref<SelectProps['options']>([]);
|
||||
// const key = dictCode + ':' + localStorage.getItem('TenantId');
|
||||
// const storageData = localStorage.getItem(key);
|
||||
|
||||
listDictionaryData({
|
||||
dictCode
|
||||
})
|
||||
.then((list) => {
|
||||
// 获取远程字典数据
|
||||
if (list.length > 0) {
|
||||
dictOptions.value = list.map((d) => {
|
||||
return {
|
||||
key: d.dictDataCode,
|
||||
value: d.dictDataName,
|
||||
label: d.dictDataName,
|
||||
comments: d.comments
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 未定义则取默认的json数据
|
||||
// dictOptions.value = getJson(dictCode);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(e.message);
|
||||
});
|
||||
|
||||
// if (!storageData) {
|
||||
// listDictionaryData({
|
||||
// dictCode
|
||||
// })
|
||||
// .then((list) => {
|
||||
// // 获取远程字典数据
|
||||
// if (list.length > 0) {
|
||||
// dictOptions.value = list.map((d) => {
|
||||
// return {
|
||||
// value: d.dictDataCode,
|
||||
// label: d.dictDataName,
|
||||
// text: d.dictDataName,
|
||||
// comments: d.comments
|
||||
// };
|
||||
// });
|
||||
// // 写入缓存
|
||||
// localStorage.setItem(key, JSON.stringify(dictOptions.value));
|
||||
// } else {
|
||||
// // 未定义则取默认的json数据
|
||||
// dictOptions.value = getJson(dictCode);
|
||||
// }
|
||||
// })
|
||||
// .catch((e) => {
|
||||
// message.error(e.message);
|
||||
// });
|
||||
// } else {
|
||||
// dictOptions.value = JSON.parse(storageData);
|
||||
// }
|
||||
return <any>dictOptions;
|
||||
};
|
||||
|
||||
// 判断是否为图片
|
||||
/*
|
||||
* @param: fileName - 文件名称
|
||||
*/
|
||||
export const isImage = (fileName) => {
|
||||
const split = fileName.split('?');
|
||||
// 后缀获取
|
||||
let suffix = '';
|
||||
try {
|
||||
const flieArr = split[0].split('.');
|
||||
suffix = flieArr[flieArr.length - 1];
|
||||
} catch (err) {
|
||||
suffix = '';
|
||||
}
|
||||
const imgList = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp'];
|
||||
return imgList.some((item) => {
|
||||
return item == suffix;
|
||||
});
|
||||
};
|
||||
|
||||
export const getWeek = (text) => {
|
||||
const week = [
|
||||
'星期日',
|
||||
'星期一',
|
||||
'星期二',
|
||||
'星期三',
|
||||
'星期四',
|
||||
'星期五',
|
||||
'星期六'
|
||||
];
|
||||
return week[text];
|
||||
};
|
||||
|
||||
/**
|
||||
* 文件大小转换
|
||||
* @param text
|
||||
*/
|
||||
export const getFileSize = (text) => {
|
||||
if (text < 1024) {
|
||||
return text + 'B';
|
||||
} else if (text < 1024 * 1024) {
|
||||
return (text / 1024).toFixed(1) + 'KB';
|
||||
} else if (text < 1024 * 1024 * 1024) {
|
||||
return (text / 1024 / 1024).toFixed(1) + 'M';
|
||||
} else {
|
||||
return (text / 1024 / 1024 / 1024).toFixed(1) + 'G';
|
||||
}
|
||||
};
|
||||
|
||||
/* 原图转缩列图 */
|
||||
export const thumbnail = (url) => {
|
||||
if (url.indexOf('/thumbnail') < 0) {
|
||||
return url.replace(FILE_SERVER, FILE_SERVER + '/thumbnail');
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
/* 缩列转图原图 */
|
||||
export const original = (url) => {
|
||||
if (url.indexOf('/thumbnail') == 0) {
|
||||
return url.replace('/thumbnail', '');
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getCompanyInfo = () => {
|
||||
const user = useUserStore();
|
||||
if (user.info?.companyInfo) {
|
||||
return user.info?.companyInfo;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getVersion = () => {
|
||||
const companyInfo = getCompanyInfo();
|
||||
if (companyInfo?.version) {
|
||||
return companyInfo?.version;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// AES加密
|
||||
export const encrypt = (text) => {
|
||||
return CryptoJS.AES.encrypt(text, APP_SECRET).toString();
|
||||
};
|
||||
|
||||
// AES解密
|
||||
export const decrypt = (encrypt) => {
|
||||
CryptoJS.AES.decrypt(encrypt, APP_SECRET);
|
||||
const bytes = CryptoJS.AES.decrypt(encrypt, APP_SECRET);
|
||||
return bytes.toString(CryptoJS.enc.Utf8);
|
||||
};
|
||||
|
||||
// 获取商户ID
|
||||
export const getMerchantId = () => {
|
||||
const merchantId = localStorage.getItem('MerchantId');
|
||||
if (merchantId) {
|
||||
return Number(merchantId);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 获取当前登录用户ID
|
||||
export const getUserId = () => {
|
||||
let userId = 0;
|
||||
const uid = Number(localStorage.getItem('UserId'));
|
||||
if (uid) {
|
||||
userId = uid;
|
||||
return userId;
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
|
||||
// 获取页签数据
|
||||
export const getPageTitle = () => {
|
||||
const { currentRoute } = useRouter();
|
||||
const { meta } = unref(currentRoute);
|
||||
const { title } = meta;
|
||||
return title;
|
||||
};
|
||||
|
||||
/**
|
||||
* 提取传参中的ID
|
||||
* param 12334.html
|
||||
* return 1234
|
||||
* @param index
|
||||
*/
|
||||
export const getIdBySpm = (index: number) => {
|
||||
console.log('split', router.currentRoute.value.query.spm);
|
||||
const split = String(router.currentRoute.value.query.spm).split('.');
|
||||
console.log(split);
|
||||
return split[index];
|
||||
};
|
||||
|
||||
/**
|
||||
* 提取传参中的token
|
||||
*/
|
||||
export const getTokenBySpm = () => {
|
||||
const token = router.currentRoute.value.query.token;
|
||||
if (token) {
|
||||
return `${token}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下划线转驼峰命名
|
||||
*/
|
||||
export function toCamelCase(str: string): string {
|
||||
return str.replace(/_([a-z])/g, function (_, letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转大驼峰命名
|
||||
*/
|
||||
export function toCamelCaseUpper(str: string): string {
|
||||
return toCamelCase(str).replace(/^[a-z]/, function (letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转为短下划线
|
||||
*/
|
||||
export function toShortUnderline(str: string): string {
|
||||
return str
|
||||
.replace(/[A-Z]/g, function (letter) {
|
||||
return '_' + letter.toLowerCase();
|
||||
})
|
||||
.replace(/^_/, '');
|
||||
}
|
||||
437
src/utils/component-optimization.ts
Normal file
437
src/utils/component-optimization.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* 组件性能优化工具
|
||||
*/
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import type { Ref, ComputedRef, WatchStopHandle } from 'vue';
|
||||
|
||||
// 防抖函数
|
||||
export function useDebounce<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
delay = 300
|
||||
): [T, () => void] {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
const debouncedFn = ((...args: any[]) => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
fn(...args);
|
||||
timeoutId = null;
|
||||
}, delay);
|
||||
}) as T;
|
||||
|
||||
const cancel = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return [debouncedFn, cancel];
|
||||
}
|
||||
|
||||
// 节流函数
|
||||
export function useThrottle<T extends (...args: any[]) => any>(
|
||||
fn: T,
|
||||
delay = 300
|
||||
): [T, () => void] {
|
||||
let lastExecTime = 0;
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
const throttledFn = ((...args: any[]) => {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastExecTime >= delay) {
|
||||
fn(...args);
|
||||
lastExecTime = now;
|
||||
} else {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = window.setTimeout(() => {
|
||||
fn(...args);
|
||||
lastExecTime = Date.now();
|
||||
timeoutId = null;
|
||||
}, delay - (now - lastExecTime));
|
||||
}
|
||||
}) as T;
|
||||
|
||||
const cancel = () => {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
return [throttledFn, cancel];
|
||||
}
|
||||
|
||||
// 虚拟滚动
|
||||
export function useVirtualScroll<T>(
|
||||
items: Ref<T[]>,
|
||||
itemHeight: number,
|
||||
containerHeight: number,
|
||||
buffer = 5
|
||||
) {
|
||||
const scrollTop = ref(0);
|
||||
const containerRef = ref<HTMLElement>();
|
||||
|
||||
const visibleRange = computed(() => {
|
||||
const start = Math.floor(scrollTop.value / itemHeight);
|
||||
const end = Math.min(
|
||||
start + Math.ceil(containerHeight / itemHeight) + buffer,
|
||||
items.value.length
|
||||
);
|
||||
|
||||
return {
|
||||
start: Math.max(0, start - buffer),
|
||||
end
|
||||
};
|
||||
});
|
||||
|
||||
const visibleItems = computed(() => {
|
||||
const { start, end } = visibleRange.value;
|
||||
return items.value.slice(start, end).map((item, index) => ({
|
||||
item,
|
||||
index: start + index
|
||||
}));
|
||||
});
|
||||
|
||||
const totalHeight = computed(() => items.value.length * itemHeight);
|
||||
|
||||
const offsetY = computed(() => visibleRange.value.start * itemHeight);
|
||||
|
||||
const handleScroll = useThrottle((event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
scrollTop.value = target.scrollTop;
|
||||
}, 16)[0]; // 60fps
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
visibleItems,
|
||||
totalHeight,
|
||||
offsetY
|
||||
};
|
||||
}
|
||||
|
||||
// 图片懒加载
|
||||
export function useLazyImage() {
|
||||
const imageRef = ref<HTMLImageElement>();
|
||||
const isLoaded = ref(false);
|
||||
const isError = ref(false);
|
||||
const isIntersecting = ref(false);
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
const load = (src: string) => {
|
||||
if (!imageRef.value || isLoaded.value) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (imageRef.value) {
|
||||
imageRef.value.src = src;
|
||||
isLoaded.value = true;
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
isError.value = true;
|
||||
};
|
||||
img.src = src;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (imageRef.value) {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
isIntersecting.value = entry.isIntersecting;
|
||||
});
|
||||
},
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
observer.observe(imageRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
imageRef,
|
||||
isLoaded,
|
||||
isError,
|
||||
isIntersecting,
|
||||
load
|
||||
};
|
||||
}
|
||||
|
||||
// 无限滚动
|
||||
export function useInfiniteScroll<T>(
|
||||
loadMore: () => Promise<T[]>,
|
||||
options: {
|
||||
threshold?: number;
|
||||
initialLoad?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
const { threshold = 100, initialLoad = true } = options;
|
||||
|
||||
const items = ref<T[]>([]);
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
const containerRef = ref<HTMLElement>();
|
||||
|
||||
const load = async () => {
|
||||
if (loading.value || finished.value) return;
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const newItems = await loadMore();
|
||||
|
||||
if (newItems.length === 0) {
|
||||
finished.value = true;
|
||||
} else {
|
||||
items.value.push(...newItems);
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err as Error;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkScroll = useThrottle(() => {
|
||||
if (!containerRef.value || loading.value || finished.value) return;
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = containerRef.value;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - threshold) {
|
||||
load();
|
||||
}
|
||||
}, 100)[0];
|
||||
|
||||
onMounted(() => {
|
||||
if (initialLoad) {
|
||||
load();
|
||||
}
|
||||
|
||||
if (containerRef.value) {
|
||||
containerRef.value.addEventListener('scroll', checkScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('scroll', checkScroll);
|
||||
}
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
items.value = [];
|
||||
loading.value = false;
|
||||
finished.value = false;
|
||||
error.value = null;
|
||||
};
|
||||
|
||||
return {
|
||||
items,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
containerRef,
|
||||
load,
|
||||
reset
|
||||
};
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
export function useBreakpoints() {
|
||||
const width = ref(window.innerWidth);
|
||||
const height = ref(window.innerHeight);
|
||||
|
||||
const updateSize = useThrottle(() => {
|
||||
width.value = window.innerWidth;
|
||||
height.value = window.innerHeight;
|
||||
}, 100)[0];
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updateSize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateSize);
|
||||
});
|
||||
|
||||
const breakpoints = computed(() => ({
|
||||
xs: width.value < 576,
|
||||
sm: width.value >= 576 && width.value < 768,
|
||||
md: width.value >= 768 && width.value < 992,
|
||||
lg: width.value >= 992 && width.value < 1200,
|
||||
xl: width.value >= 1200 && width.value < 1600,
|
||||
xxl: width.value >= 1600
|
||||
}));
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
breakpoints
|
||||
};
|
||||
}
|
||||
|
||||
// 组件可见性检测
|
||||
export function useVisibility(threshold = 0.1) {
|
||||
const elementRef = ref<HTMLElement>();
|
||||
const isVisible = ref(false);
|
||||
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (elementRef.value) {
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
isVisible.value = entry.isIntersecting;
|
||||
});
|
||||
},
|
||||
{ threshold }
|
||||
);
|
||||
|
||||
observer.observe(elementRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
elementRef,
|
||||
isVisible
|
||||
};
|
||||
}
|
||||
|
||||
// 长列表优化
|
||||
export function useLongList<T>(
|
||||
data: Ref<T[]>,
|
||||
itemHeight = 50,
|
||||
visibleCount = 10
|
||||
) {
|
||||
const scrollTop = ref(0);
|
||||
const containerRef = ref<HTMLElement>();
|
||||
|
||||
const startIndex = computed(() => {
|
||||
return Math.floor(scrollTop.value / itemHeight);
|
||||
});
|
||||
|
||||
const endIndex = computed(() => {
|
||||
return Math.min(startIndex.value + visibleCount, data.value.length);
|
||||
});
|
||||
|
||||
const visibleData = computed(() => {
|
||||
return data.value.slice(startIndex.value, endIndex.value);
|
||||
});
|
||||
|
||||
const paddingTop = computed(() => {
|
||||
return startIndex.value * itemHeight;
|
||||
});
|
||||
|
||||
const paddingBottom = computed(() => {
|
||||
return (data.value.length - endIndex.value) * itemHeight;
|
||||
});
|
||||
|
||||
const handleScroll = useThrottle((event: Event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
scrollTop.value = target.scrollTop;
|
||||
}, 16)[0];
|
||||
|
||||
onMounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.addEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (containerRef.value) {
|
||||
containerRef.value.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
containerRef,
|
||||
visibleData,
|
||||
paddingTop,
|
||||
paddingBottom
|
||||
};
|
||||
}
|
||||
|
||||
// 内存泄漏检测
|
||||
export function useMemoryLeakDetection(componentName: string) {
|
||||
const watchers: WatchStopHandle[] = [];
|
||||
const timers: number[] = [];
|
||||
const listeners: Array<{
|
||||
element: EventTarget;
|
||||
event: string;
|
||||
handler: EventListener;
|
||||
}> = [];
|
||||
|
||||
const addWatcher = (stopHandle: WatchStopHandle) => {
|
||||
watchers.push(stopHandle);
|
||||
};
|
||||
|
||||
const addTimer = (timerId: number) => {
|
||||
timers.push(timerId);
|
||||
};
|
||||
|
||||
const addListener = (
|
||||
element: EventTarget,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
) => {
|
||||
element.addEventListener(event, handler);
|
||||
listeners.push({ element, event, handler });
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理 watchers
|
||||
watchers.forEach((stop) => stop());
|
||||
|
||||
// 清理 timers
|
||||
timers.forEach((id) => clearTimeout(id));
|
||||
|
||||
// 清理 listeners
|
||||
listeners.forEach(({ element, event, handler }) => {
|
||||
element.removeEventListener(event, handler);
|
||||
});
|
||||
|
||||
console.log(`${componentName} 组件已清理完成`);
|
||||
});
|
||||
|
||||
return {
|
||||
addWatcher,
|
||||
addTimer,
|
||||
addListener
|
||||
};
|
||||
}
|
||||
71
src/utils/document-title-util.ts
Normal file
71
src/utils/document-title-util.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
import {
|
||||
routeI18nKey,
|
||||
findTabByPath
|
||||
} from 'ele-admin-pro/es/ele-pro-layout/util';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { PROJECT_NAME, REDIRECT_PATH, I18N_ENABLE } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 修改浏览器标题
|
||||
* @param title 标题
|
||||
*/
|
||||
export function setDocumentTitle(title: string) {
|
||||
const names: string[] = [];
|
||||
const tenantName = localStorage.getItem('TenantName');
|
||||
if (title) {
|
||||
names.push(title);
|
||||
}
|
||||
if (tenantName) {
|
||||
names.push(tenantName);
|
||||
} else {
|
||||
names.push(PROJECT_NAME);
|
||||
}
|
||||
document.title = names.join(' - ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由切换更新浏览器标题
|
||||
*/
|
||||
export function useSetDocumentTitle() {
|
||||
const { currentRoute } = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const themeStore = useThemeStore();
|
||||
const { tabs } = storeToRefs(themeStore);
|
||||
|
||||
const updateTitle = (route: RouteLocationNormalizedLoaded) => {
|
||||
const { path, meta, fullPath } = route;
|
||||
if (path.includes(REDIRECT_PATH)) {
|
||||
return;
|
||||
}
|
||||
const pathKey = routeI18nKey(path);
|
||||
if (!pathKey) {
|
||||
return;
|
||||
}
|
||||
const tab = findTabByPath(fullPath, tabs.value);
|
||||
const title = tab?.title || (meta?.title as string);
|
||||
if (!I18N_ENABLE) {
|
||||
setDocumentTitle(title);
|
||||
return;
|
||||
}
|
||||
const k = `route.${pathKey}._name`;
|
||||
const v = t(k);
|
||||
setDocumentTitle(v === k || !v ? title : v);
|
||||
};
|
||||
|
||||
watch(
|
||||
currentRoute,
|
||||
(route) => {
|
||||
updateTitle(route);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(locale, () => {
|
||||
updateTitle(currentRoute.value);
|
||||
});
|
||||
}
|
||||
144
src/utils/domain.ts
Normal file
144
src/utils/domain.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { isNumber } from 'ele-admin-pro';
|
||||
import { listCmsWebsite } from '@/api/cms/cmsWebsite';
|
||||
|
||||
// 解析域名结构
|
||||
export function getHost(): any {
|
||||
const host = window.location.host;
|
||||
return host.split('.');
|
||||
}
|
||||
|
||||
// 是否https
|
||||
export function isHttps() {
|
||||
const protocol = window.location.protocol;
|
||||
if (protocol == 'https:') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始域名
|
||||
* @return http://www.domain.com
|
||||
*/
|
||||
export function getOriginDomain(): string {
|
||||
return window.origin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 域名的第一部分
|
||||
* 获取tenantId
|
||||
* @return 10140
|
||||
*/
|
||||
export function getDomainPart1(): any {
|
||||
const split = getHost();
|
||||
if (split[0] == '127') {
|
||||
return undefined;
|
||||
}
|
||||
const ip = Number(split[0]);
|
||||
if (ip < 1000) {
|
||||
return undefined;
|
||||
}
|
||||
if (isNumber(split[0])) {
|
||||
return split[0];
|
||||
}
|
||||
const tenantId = localStorage.getItem('TenantId');
|
||||
if (tenantId) {
|
||||
return Number(tenantId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过解析泛域名获取租户ID
|
||||
* https://10140.wsdns.cn
|
||||
* @return 10140
|
||||
*/
|
||||
export function getTenantId() {
|
||||
return getDomainPart1();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* hostname
|
||||
*/
|
||||
export function getHostname(): string {
|
||||
return window.location.hostname;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域名
|
||||
* @return https://www.domain.com
|
||||
*/
|
||||
export function getDomain(): string {
|
||||
return window.location.protocol + '//www.' + getRootDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取根域名
|
||||
* abc.com
|
||||
*/
|
||||
export function getRootDomain(): string {
|
||||
const split = getHost();
|
||||
return split[split.length - 2] + '.' + split[split.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取二级域名
|
||||
* @return abc.com
|
||||
*/
|
||||
export function getSubDomainPath(): string {
|
||||
const split = getHost();
|
||||
if (split.length == 2) {
|
||||
return '';
|
||||
}
|
||||
return split[split.length - 3];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品标识
|
||||
* @return 10048
|
||||
*/
|
||||
export function getProductCode(): string | null {
|
||||
const subDomain = getSubDomainPath();
|
||||
if (subDomain == undefined) {
|
||||
return null;
|
||||
}
|
||||
const split = subDomain.split('-');
|
||||
return split[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台域名
|
||||
*/
|
||||
export function navSubDomain(path): string {
|
||||
return `${window.location.protocol}//${path}.${getRootDomain()}`;
|
||||
}
|
||||
|
||||
// 获取网站域名(推荐使用)
|
||||
export function getSiteDomain(): string {
|
||||
const siteDomain = localStorage.getItem('Domain');
|
||||
if (!siteDomain) {
|
||||
listCmsWebsite({ limit: 1 }).then((list) => {
|
||||
if (list.length > 0) {
|
||||
const d = list[0];
|
||||
if (d.domain) {
|
||||
localStorage.setItem('Domain', `https://${d.domain}`);
|
||||
} else {
|
||||
localStorage.setItem('Domain', `https://${d.websiteCode}.wsdns.cn`);
|
||||
}
|
||||
return localStorage.getItem('Domain');
|
||||
}
|
||||
});
|
||||
}
|
||||
// 开发环境调试域名
|
||||
if (localStorage.getItem('DevDomain')) {
|
||||
return `${localStorage.getItem('DevDomain')}`;
|
||||
}
|
||||
return `${localStorage.getItem('Domain')}`;
|
||||
}
|
||||
|
||||
// 检查 URL 是否为 HTTPS
|
||||
export const checkIfHttps = (text: string) => {
|
||||
const url = new URL(text);
|
||||
return url.protocol === 'https:';
|
||||
};
|
||||
47
src/utils/editor.ts
Normal file
47
src/utils/editor.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { ref } from 'vue';
|
||||
import TinymceEditor from '@/components/TinymceEditor/index.vue';
|
||||
|
||||
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
|
||||
// 编辑器配置信息
|
||||
export function editorConfig(height?: 500): void {
|
||||
const resultData = ref<any>({
|
||||
height: height,
|
||||
// 自定义文件上传(这里使用把选择的文件转成 blob 演示)
|
||||
file_picker_callback: (callback: any, _value: any, meta: any) => {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('type', 'file');
|
||||
// 设定文件可选类型
|
||||
if (meta.filetype === 'image') {
|
||||
input.setAttribute('accept', 'image/*');
|
||||
} else if (meta.filetype === 'media') {
|
||||
input.setAttribute('accept', 'video/*');
|
||||
}
|
||||
input.onchange = () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (meta.filetype === 'media') {
|
||||
if (!file.type.startsWith('video/')) {
|
||||
editorRef.value?.alert({ content: '只能选择视频文件' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
editorRef.value?.alert({ content: '大小不能超过 20MB' });
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result != null) {
|
||||
const blob = new Blob([e.target.result], { type: file.type });
|
||||
callback(URL.createObjectURL(blob));
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
return resultData;
|
||||
}
|
||||
371
src/utils/enhanced-request.ts
Normal file
371
src/utils/enhanced-request.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 增强的 API 请求工具
|
||||
*/
|
||||
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
|
||||
import { apiPerformanceMonitor } from './performance';
|
||||
import { memoryCache } from './cache-manager';
|
||||
import { getToken } from './token-util';
|
||||
import { API_BASE_URL, TOKEN_HEADER_NAME } from '@/config/setting';
|
||||
|
||||
// 请求配置接口
|
||||
interface EnhancedRequestConfig extends AxiosRequestConfig {
|
||||
// 缓存配置
|
||||
cache?: {
|
||||
enabled: boolean;
|
||||
expiry?: number;
|
||||
key?: string;
|
||||
tags?: string[];
|
||||
};
|
||||
// 重试配置
|
||||
retry?: {
|
||||
times: number;
|
||||
delay: number;
|
||||
condition?: (error: AxiosError) => boolean;
|
||||
};
|
||||
// 性能监控
|
||||
performance?: boolean;
|
||||
// 请求去重
|
||||
dedupe?: boolean;
|
||||
// 超时重试
|
||||
timeoutRetry?: boolean;
|
||||
}
|
||||
|
||||
// 请求队列管理
|
||||
class RequestQueue {
|
||||
private pendingRequests = new Map<string, Promise<any>>();
|
||||
|
||||
// 生成请求键
|
||||
private generateKey(config: AxiosRequestConfig): string {
|
||||
const { method, url, params, data } = config;
|
||||
return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`;
|
||||
}
|
||||
|
||||
// 添加请求到队列
|
||||
add<T>(config: AxiosRequestConfig, executor: () => Promise<T>): Promise<T> {
|
||||
const key = this.generateKey(config);
|
||||
|
||||
if (this.pendingRequests.has(key)) {
|
||||
return this.pendingRequests.get(key);
|
||||
}
|
||||
|
||||
const promise = executor().finally(() => {
|
||||
this.pendingRequests.delete(key);
|
||||
});
|
||||
|
||||
this.pendingRequests.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// 清除队列
|
||||
clear(): void {
|
||||
this.pendingRequests.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 重试机制
|
||||
class RetryManager {
|
||||
static async retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
config: {
|
||||
times: number;
|
||||
delay: number;
|
||||
condition?: (error: any) => boolean;
|
||||
}
|
||||
): Promise<T> {
|
||||
let lastError: any;
|
||||
|
||||
for (let i = 0; i <= config.times; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
// 检查是否应该重试
|
||||
if (
|
||||
i < config.times &&
|
||||
(!config.condition || config.condition(error as AxiosError))
|
||||
) {
|
||||
await this.delay(config.delay * Math.pow(2, i)); // 指数退避
|
||||
console.warn(`请求重试 ${i + 1}/${config.times}:`, error);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
private static delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
// 增强的请求类
|
||||
export class EnhancedRequest {
|
||||
private instance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
private requestQueue = new RequestQueue();
|
||||
|
||||
constructor() {
|
||||
this.setupInterceptors();
|
||||
}
|
||||
|
||||
private setupInterceptors() {
|
||||
// 请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加认证头
|
||||
const token = getToken();
|
||||
if (token && config.headers) {
|
||||
config.headers[TOKEN_HEADER_NAME] = token;
|
||||
}
|
||||
|
||||
// 添加请求时间戳
|
||||
(config as any).startTime = Date.now();
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// 记录性能数据
|
||||
const config = response.config as any;
|
||||
if (config.startTime) {
|
||||
const duration = Date.now() - config.startTime;
|
||||
apiPerformanceMonitor.recordApiCall(config.url, duration);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 记录错误的性能数据
|
||||
const config = error.config as any;
|
||||
if (config && config.startTime) {
|
||||
const duration = Date.now() - config.startTime;
|
||||
apiPerformanceMonitor.recordApiCall(config.url, duration);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 通用请求方法
|
||||
async request<T = any>(config: EnhancedRequestConfig): Promise<T> {
|
||||
const {
|
||||
cache,
|
||||
retry,
|
||||
performance = true,
|
||||
dedupe = true,
|
||||
timeoutRetry = true,
|
||||
...axiosConfig
|
||||
} = config;
|
||||
|
||||
// 生成缓存键
|
||||
const cacheKey = cache?.key || this.generateCacheKey(axiosConfig);
|
||||
|
||||
// 尝试从缓存获取
|
||||
if (cache?.enabled) {
|
||||
const cachedData = memoryCache.get<T>(cacheKey);
|
||||
if (cachedData !== null) {
|
||||
return cachedData;
|
||||
}
|
||||
}
|
||||
|
||||
// 请求执行器
|
||||
const executor = async (): Promise<T> => {
|
||||
const response = await this.instance.request<T>(axiosConfig);
|
||||
|
||||
// 缓存响应数据
|
||||
if (cache?.enabled && response.data) {
|
||||
memoryCache.set(cacheKey, response.data, cache.expiry, cache.tags);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
// 请求去重
|
||||
if (dedupe) {
|
||||
return this.requestQueue.add(axiosConfig, async () => {
|
||||
// 重试机制
|
||||
if (retry) {
|
||||
return RetryManager.retry(executor, {
|
||||
...retry,
|
||||
condition: retry.condition || this.shouldRetry
|
||||
});
|
||||
}
|
||||
|
||||
return executor();
|
||||
});
|
||||
}
|
||||
|
||||
// 重试机制
|
||||
if (retry) {
|
||||
return RetryManager.retry(executor, {
|
||||
...retry,
|
||||
condition: retry.condition || this.shouldRetry
|
||||
});
|
||||
}
|
||||
|
||||
return executor();
|
||||
}
|
||||
|
||||
// GET 请求
|
||||
get<T = any>(url: string, config?: EnhancedRequestConfig): Promise<T> {
|
||||
return this.request<T>({
|
||||
...config,
|
||||
method: 'GET',
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
// POST 请求
|
||||
post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: EnhancedRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request<T>({
|
||||
...config,
|
||||
method: 'POST',
|
||||
url,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// PUT 请求
|
||||
put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: EnhancedRequestConfig
|
||||
): Promise<T> {
|
||||
return this.request<T>({
|
||||
...config,
|
||||
method: 'PUT',
|
||||
url,
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE 请求
|
||||
delete<T = any>(url: string, config?: EnhancedRequestConfig): Promise<T> {
|
||||
return this.request<T>({
|
||||
...config,
|
||||
method: 'DELETE',
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
// 批量请求
|
||||
async batch<T = any>(requests: EnhancedRequestConfig[]): Promise<T[]> {
|
||||
const promises = requests.map((config) => this.request<T>(config));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
// 并发控制请求
|
||||
async concurrent<T = any>(
|
||||
requests: EnhancedRequestConfig[],
|
||||
limit = 5
|
||||
): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
|
||||
for (let i = 0; i < requests.length; i += limit) {
|
||||
const batch = requests.slice(i, i + limit);
|
||||
const batchResults = await this.batch<T>(batch);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 生成缓存键
|
||||
private generateCacheKey(config: AxiosRequestConfig): string {
|
||||
const { method, url, params, data } = config;
|
||||
return `api_${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(
|
||||
data
|
||||
)}`;
|
||||
}
|
||||
|
||||
// 判断是否应该重试
|
||||
private shouldRetry(error: AxiosError): boolean {
|
||||
// 网络错误或超时错误重试
|
||||
if (!error.response) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 5xx 服务器错误重试
|
||||
const status = error.response.status;
|
||||
return status >= 500 && status < 600;
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
clearCache(tags?: string[]): void {
|
||||
if (tags) {
|
||||
memoryCache.clearByTags(tags);
|
||||
} else {
|
||||
memoryCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 取消所有请求
|
||||
cancelAll(): void {
|
||||
this.requestQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
export const enhancedRequest = new EnhancedRequest();
|
||||
|
||||
// 便捷方法
|
||||
export const {
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
delete: del,
|
||||
batch,
|
||||
concurrent
|
||||
} = enhancedRequest;
|
||||
|
||||
// 带缓存的 GET 请求
|
||||
export function cachedGet<T = any>(
|
||||
url: string,
|
||||
config?: Omit<EnhancedRequestConfig, 'cache'> & {
|
||||
expiry?: number;
|
||||
tags?: string[];
|
||||
}
|
||||
): Promise<T> {
|
||||
const { expiry = 5 * 60 * 1000, tags, ...restConfig } = config || {};
|
||||
|
||||
return enhancedRequest.get<T>(url, {
|
||||
...restConfig,
|
||||
cache: {
|
||||
enabled: true,
|
||||
expiry,
|
||||
tags
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 带重试的请求
|
||||
export function retryRequest<T = any>(
|
||||
config: EnhancedRequestConfig,
|
||||
retryTimes = 3,
|
||||
retryDelay = 1000
|
||||
): Promise<T> {
|
||||
return enhancedRequest.request<T>({
|
||||
...config,
|
||||
retry: {
|
||||
times: retryTimes,
|
||||
delay: retryDelay
|
||||
}
|
||||
});
|
||||
}
|
||||
358
src/utils/lazy-load.ts
Normal file
358
src/utils/lazy-load.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 组件懒加载工具
|
||||
*/
|
||||
import { defineAsyncComponent, Component } from 'vue';
|
||||
import { LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import { h } from 'vue';
|
||||
|
||||
// 加载状态组件
|
||||
const LoadingComponent = {
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '200px',
|
||||
fontSize: '16px',
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
[h(LoadingOutlined, { style: { marginRight: '8px' } }), '加载中...']
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 错误状态组件
|
||||
const ErrorComponent = {
|
||||
props: ['error'],
|
||||
setup(props: { error: Error }) {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '200px',
|
||||
padding: '20px',
|
||||
color: '#ff4d4f',
|
||||
backgroundColor: '#fff2f0',
|
||||
border: '1px solid #ffccc7',
|
||||
borderRadius: '6px'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(
|
||||
'div',
|
||||
{ style: { fontSize: '16px', marginBottom: '8px' } },
|
||||
'组件加载失败'
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ style: { fontSize: '12px', color: '#999' } },
|
||||
props.error.message
|
||||
),
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
style: {
|
||||
marginTop: '12px',
|
||||
padding: '4px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#fff',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
onClick: () => window.location.reload()
|
||||
},
|
||||
'重新加载'
|
||||
)
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// 懒加载配置选项
|
||||
interface LazyLoadOptions {
|
||||
loading?: Component;
|
||||
error?: Component;
|
||||
delay?: number;
|
||||
timeout?: number;
|
||||
retries?: number;
|
||||
retryDelay?: number;
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions: LazyLoadOptions = {
|
||||
loading: LoadingComponent,
|
||||
error: ErrorComponent,
|
||||
delay: 200,
|
||||
timeout: 30000,
|
||||
retries: 3,
|
||||
retryDelay: 1000
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建懒加载组件
|
||||
* @param loader 组件加载函数
|
||||
* @param options 配置选项
|
||||
*/
|
||||
export function createLazyComponent(
|
||||
loader: () => Promise<any>,
|
||||
options: LazyLoadOptions = {}
|
||||
) {
|
||||
const config = { ...defaultOptions, ...options };
|
||||
|
||||
return defineAsyncComponent({
|
||||
loader: createRetryLoader(loader, config.retries!, config.retryDelay!),
|
||||
loadingComponent: config.loading,
|
||||
errorComponent: config.error,
|
||||
delay: config.delay,
|
||||
timeout: config.timeout
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带重试机制的加载器
|
||||
*/
|
||||
function createRetryLoader(
|
||||
loader: () => Promise<any>,
|
||||
retries: number,
|
||||
retryDelay: number
|
||||
) {
|
||||
return async () => {
|
||||
let lastError: Error;
|
||||
|
||||
for (let i = 0; i <= retries; i++) {
|
||||
try {
|
||||
return await loader();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
if (i < retries) {
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
||||
console.warn(`组件加载失败,正在重试 (${i + 1}/${retries}):`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由懒加载
|
||||
*/
|
||||
export function lazyRoute(
|
||||
loader: () => Promise<any>,
|
||||
options?: LazyLoadOptions
|
||||
) {
|
||||
return createLazyComponent(loader, {
|
||||
...options,
|
||||
loading: options?.loading || {
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '60vh',
|
||||
fontSize: '16px',
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(LoadingOutlined, { style: { marginRight: '8px' } }),
|
||||
'页面加载中...'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模态框懒加载
|
||||
*/
|
||||
export function lazyModal(
|
||||
loader: () => Promise<any>,
|
||||
options?: LazyLoadOptions
|
||||
) {
|
||||
return createLazyComponent(loader, {
|
||||
...options,
|
||||
loading: options?.loading || {
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '300px',
|
||||
fontSize: '14px',
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
[h(LoadingOutlined, { style: { marginRight: '8px' } }), '加载中...']
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 图表懒加载
|
||||
*/
|
||||
export function lazyChart(
|
||||
loader: () => Promise<any>,
|
||||
options?: LazyLoadOptions
|
||||
) {
|
||||
return createLazyComponent(loader, {
|
||||
...options,
|
||||
loading: options?.loading || {
|
||||
setup() {
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: '400px',
|
||||
backgroundColor: '#fafafa',
|
||||
border: '1px dashed #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
color: '#999'
|
||||
}
|
||||
},
|
||||
[
|
||||
h(LoadingOutlined, { style: { marginRight: '8px' } }),
|
||||
'图表加载中...'
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载组件
|
||||
*/
|
||||
export class ComponentPreloader {
|
||||
private preloadedComponents = new Map<string, Promise<any>>();
|
||||
|
||||
/**
|
||||
* 预加载组件
|
||||
*/
|
||||
preload(key: string, loader: () => Promise<any>) {
|
||||
if (!this.preloadedComponents.has(key)) {
|
||||
this.preloadedComponents.set(key, loader());
|
||||
}
|
||||
return this.preloadedComponents.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预加载的组件
|
||||
*/
|
||||
get(key: string) {
|
||||
return this.preloadedComponents.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除预加载的组件
|
||||
*/
|
||||
clear(key?: string) {
|
||||
if (key) {
|
||||
this.preloadedComponents.delete(key);
|
||||
} else {
|
||||
this.preloadedComponents.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量预加载
|
||||
*/
|
||||
batchPreload(components: Record<string, () => Promise<any>>) {
|
||||
Object.entries(components).forEach(([key, loader]) => {
|
||||
this.preload(key, loader);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 全局预加载器实例
|
||||
export const componentPreloader = new ComponentPreloader();
|
||||
|
||||
/**
|
||||
* 智能懒加载 - 根据网络状况调整策略
|
||||
*/
|
||||
export function smartLazyComponent(
|
||||
loader: () => Promise<any>,
|
||||
options: LazyLoadOptions = {}
|
||||
) {
|
||||
// 检测网络状况
|
||||
const connection = (navigator as any).connection;
|
||||
const isSlowNetwork =
|
||||
connection &&
|
||||
(connection.effectiveType === 'slow-2g' ||
|
||||
connection.effectiveType === '2g' ||
|
||||
connection.saveData);
|
||||
|
||||
// 根据网络状况调整配置
|
||||
const smartOptions = {
|
||||
...options,
|
||||
timeout: isSlowNetwork ? 60000 : options.timeout || 30000,
|
||||
retries: isSlowNetwork ? 5 : options.retries || 3,
|
||||
retryDelay: isSlowNetwork ? 2000 : options.retryDelay || 1000
|
||||
};
|
||||
|
||||
return createLazyComponent(loader, smartOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可见性懒加载 - 只有当组件进入视口时才加载
|
||||
*/
|
||||
export function visibilityLazyComponent(
|
||||
loader: () => Promise<any>,
|
||||
options: LazyLoadOptions = {}
|
||||
) {
|
||||
return defineAsyncComponent({
|
||||
loader: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
observer.disconnect();
|
||||
loader().then(resolve).catch(reject);
|
||||
}
|
||||
});
|
||||
|
||||
// 创建一个占位元素来观察
|
||||
const placeholder = document.createElement('div');
|
||||
document.body.appendChild(placeholder);
|
||||
observer.observe(placeholder);
|
||||
|
||||
// 清理函数
|
||||
setTimeout(() => {
|
||||
observer.disconnect();
|
||||
document.body.removeChild(placeholder);
|
||||
reject(new Error('Visibility timeout'));
|
||||
}, options.timeout || 30000);
|
||||
});
|
||||
},
|
||||
loadingComponent: options.loading || LoadingComponent,
|
||||
errorComponent: options.error || ErrorComponent,
|
||||
delay: options.delay || 200
|
||||
});
|
||||
}
|
||||
42
src/utils/merchant.ts
Normal file
42
src/utils/merchant.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// 获取商户ID
|
||||
export const getMerchantId = () => {
|
||||
const merchantId = localStorage.getItem('MerchantId');
|
||||
if (merchantId) {
|
||||
return Number(merchantId);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 获取商户名称
|
||||
export const getMerchantName = () => {
|
||||
const MerchantName = localStorage.getItem('MerchantName');
|
||||
if (MerchantName) {
|
||||
return MerchantName;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// 获取商户Logo
|
||||
export const getMerchantAvatar = () => {
|
||||
const MerchantLogo = localStorage.getItem('MerchantLogo');
|
||||
if (MerchantLogo) {
|
||||
return MerchantLogo;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getRoleIdByMerchant = () => {
|
||||
const id = localStorage.getItem('RoleIdByMerchant');
|
||||
if (id) {
|
||||
return Number(id);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getRoleNameByMerchant = () => {
|
||||
const name = localStorage.getItem('RoleNameByMerchant');
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
18
src/utils/on-size-change.ts
Normal file
18
src/utils/on-size-change.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 监听屏幕尺寸改变封装
|
||||
*/
|
||||
import { watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
|
||||
export function onSizeChange(hook: Function) {
|
||||
if (!hook) {
|
||||
return;
|
||||
}
|
||||
const themeStore = useThemeStore();
|
||||
const { contentWidth } = storeToRefs(themeStore);
|
||||
|
||||
watch(contentWidth, () => {
|
||||
hook();
|
||||
});
|
||||
}
|
||||
43
src/utils/oss.js
Normal file
43
src/utils/oss.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import OSS from 'ali-oss';
|
||||
const baseUrl = 'http://oss-aishangjia.oss-cn-shenzhen.aliyuncs.com';
|
||||
export const uploadAliOss = async (file, options) => {
|
||||
// 构建上传文件参数
|
||||
|
||||
// 获取上传文件所需要的STS Token
|
||||
// 直接通过node.js上传
|
||||
// console.log(token)
|
||||
const client = new OSS({
|
||||
region: 'oss-cn-shenzhen',
|
||||
accessKeyId: 'LTAI4GKGZ9Z2Z8JZ77c3GNZP',
|
||||
accessKeySecret: 'BiDkpS7UXj72HWwDWaFZxiXjNFBNCM',
|
||||
bucket: 'oss-aishangjia',
|
||||
secure: true
|
||||
});
|
||||
|
||||
const headers = {
|
||||
// 指定该Object被下载时的网页缓存行为。
|
||||
'Cache-Control': 'no-cache',
|
||||
// 指定该Object被下载时的名称。
|
||||
// 指定该Object被下载时的内容编码格式。
|
||||
'Content-Encoding': 'utf-8',
|
||||
// 指定过期时间,单位为毫秒。
|
||||
Expires: '1000',
|
||||
// 指定Object的存储类型。
|
||||
'x-oss-storage-class': 'Standard',
|
||||
// 指定初始化分片上传时是否覆盖同名Object。此处设置为true,表示禁止覆盖同名Object。
|
||||
'x-oss-forbid-overwrite': 'true'
|
||||
};
|
||||
|
||||
const suffix = file.name.substring(file.name.lastIndexOf('.')); // .txt
|
||||
const objectName = Date.now() + suffix;
|
||||
|
||||
// object-name可以自定义为文件名(例如file.txt)或目录(例如abc/test/file.txt)的形式,实现将文件上传至当前Bucket或Bucket下的指定目录。
|
||||
const result = await client.multipartUpload(objectName, file, {
|
||||
...options,
|
||||
parallel: 4,
|
||||
partSize: 1024 * 1024 * 5
|
||||
});
|
||||
|
||||
result.url = `${baseUrl}/${result.name}`;
|
||||
return Promise.resolve(result);
|
||||
};
|
||||
261
src/utils/page-tab-util.ts
Normal file
261
src/utils/page-tab-util.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* 页签操作封装
|
||||
*/
|
||||
import { unref } from 'vue';
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router';
|
||||
import type { TabItem, TabRemoveOption } from 'ele-admin-pro/es';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import router, { resetDynamicRoutes } from '@/router';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import type { RouteReloadOption } from '@/store/modules/theme';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { removeToken } from '@/utils/token-util';
|
||||
import { setDocumentTitle } from '@/utils/document-title-util';
|
||||
import {
|
||||
HOME_PATH,
|
||||
LAYOUT_PATH,
|
||||
REDIRECT_PATH,
|
||||
REPEATABLE_TABS
|
||||
} from '@/config/setting';
|
||||
const HOME_ROUTE = HOME_PATH || LAYOUT_PATH;
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
|
||||
/**
|
||||
* 刷新页签参数类型
|
||||
*/
|
||||
export interface TabReloadOptions {
|
||||
// 是否是主页
|
||||
isHome?: boolean;
|
||||
// 路由地址
|
||||
fullPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前路由
|
||||
*/
|
||||
export function reloadPageTab(option?: TabReloadOptions) {
|
||||
if (!option) {
|
||||
// 刷新当前路由
|
||||
const { path, fullPath, query } = unref(router.currentRoute);
|
||||
if (path.includes(REDIRECT_PATH)) {
|
||||
return;
|
||||
}
|
||||
const isHome = isHomeRoute(unref(router.currentRoute));
|
||||
setRouteReload({
|
||||
reloadHome: isHome,
|
||||
reloadPath: isHome ? void 0 : fullPath
|
||||
});
|
||||
router.replace({
|
||||
path: REDIRECT_PATH + path,
|
||||
query
|
||||
});
|
||||
} else {
|
||||
// 刷新指定页签
|
||||
const { fullPath, isHome } = option;
|
||||
setRouteReload({
|
||||
reloadHome: isHome,
|
||||
reloadPath: isHome ? void 0 : fullPath
|
||||
});
|
||||
router.replace(REDIRECT_PATH + fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前页签
|
||||
*/
|
||||
export function finishPageTab() {
|
||||
const key = getRouteTabKey();
|
||||
removePageTab({ key, active: key });
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭页签
|
||||
*/
|
||||
export function removePageTab(option: TabRemoveOption) {
|
||||
useThemeStore()
|
||||
.tabRemove(option)
|
||||
.then(({ path, home }) => {
|
||||
if (path) {
|
||||
router.push(path);
|
||||
} else if (home) {
|
||||
router.push(HOME_ROUTE);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('当前页签不可关闭');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧页签
|
||||
*/
|
||||
export function removeLeftPageTab(option: TabRemoveOption) {
|
||||
useThemeStore()
|
||||
.tabRemoveLeft(option)
|
||||
.then(({ path }) => {
|
||||
if (path) {
|
||||
router.push(path);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('左侧没有可关闭的页签');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧页签
|
||||
*/
|
||||
export function removeRightPageTab(option: TabRemoveOption) {
|
||||
useThemeStore()
|
||||
.tabRemoveRight(option)
|
||||
.then(({ path, home }) => {
|
||||
if (path) {
|
||||
router.push(path);
|
||||
} else if (home) {
|
||||
router.push(HOME_ROUTE);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('右侧没有可关闭的页签');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其它页签
|
||||
*/
|
||||
export function removeOtherPageTab(option: TabRemoveOption) {
|
||||
useThemeStore()
|
||||
.tabRemoveOther(option)
|
||||
.then(({ path, home }) => {
|
||||
if (path) {
|
||||
router.push(path);
|
||||
} else if (home) {
|
||||
router.push(HOME_ROUTE);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('没有可关闭的页签');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭全部页签
|
||||
* @param active 当前选中页签
|
||||
*/
|
||||
export function removeAllPageTab(active: string) {
|
||||
useThemeStore()
|
||||
.tabRemoveAll(active)
|
||||
.then(({ home }) => {
|
||||
if (home) {
|
||||
router.push(HOME_ROUTE);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('没有可关闭的页签');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后清空页签
|
||||
*/
|
||||
export function cleanPageTabs() {
|
||||
useThemeStore().setTabs([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加页签
|
||||
* @param data 页签数据
|
||||
*/
|
||||
export function addPageTab(data: TabItem | TabItem[]) {
|
||||
useThemeStore().tabAdd(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改页签
|
||||
* @param data 页签数据
|
||||
*/
|
||||
export function setPageTab(data: TabItem) {
|
||||
useThemeStore().tabSetItem(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改页签标题
|
||||
* @param title 标题
|
||||
*/
|
||||
export function setPageTabTitle(title: string) {
|
||||
setPageTab({ key: getRouteTabKey(), title });
|
||||
setDocumentTitle(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前路由对应的页签 key
|
||||
*/
|
||||
export function getRouteTabKey() {
|
||||
const { path, fullPath, meta } = unref(router.currentRoute);
|
||||
const isUnique = meta.tabUnique === false || REPEATABLE_TABS.includes(path);
|
||||
return isUnique ? fullPath : path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主页的组件名称
|
||||
* @param components 组件名称
|
||||
*/
|
||||
export function setHomeComponents(components: string[]) {
|
||||
useThemeStore().setHomeComponents(components);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置路由刷新信息
|
||||
* @param option 路由刷新参数
|
||||
*/
|
||||
export function setRouteReload(option: RouteReloadOption | null) {
|
||||
return useThemeStore().setRouteReload(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断路由是否是主页
|
||||
* @param route 路由信息
|
||||
*/
|
||||
export function isHomeRoute(route: RouteLocationNormalizedLoaded) {
|
||||
const { path, matched } = route;
|
||||
if (HOME_ROUTE === path) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
matched[0] &&
|
||||
matched[0].path === LAYOUT_PATH &&
|
||||
matched[0].redirect === path
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录成功后跳转首页
|
||||
* @param from 登录前的地址
|
||||
*/
|
||||
export function goHomeRoute(from?: string) {
|
||||
router.replace(from || HOME_ROUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @param route 是否使用路由跳转
|
||||
* @param from 登录后跳转的地址
|
||||
*/
|
||||
export function logout(route?: boolean, from?: string) {
|
||||
removeToken();
|
||||
// 重置动态路由注册状态
|
||||
resetDynamicRoutes();
|
||||
// 重置用户状态
|
||||
const userStore = useUserStore();
|
||||
userStore.resetUserState();
|
||||
|
||||
if (route) {
|
||||
router.push({
|
||||
path: '/login',
|
||||
query: from ? { from } : void 0
|
||||
});
|
||||
} else {
|
||||
// 这样跳转避免再次登录重复注册动态路由
|
||||
location.replace(BASE_URL + 'login' + (from ? '?from=' + from : ''));
|
||||
}
|
||||
}
|
||||
273
src/utils/performance.ts
Normal file
273
src/utils/performance.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 性能监控工具
|
||||
*/
|
||||
|
||||
// 性能指标接口
|
||||
export interface PerformanceMetrics {
|
||||
// 页面加载时间
|
||||
pageLoadTime: number;
|
||||
// 首次内容绘制
|
||||
fcp: number;
|
||||
// 最大内容绘制
|
||||
lcp: number;
|
||||
// 首次输入延迟
|
||||
fid: number;
|
||||
// 累积布局偏移
|
||||
cls: number;
|
||||
// 内存使用情况
|
||||
memory?: {
|
||||
used: number;
|
||||
total: number;
|
||||
limit: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 性能监控类
|
||||
export class PerformanceMonitor {
|
||||
private metrics: Partial<PerformanceMetrics> = {};
|
||||
private observers: PerformanceObserver[] = [];
|
||||
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 监听页面加载完成
|
||||
if (document.readyState === 'complete') {
|
||||
this.measurePageLoad();
|
||||
} else {
|
||||
window.addEventListener('load', () => this.measurePageLoad());
|
||||
}
|
||||
|
||||
// 监听 Web Vitals
|
||||
this.observeWebVitals();
|
||||
}
|
||||
|
||||
private measurePageLoad() {
|
||||
const navigation = performance.getEntriesByType(
|
||||
'navigation'
|
||||
)[0] as PerformanceNavigationTiming;
|
||||
if (navigation) {
|
||||
this.metrics.pageLoadTime =
|
||||
navigation.loadEventEnd - navigation.fetchStart;
|
||||
}
|
||||
}
|
||||
|
||||
private observeWebVitals() {
|
||||
// FCP (First Contentful Paint)
|
||||
this.observePerformance('paint', (entries) => {
|
||||
const fcpEntry = entries.find(
|
||||
(entry) => entry.name === 'first-contentful-paint'
|
||||
);
|
||||
if (fcpEntry) {
|
||||
this.metrics.fcp = fcpEntry.startTime;
|
||||
}
|
||||
});
|
||||
|
||||
// LCP (Largest Contentful Paint)
|
||||
this.observePerformance('largest-contentful-paint', (entries) => {
|
||||
const lcpEntry = entries[entries.length - 1];
|
||||
if (lcpEntry) {
|
||||
this.metrics.lcp = lcpEntry.startTime;
|
||||
}
|
||||
});
|
||||
|
||||
// FID (First Input Delay)
|
||||
this.observePerformance('first-input', (entries) => {
|
||||
const fidEntry = entries[0];
|
||||
if (fidEntry) {
|
||||
this.metrics.fid = fidEntry.processingStart - fidEntry.startTime;
|
||||
}
|
||||
});
|
||||
|
||||
// CLS (Cumulative Layout Shift)
|
||||
this.observePerformance('layout-shift', (entries) => {
|
||||
let clsValue = 0;
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.hadRecentInput) {
|
||||
clsValue += entry.value;
|
||||
}
|
||||
});
|
||||
this.metrics.cls = clsValue;
|
||||
});
|
||||
}
|
||||
|
||||
private observePerformance(
|
||||
type: string,
|
||||
callback: (entries: PerformanceEntry[]) => void
|
||||
) {
|
||||
try {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
callback(list.getEntries());
|
||||
});
|
||||
observer.observe({ type, buffered: true });
|
||||
this.observers.push(observer);
|
||||
} catch (error) {
|
||||
console.warn(`Performance observer for ${type} not supported:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取内存使用情况
|
||||
getMemoryUsage(): PerformanceMetrics['memory'] | null {
|
||||
if ('memory' in performance) {
|
||||
const memory = (performance as any).memory;
|
||||
return {
|
||||
used: Math.round(memory.usedJSHeapSize / 1024 / 1024),
|
||||
total: Math.round(memory.totalJSHeapSize / 1024 / 1024),
|
||||
limit: Math.round(memory.jsHeapSizeLimit / 1024 / 1024)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取所有性能指标
|
||||
getMetrics(): PerformanceMetrics {
|
||||
return {
|
||||
...this.metrics,
|
||||
memory: this.getMemoryUsage()
|
||||
} as PerformanceMetrics;
|
||||
}
|
||||
|
||||
// 清理观察器
|
||||
disconnect() {
|
||||
this.observers.forEach((observer) => observer.disconnect());
|
||||
this.observers = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 路由性能监控
|
||||
export class RoutePerformanceMonitor {
|
||||
private routeStartTime = 0;
|
||||
private routeMetrics: Map<string, number[]> = new Map();
|
||||
|
||||
startRouteTimer() {
|
||||
this.routeStartTime = performance.now();
|
||||
}
|
||||
|
||||
endRouteTimer(routeName: string) {
|
||||
if (this.routeStartTime) {
|
||||
const duration = performance.now() - this.routeStartTime;
|
||||
|
||||
if (!this.routeMetrics.has(routeName)) {
|
||||
this.routeMetrics.set(routeName, []);
|
||||
}
|
||||
|
||||
const metrics = this.routeMetrics.get(routeName)!;
|
||||
metrics.push(duration);
|
||||
|
||||
// 只保留最近10次记录
|
||||
if (metrics.length > 10) {
|
||||
metrics.shift();
|
||||
}
|
||||
|
||||
this.routeStartTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
getRouteMetrics(routeName: string) {
|
||||
const metrics = this.routeMetrics.get(routeName) || [];
|
||||
if (metrics.length === 0) return null;
|
||||
|
||||
const avg = metrics.reduce((sum, time) => sum + time, 0) / metrics.length;
|
||||
const min = Math.min(...metrics);
|
||||
const max = Math.max(...metrics);
|
||||
|
||||
return { avg, min, max, count: metrics.length };
|
||||
}
|
||||
|
||||
getAllRouteMetrics() {
|
||||
const result: Record<string, any> = {};
|
||||
this.routeMetrics.forEach((metrics, routeName) => {
|
||||
result[routeName] = this.getRouteMetrics(routeName);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// API 性能监控
|
||||
export class ApiPerformanceMonitor {
|
||||
private apiMetrics: Map<string, number[]> = new Map();
|
||||
|
||||
recordApiCall(url: string, duration: number) {
|
||||
if (!this.apiMetrics.has(url)) {
|
||||
this.apiMetrics.set(url, []);
|
||||
}
|
||||
|
||||
const metrics = this.apiMetrics.get(url)!;
|
||||
metrics.push(duration);
|
||||
|
||||
// 只保留最近20次记录
|
||||
if (metrics.length > 20) {
|
||||
metrics.shift();
|
||||
}
|
||||
}
|
||||
|
||||
getApiMetrics(url: string) {
|
||||
const metrics = this.apiMetrics.get(url) || [];
|
||||
if (metrics.length === 0) return null;
|
||||
|
||||
const avg = metrics.reduce((sum, time) => sum + time, 0) / metrics.length;
|
||||
const min = Math.min(...metrics);
|
||||
const max = Math.max(...metrics);
|
||||
|
||||
return { avg, min, max, count: metrics.length };
|
||||
}
|
||||
|
||||
getSlowApis(threshold = 1000) {
|
||||
const slowApis: Array<{ url: string; avgTime: number }> = [];
|
||||
|
||||
this.apiMetrics.forEach((metrics, url) => {
|
||||
const avg = metrics.reduce((sum, time) => sum + time, 0) / metrics.length;
|
||||
if (avg > threshold) {
|
||||
slowApis.push({ url, avgTime: avg });
|
||||
}
|
||||
});
|
||||
|
||||
return slowApis.sort((a, b) => b.avgTime - a.avgTime);
|
||||
}
|
||||
}
|
||||
|
||||
// 全局性能监控实例
|
||||
export const performanceMonitor = new PerformanceMonitor();
|
||||
export const routePerformanceMonitor = new RoutePerformanceMonitor();
|
||||
export const apiPerformanceMonitor = new ApiPerformanceMonitor();
|
||||
|
||||
// 性能报告生成器
|
||||
export function generatePerformanceReport() {
|
||||
const metrics = performanceMonitor.getMetrics();
|
||||
const routeMetrics = routePerformanceMonitor.getAllRouteMetrics();
|
||||
const slowApis = apiPerformanceMonitor.getSlowApis();
|
||||
|
||||
return {
|
||||
webVitals: metrics,
|
||||
routes: routeMetrics,
|
||||
slowApis,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// 性能警告
|
||||
export function checkPerformanceWarnings() {
|
||||
const metrics = performanceMonitor.getMetrics();
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (metrics.lcp && metrics.lcp > 2500) {
|
||||
warnings.push(`LCP 过慢: ${metrics.lcp.toFixed(2)}ms (建议 < 2500ms)`);
|
||||
}
|
||||
|
||||
if (metrics.fid && metrics.fid > 100) {
|
||||
warnings.push(`FID 过慢: ${metrics.fid.toFixed(2)}ms (建议 < 100ms)`);
|
||||
}
|
||||
|
||||
if (metrics.cls && metrics.cls > 0.1) {
|
||||
warnings.push(`CLS 过高: ${metrics.cls.toFixed(3)} (建议 < 0.1)`);
|
||||
}
|
||||
|
||||
if (metrics.memory && metrics.memory.used > metrics.memory.limit * 0.8) {
|
||||
warnings.push(
|
||||
`内存使用过高: ${metrics.memory.used}MB / ${metrics.memory.limit}MB`
|
||||
);
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
119
src/utils/permission.ts
Normal file
119
src/utils/permission.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 按钮级权限控制
|
||||
*/
|
||||
import type { App } from 'vue';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
/* 判断数组是否有某些值 */
|
||||
function arrayHas(
|
||||
array: (string | undefined)[],
|
||||
value: string | string[]
|
||||
): boolean {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!array) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (array.indexOf(value[i]) === -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return array.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
/* 判断数组是否有任意值 */
|
||||
function arrayHasAny(
|
||||
array: (string | undefined)[],
|
||||
value: string | string[]
|
||||
): boolean {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!array) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (array.indexOf(value[i]) !== -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return array.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有某些角色
|
||||
* @param value 角色字符或字符数组
|
||||
*/
|
||||
export function hasRole(value: string | string[]): boolean {
|
||||
const userStore = useUserStore();
|
||||
return arrayHas(userStore?.roles, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有任意角色
|
||||
* @param value 角色字符或字符数组
|
||||
*/
|
||||
export function hasAnyRole(value: string | string[]): boolean {
|
||||
const userStore = useUserStore();
|
||||
return arrayHasAny(userStore?.roles, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有某些权限
|
||||
* @param value 权限字符或字符数组
|
||||
*/
|
||||
export function hasPermission(value: string | string[]): boolean {
|
||||
const userStore = useUserStore();
|
||||
return arrayHas(userStore?.authorities, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有任意权限
|
||||
* @param value 权限字符或字符数组
|
||||
*/
|
||||
export function hasAnyPermission(value: string | string[]): boolean {
|
||||
const userStore = useUserStore();
|
||||
return arrayHasAny(userStore?.authorities, value);
|
||||
}
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
// 添加自定义指令
|
||||
app.directive('role', {
|
||||
mounted: (el, binding) => {
|
||||
if (!hasRole(binding.value)) {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.directive('any-role', {
|
||||
mounted: (el, binding) => {
|
||||
if (!hasAnyRole(binding.value)) {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.directive('permission', {
|
||||
mounted: (el, binding) => {
|
||||
if (!hasPermission(binding.value)) {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
app.directive('any-permission', {
|
||||
mounted: (el, binding) => {
|
||||
if (!hasAnyPermission(binding.value)) {
|
||||
el.parentNode?.removeChild(el);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
53
src/utils/plug-uitl.ts
Normal file
53
src/utils/plug-uitl.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createVNode } from 'vue';
|
||||
import { Company } from '@/api/system/company/model';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { messageLoading } from 'ele-admin-pro';
|
||||
import { clone } from '@/api/system/menu';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import { Menu } from '@/api/system/menu/model';
|
||||
|
||||
// 表单数据
|
||||
const { form } = useFormData<Menu>({
|
||||
title: '',
|
||||
icon: '',
|
||||
path: '',
|
||||
component: '',
|
||||
tenantId: undefined,
|
||||
tenantName: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* 一键克隆
|
||||
* @param item
|
||||
*/
|
||||
export const onClone = (item: Company) => {
|
||||
const tenantId = Number(localStorage.getItem('TenantId'));
|
||||
if (tenantId == item.tenantId) {
|
||||
message.error('不能克隆自己');
|
||||
return false;
|
||||
}
|
||||
// 提交状态
|
||||
Modal.confirm({
|
||||
title: '确认操作吗?',
|
||||
content: `将复制【${item.tenantName}】的所有菜单和权限(不含数据),原有企业数据不会删除。`,
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = messageLoading('模块安装中请稍等...', 0);
|
||||
form.tenantId = item.tenantId;
|
||||
clone(form)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
setTimeout(() => {
|
||||
window.open('/', '_self');
|
||||
}, 1000);
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
369
src/utils/port-config-manager.ts
Normal file
369
src/utils/port-config-manager.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* 端口配置管理器
|
||||
* 集成环境变量和智能端口管理
|
||||
*/
|
||||
|
||||
import type { PortStrategy } from '@/lib/port-manager';
|
||||
import type { Environment } from '@/lib/port-strategy';
|
||||
|
||||
// 端口配置接口
|
||||
export interface PortEnvironmentConfig {
|
||||
// 基础配置
|
||||
strategy: 'auto' | 'manual' | 'tenant-based' | 'sequential';
|
||||
basePort: number;
|
||||
portRangeStart: number;
|
||||
portRangeEnd: number;
|
||||
|
||||
// 租户配置
|
||||
tenantPortOffset: number;
|
||||
environmentPortOffset: number;
|
||||
|
||||
// 行为配置
|
||||
autoDetect: boolean;
|
||||
strictMode: boolean;
|
||||
cacheEnabled: boolean;
|
||||
cacheExpiry: number;
|
||||
|
||||
// 开发服务器配置
|
||||
devHost: string;
|
||||
openBrowser: boolean;
|
||||
corsEnabled: boolean;
|
||||
httpsEnabled: boolean;
|
||||
}
|
||||
|
||||
// 配置验证结果
|
||||
export interface ConfigValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
// 端口配置管理器
|
||||
export class PortConfigManager {
|
||||
private config: PortEnvironmentConfig;
|
||||
private environment: Environment;
|
||||
|
||||
constructor() {
|
||||
this.environment = this.detectEnvironment();
|
||||
this.config = this.loadConfiguration();
|
||||
this.validateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前环境
|
||||
*/
|
||||
private detectEnvironment(): Environment {
|
||||
// 在 Vite 配置阶段,import.meta.env 可能不可用,使用 process.env
|
||||
const nodeEnv =
|
||||
(typeof process !== 'undefined' ? process.env.NODE_ENV : undefined) ||
|
||||
(typeof import.meta !== 'undefined' && import.meta.env
|
||||
? import.meta.env.NODE_ENV
|
||||
: undefined) ||
|
||||
'development';
|
||||
|
||||
switch (nodeEnv.toLowerCase()) {
|
||||
case 'production':
|
||||
case 'prod':
|
||||
return 'production';
|
||||
case 'test':
|
||||
case 'testing':
|
||||
return 'test';
|
||||
case 'staging':
|
||||
case 'stage':
|
||||
return 'staging';
|
||||
default:
|
||||
return 'development';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
private loadConfiguration(): PortEnvironmentConfig {
|
||||
// 获取环境变量的辅助函数
|
||||
const getEnv = (key: string, defaultValue?: string) => {
|
||||
if (typeof process !== 'undefined' && process.env) {
|
||||
return process.env[key];
|
||||
}
|
||||
if (typeof import.meta !== 'undefined' && import.meta.env) {
|
||||
return import.meta.env[key];
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
return {
|
||||
// 基础配置
|
||||
strategy: (getEnv('VITE_PORT_STRATEGY') as any) || 'auto',
|
||||
basePort: parseInt(getEnv('VITE_BASE_PORT') || '3000'),
|
||||
portRangeStart: parseInt(getEnv('VITE_PORT_RANGE_START') || '3000'),
|
||||
portRangeEnd: parseInt(getEnv('VITE_PORT_RANGE_END') || '9999'),
|
||||
|
||||
// 租户配置
|
||||
tenantPortOffset: parseInt(getEnv('VITE_TENANT_PORT_OFFSET') || '10'),
|
||||
environmentPortOffset: parseInt(
|
||||
getEnv('VITE_ENVIRONMENT_PORT_OFFSET') || '1000'
|
||||
),
|
||||
|
||||
// 行为配置
|
||||
autoDetect: getEnv('VITE_PORT_AUTO_DETECT') !== 'false',
|
||||
strictMode: getEnv('VITE_PORT_STRICT_MODE') === 'true',
|
||||
cacheEnabled: getEnv('VITE_PORT_CACHE_ENABLED') !== 'false',
|
||||
cacheExpiry: parseInt(getEnv('VITE_PORT_CACHE_EXPIRY') || '86400000'), // 24小时
|
||||
|
||||
// 开发服务器配置
|
||||
devHost: getEnv('VITE_DEV_HOST') || 'localhost',
|
||||
openBrowser: getEnv('VITE_DEV_OPEN_BROWSER') !== 'false',
|
||||
corsEnabled: getEnv('VITE_DEV_CORS_ENABLED') !== 'false',
|
||||
httpsEnabled: getEnv('VITE_DEV_HTTPS_ENABLED') === 'true'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
*/
|
||||
private validateConfiguration(): ConfigValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 验证端口范围
|
||||
if (this.config.portRangeStart >= this.config.portRangeEnd) {
|
||||
errors.push('端口范围无效:起始端口必须小于结束端口');
|
||||
}
|
||||
|
||||
if (
|
||||
this.config.portRangeStart < 1024 &&
|
||||
this.environment === 'production'
|
||||
) {
|
||||
warnings.push('生产环境使用系统端口(<1024)可能需要管理员权限');
|
||||
}
|
||||
|
||||
// 验证基础端口
|
||||
if (
|
||||
this.config.basePort < this.config.portRangeStart ||
|
||||
this.config.basePort > this.config.portRangeEnd
|
||||
) {
|
||||
errors.push('基础端口不在允许的端口范围内');
|
||||
}
|
||||
|
||||
// 验证租户偏移
|
||||
if (this.config.tenantPortOffset <= 0) {
|
||||
warnings.push('租户端口偏移为0可能导致端口冲突');
|
||||
}
|
||||
|
||||
// 环境特定验证
|
||||
switch (this.environment) {
|
||||
case 'development':
|
||||
if (this.config.httpsEnabled) {
|
||||
warnings.push('开发环境启用HTTPS可能增加配置复杂度');
|
||||
}
|
||||
if (!this.config.autoDetect) {
|
||||
recommendations.push('开发环境建议启用端口自动检测');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'production':
|
||||
if (!this.config.httpsEnabled) {
|
||||
warnings.push('生产环境建议启用HTTPS');
|
||||
}
|
||||
if (this.config.autoDetect) {
|
||||
warnings.push('生产环境不建议启用端口自动检测');
|
||||
}
|
||||
if (!this.config.strictMode) {
|
||||
recommendations.push('生产环境建议启用严格模式');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'test':
|
||||
if (this.config.openBrowser) {
|
||||
recommendations.push('测试环境建议禁用自动打开浏览器');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 缓存配置验证
|
||||
if (this.config.cacheEnabled && this.config.cacheExpiry < 60000) {
|
||||
warnings.push('缓存过期时间过短可能影响性能');
|
||||
}
|
||||
|
||||
const result = {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
recommendations
|
||||
};
|
||||
|
||||
// 输出验证结果
|
||||
if (errors.length > 0) {
|
||||
console.error('❌ 端口配置验证失败:', errors);
|
||||
}
|
||||
if (warnings.length > 0) {
|
||||
console.warn('⚠️ 端口配置警告:', warnings);
|
||||
}
|
||||
if (recommendations.length > 0) {
|
||||
console.info('💡 端口配置建议:', recommendations);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
getConfig(): PortEnvironmentConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取端口策略配置
|
||||
*/
|
||||
getPortStrategy(): PortStrategy {
|
||||
return {
|
||||
basePort: this.config.basePort,
|
||||
portRange: [this.config.portRangeStart, this.config.portRangeEnd],
|
||||
tenantOffset: this.config.tenantPortOffset,
|
||||
environmentOffset: this.config.environmentPortOffset,
|
||||
maxRetries: this.config.strictMode ? 10 : 50
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取开发服务器配置
|
||||
*/
|
||||
getDevServerConfig(): {
|
||||
host: string;
|
||||
port?: number;
|
||||
open: boolean;
|
||||
cors: boolean;
|
||||
https: boolean;
|
||||
strictPort: boolean;
|
||||
} {
|
||||
return {
|
||||
host: this.config.devHost,
|
||||
open: this.config.openBrowser,
|
||||
cors: this.config.corsEnabled,
|
||||
https: this.config.httpsEnabled,
|
||||
strictPort: this.config.strictMode
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取环境信息
|
||||
*/
|
||||
getEnvironmentInfo(): {
|
||||
current: Environment;
|
||||
config: PortEnvironmentConfig;
|
||||
validation: ConfigValidationResult;
|
||||
recommendations: string[];
|
||||
} {
|
||||
const validation = this.validateConfiguration();
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 基于环境生成建议
|
||||
switch (this.environment) {
|
||||
case 'development':
|
||||
recommendations.push('开发环境:优先考虑便利性和调试体验');
|
||||
recommendations.push('建议启用热重载和自动刷新功能');
|
||||
break;
|
||||
case 'test':
|
||||
recommendations.push('测试环境:注重隔离性和可重复性');
|
||||
recommendations.push('建议配置独立的端口范围避免冲突');
|
||||
break;
|
||||
case 'production':
|
||||
recommendations.push('生产环境:优先考虑安全性和稳定性');
|
||||
recommendations.push('建议使用固定端口和负载均衡');
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
current: this.environment,
|
||||
config: this.config,
|
||||
validation,
|
||||
recommendations: [...validation.recommendations, ...recommendations]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(
|
||||
updates: Partial<PortEnvironmentConfig>
|
||||
): ConfigValidationResult {
|
||||
this.config = { ...this.config, ...updates };
|
||||
return this.validateConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置为默认配置
|
||||
*/
|
||||
resetToDefaults(): void {
|
||||
this.config = this.loadConfiguration();
|
||||
console.log('🔄 端口配置已重置为默认值');
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出配置
|
||||
*/
|
||||
exportConfig(): string {
|
||||
const configLines = [
|
||||
'# 智能端口管理配置',
|
||||
`VITE_PORT_STRATEGY=${this.config.strategy}`,
|
||||
`VITE_BASE_PORT=${this.config.basePort}`,
|
||||
`VITE_PORT_RANGE_START=${this.config.portRangeStart}`,
|
||||
`VITE_PORT_RANGE_END=${this.config.portRangeEnd}`,
|
||||
`VITE_TENANT_PORT_OFFSET=${this.config.tenantPortOffset}`,
|
||||
`VITE_ENVIRONMENT_PORT_OFFSET=${this.config.environmentPortOffset}`,
|
||||
`VITE_PORT_AUTO_DETECT=${this.config.autoDetect}`,
|
||||
`VITE_PORT_STRICT_MODE=${this.config.strictMode}`,
|
||||
`VITE_PORT_CACHE_ENABLED=${this.config.cacheEnabled}`,
|
||||
`VITE_PORT_CACHE_EXPIRY=${this.config.cacheExpiry}`,
|
||||
'',
|
||||
'# 开发服务器配置',
|
||||
`VITE_DEV_HOST=${this.config.devHost}`,
|
||||
`VITE_DEV_OPEN_BROWSER=${this.config.openBrowser}`,
|
||||
`VITE_DEV_CORS_ENABLED=${this.config.corsEnabled}`,
|
||||
`VITE_DEV_HTTPS_ENABLED=${this.config.httpsEnabled}`
|
||||
];
|
||||
|
||||
return configLines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置摘要
|
||||
*/
|
||||
getConfigSummary(): {
|
||||
environment: Environment;
|
||||
strategy: string;
|
||||
portRange: string;
|
||||
features: string[];
|
||||
status: 'healthy' | 'warning' | 'error';
|
||||
} {
|
||||
const validation = this.validateConfiguration();
|
||||
const features: string[] = [];
|
||||
|
||||
if (this.config.autoDetect) features.push('自动检测');
|
||||
if (this.config.cacheEnabled) features.push('缓存启用');
|
||||
if (this.config.strictMode) features.push('严格模式');
|
||||
if (this.config.httpsEnabled) features.push('HTTPS');
|
||||
if (this.config.corsEnabled) features.push('CORS');
|
||||
|
||||
let status: 'healthy' | 'warning' | 'error' = 'healthy';
|
||||
if (validation.errors.length > 0) {
|
||||
status = 'error';
|
||||
} else if (validation.warnings.length > 0) {
|
||||
status = 'warning';
|
||||
}
|
||||
|
||||
return {
|
||||
environment: this.environment,
|
||||
strategy: this.config.strategy,
|
||||
portRange: `${this.config.portRangeStart}-${this.config.portRangeEnd}`,
|
||||
features,
|
||||
status
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 导出默认实例
|
||||
export const portConfigManager = new PortConfigManager();
|
||||
116
src/utils/request.ts
Normal file
116
src/utils/request.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* axios 实例
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
import { unref } from 'vue';
|
||||
import router from '@/router';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { API_BASE_URL, TOKEN_HEADER_NAME, LAYOUT_PATH } from '@/config/setting';
|
||||
import { getToken, setToken } from './token-util';
|
||||
import { logout } from './page-tab-util';
|
||||
import type { ApiResult } from '@/api';
|
||||
import { getHostname, getTenantId } from '@/utils/domain';
|
||||
import { getMerchantId } from '@/utils/merchant';
|
||||
|
||||
// 获取API基础地址的函数
|
||||
const getBaseUrl = (): string => {
|
||||
// 尝试从配置store获取后台配置的API地址
|
||||
try {
|
||||
// 如果store中没有,则尝试从localStorage获取
|
||||
const ApiUrl = localStorage.getItem('ApiUrl');
|
||||
if (ApiUrl) {
|
||||
return ApiUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取后台配置API地址失败:', error);
|
||||
}
|
||||
|
||||
// 如果后台没有配置API地址,则使用本地配置
|
||||
console.log('使用本地配置的API地址:', API_BASE_URL);
|
||||
return API_BASE_URL;
|
||||
};
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: getBaseUrl()
|
||||
});
|
||||
|
||||
/**
|
||||
* 添加请求拦截器
|
||||
*/
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
const TENANT_ID = getTenantId();
|
||||
const token = getToken();
|
||||
// 添加 token 到 header
|
||||
if (token && config.headers) {
|
||||
config.headers.common[TOKEN_HEADER_NAME] = token;
|
||||
}
|
||||
// 获取租户ID
|
||||
if (config.headers) {
|
||||
// 附加企业ID
|
||||
const companyId = localStorage.getItem('CompanyId');
|
||||
if (companyId) {
|
||||
config.headers.common['CompanyId'] = companyId;
|
||||
}
|
||||
// 附加商户ID
|
||||
if (getMerchantId()) {
|
||||
config.headers.common['MerchantId'] = getMerchantId();
|
||||
}
|
||||
// 通过网站域名获取租户ID
|
||||
if (getHostname()) {
|
||||
config.headers.common['Domain'] = getHostname();
|
||||
}
|
||||
// 解析二级域名获取租户ID
|
||||
if (getTenantId()) {
|
||||
config.headers.common['TenantId'] = getTenantId();
|
||||
return config;
|
||||
}
|
||||
if (TENANT_ID) {
|
||||
config.headers.common['TenantId'] = TENANT_ID;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 添加响应拦截器
|
||||
*/
|
||||
service.interceptors.response.use(
|
||||
(res: AxiosResponse<ApiResult<unknown>>) => {
|
||||
// 登录过期处理
|
||||
if (res.data?.code === 401) {
|
||||
const currentPath = unref(router.currentRoute).path;
|
||||
if (currentPath == LAYOUT_PATH) {
|
||||
logout(true);
|
||||
} else {
|
||||
Modal.destroyAll();
|
||||
Modal.info({
|
||||
title: '系统提示',
|
||||
content: '登录状态已过期, 请退出重新登录!',
|
||||
okText: '重新登录',
|
||||
onOk: () => {
|
||||
logout(false, currentPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
// token 自动续期
|
||||
const token = res.headers[TOKEN_HEADER_NAME.toLowerCase()];
|
||||
if (token) {
|
||||
setToken(token);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default service;
|
||||
90
src/utils/shop.ts
Normal file
90
src/utils/shop.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// 支付方式
|
||||
export function getPayType(index?: number): any {
|
||||
const payType = [
|
||||
{
|
||||
value: 0,
|
||||
label: '余额支付'
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: '微信支付'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '积分'
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '支付宝'
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '现金'
|
||||
},
|
||||
{
|
||||
value: 5,
|
||||
label: 'POS机'
|
||||
},
|
||||
{
|
||||
value: 6,
|
||||
label: '会员卡'
|
||||
}
|
||||
// {
|
||||
// value: 7,
|
||||
// label: 'VIP年卡',
|
||||
// },
|
||||
// {
|
||||
// value: 8,
|
||||
// label: 'VIP次卡',
|
||||
// },
|
||||
// {
|
||||
// value: 9,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'IC月卡',
|
||||
// },
|
||||
// {
|
||||
// value: 10,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'IC年卡',
|
||||
// },
|
||||
// {
|
||||
// value: 11,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'IC次卡',
|
||||
// },
|
||||
// {
|
||||
// value: 12,
|
||||
// icon: '',
|
||||
// label: '免费',
|
||||
// },
|
||||
// {
|
||||
// value: 13,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'VIP充值卡',
|
||||
// },
|
||||
// {
|
||||
// value: 14,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'IC充值卡',
|
||||
// },
|
||||
// {
|
||||
// value: 15,
|
||||
// icon: '',
|
||||
// label: '积分支付',
|
||||
// },
|
||||
// {
|
||||
// value: 16,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'VIP季卡',
|
||||
// },
|
||||
// {
|
||||
// value: 17,
|
||||
// icon: 'IdcardOutlined',
|
||||
// label: 'IC季卡',
|
||||
// },
|
||||
];
|
||||
if (index) {
|
||||
return payType[index].label || '';
|
||||
}
|
||||
return payType;
|
||||
}
|
||||
71
src/utils/token-util.ts
Normal file
71
src/utils/token-util.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* token 操作封装
|
||||
*/
|
||||
import { APP_SECRET, TOKEN_STORE_NAME } from '@/config/setting';
|
||||
import md5 from 'js-md5';
|
||||
|
||||
/**
|
||||
* 获取缓存的 token
|
||||
*/
|
||||
export function getToken(): string | null {
|
||||
const token = localStorage.getItem(TOKEN_STORE_NAME);
|
||||
if (!token) {
|
||||
return sessionStorage.getItem(TOKEN_STORE_NAME);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存 token
|
||||
* @param token token
|
||||
* @param remember 是否永久存储
|
||||
*/
|
||||
export function setToken(token?: string, remember?: boolean) {
|
||||
removeToken();
|
||||
if (token) {
|
||||
if (remember) {
|
||||
localStorage.setItem(TOKEN_STORE_NAME, token);
|
||||
} else {
|
||||
sessionStorage.setItem(TOKEN_STORE_NAME, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 token
|
||||
*/
|
||||
export function removeToken() {
|
||||
localStorage.removeItem(TOKEN_STORE_NAME);
|
||||
sessionStorage.removeItem(TOKEN_STORE_NAME);
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
// 封装签名
|
||||
export function getSign(form) {
|
||||
if (form == null) {
|
||||
return false;
|
||||
}
|
||||
let sign = '';
|
||||
form.timestamp = new Date().getTime();
|
||||
form.version = 'v3';
|
||||
const arr = objKeySort(form);
|
||||
Object.keys(arr).forEach((k) => {
|
||||
if (form[k] != null && form[k] != '') {
|
||||
sign = sign.concat(form[k]).concat('-');
|
||||
}
|
||||
});
|
||||
sign = sign.concat(APP_SECRET);
|
||||
return md5(sign);
|
||||
}
|
||||
// 参数按照字母顺序排序
|
||||
export const objKeySort = (obj) => {
|
||||
//排序的函数
|
||||
const newkey = Object.keys(obj).sort();
|
||||
//先用Object内置类的keys方法获取要排序对象的属性名,再利用Array原型上的sort方法对获取的属性名进行排序,newkey是一个数组
|
||||
const newObj = {}; //创建一个新的对象,用于存放排好序的键值对
|
||||
for (let i = 0; i < newkey.length; i++) {
|
||||
//遍历newkey数组
|
||||
newObj[newkey[i]] = obj[newkey[i]]; //向新创建的对象中按照排好的顺序依次增加键值对
|
||||
}
|
||||
return newObj; //返回排好序的新对象
|
||||
};
|
||||
104
src/utils/type-guards.ts
Normal file
104
src/utils/type-guards.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 类型保护工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检查值是否为有效的数字
|
||||
*/
|
||||
export function isValidNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && !isNaN(value) && isFinite(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为有效的字符串
|
||||
*/
|
||||
export function isValidString(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查对象是否有有效的 ID
|
||||
*/
|
||||
export function hasValidId(obj: unknown): obj is { id: number } {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
'id' in obj &&
|
||||
isValidNumber((obj as any).id)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取数字值,提供默认值
|
||||
*/
|
||||
export function safeNumber(value: unknown, defaultValue = 0): number {
|
||||
if (isValidNumber(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return isValidNumber(parsed) ? parsed : defaultValue;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取字符串值,提供默认值
|
||||
*/
|
||||
export function safeString(value: unknown, defaultValue = ''): string {
|
||||
return typeof value === 'string' ? value : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 API 响应是否有效
|
||||
*/
|
||||
export function isValidApiResponse<T>(
|
||||
response: unknown
|
||||
): response is { count: number; list?: T[] } {
|
||||
return (
|
||||
typeof response === 'object' &&
|
||||
response !== null &&
|
||||
'count' in response &&
|
||||
isValidNumber((response as any).count)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查统计数据是否有效
|
||||
*/
|
||||
export function isValidStatistics(data: unknown): data is {
|
||||
id?: number;
|
||||
userCount?: number;
|
||||
orderCount?: number;
|
||||
totalSales?: number;
|
||||
} {
|
||||
return typeof data === 'object' && data !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全的对象合并,过滤掉 undefined 值
|
||||
*/
|
||||
export function safeMerge<T extends Record<string, any>>(
|
||||
target: T,
|
||||
source: Partial<T>
|
||||
): T {
|
||||
const result = { ...target };
|
||||
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value !== undefined) {
|
||||
result[key as keyof T] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带有默认值的对象
|
||||
*/
|
||||
export function withDefaults<T extends Record<string, any>>(
|
||||
data: Partial<T>,
|
||||
defaults: T
|
||||
): T {
|
||||
return safeMerge(defaults, data);
|
||||
}
|
||||
76
src/utils/use-echarts.ts
Normal file
76
src/utils/use-echarts.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* echarts 自动切换主题、重置尺寸封装
|
||||
*/
|
||||
import type { Ref } from 'vue';
|
||||
import {
|
||||
ref,
|
||||
reactive,
|
||||
unref,
|
||||
provide,
|
||||
watch,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
nextTick
|
||||
} from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { THEME_KEY } from 'vue-echarts';
|
||||
import type VChart from 'vue-echarts';
|
||||
import { ChartTheme, ChartThemeDark } from 'ele-admin-pro/es';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { onSizeChange } from './on-size-change';
|
||||
|
||||
export default function (chartRefs: Ref<InstanceType<typeof VChart> | null>[]) {
|
||||
// 当前框架是否是暗黑主题
|
||||
const themeStore = useThemeStore();
|
||||
const { darkMode } = storeToRefs(themeStore);
|
||||
// 是否为 deactivated 状态
|
||||
const deactivated = ref<boolean>(false);
|
||||
// 当前图表是否是暗黑主题
|
||||
const isDark = ref<boolean>(unref(darkMode));
|
||||
// 当前图表主题
|
||||
const chartsTheme = reactive({
|
||||
...(unref(isDark) ? ChartThemeDark : ChartTheme)
|
||||
});
|
||||
|
||||
// 设置图表主题
|
||||
provide(THEME_KEY, chartsTheme);
|
||||
|
||||
/* 重置图表尺寸 */
|
||||
const resizeCharts = () => {
|
||||
chartRefs.forEach((chartRef) => {
|
||||
unref(chartRef)?.resize();
|
||||
});
|
||||
};
|
||||
|
||||
/* 屏幕尺寸变化监听 */
|
||||
onSizeChange(() => {
|
||||
resizeCharts();
|
||||
});
|
||||
|
||||
/* 更改图表主题 */
|
||||
const changeTheme = (dark: boolean) => {
|
||||
isDark.value = dark;
|
||||
Object.assign(chartsTheme, dark ? ChartThemeDark : ChartTheme);
|
||||
};
|
||||
|
||||
onActivated(() => {
|
||||
deactivated.value = false;
|
||||
nextTick(() => {
|
||||
if (unref(isDark) !== unref(darkMode)) {
|
||||
changeTheme(unref(darkMode));
|
||||
} else {
|
||||
resizeCharts();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
deactivated.value = true;
|
||||
});
|
||||
|
||||
watch(darkMode, (dark) => {
|
||||
if (!unref(deactivated)) {
|
||||
changeTheme(dark);
|
||||
}
|
||||
});
|
||||
}
|
||||
29
src/utils/use-form-data.ts
Normal file
29
src/utils/use-form-data.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
/**
|
||||
* 表单数据 hook
|
||||
* @param initValue 默认值
|
||||
*/
|
||||
export default function <T extends object>(initValue?: T) {
|
||||
const form = reactive<T>({ ...initValue } as T);
|
||||
|
||||
const resetFields = () => {
|
||||
Object.keys(form).forEach((key) => {
|
||||
form[key] = initValue ? initValue[key] : void 0;
|
||||
});
|
||||
};
|
||||
|
||||
const assignFields = (data: object) => {
|
||||
Object.keys(form).forEach((key) => {
|
||||
form[key] = data[key];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
form,
|
||||
// 重置为初始值
|
||||
resetFields,
|
||||
// 赋值不改变字段
|
||||
assignFields
|
||||
};
|
||||
}
|
||||
27
src/utils/use-search.ts
Normal file
27
src/utils/use-search.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { reactive } from 'vue';
|
||||
|
||||
/**
|
||||
* 搜索表单 hook
|
||||
* @param initValue 默认值
|
||||
*/
|
||||
export default function <T extends object>(initValue?: T) {
|
||||
const where = reactive<T>({ ...initValue } as T);
|
||||
|
||||
const resetFields = () => {
|
||||
Object.keys(where).forEach((key) => {
|
||||
where[key] = initValue ? initValue[key] : void 0;
|
||||
});
|
||||
};
|
||||
|
||||
const assignFields = (data: object) => {
|
||||
Object.keys(where).forEach((key) => {
|
||||
where[key] = data[key];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
where,
|
||||
resetFields,
|
||||
assignFields
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user