1742 lines
50 KiB
Markdown
1742 lines
50 KiB
Markdown
# DataTools Pro 前端模块化设计文档
|
||
|
||
## 1. 前端架构概述
|
||
|
||
### 1.1 设计理念
|
||
DataTools Pro前端采用现代化的模块化架构,基于ES6模块系统构建,追求高内聚、低耦合的设计原则,提供可维护、可扩展的前端解决方案。
|
||
|
||
### 1.2 技术栈
|
||
```
|
||
前端技术栈:
|
||
├── 核心技术
|
||
│ ├── 原生JavaScript (ES6+)
|
||
│ ├── ES6 Modules (import/export)
|
||
│ └── 现代浏览器API (Fetch, Promise, async/await)
|
||
├── UI框架
|
||
│ ├── Bootstrap 5.1.3 (响应式UI框架)
|
||
│ ├── Font Awesome 6.0.0 (图标库)
|
||
│ └── 原生CSS3 (自定义样式和动画)
|
||
└── 开发模式
|
||
├── 模块化开发
|
||
├── 组件化设计
|
||
└── 事件驱动架构
|
||
```
|
||
|
||
### 1.3 架构优势
|
||
- **无构建依赖**: 直接在浏览器中运行,无需复杂的构建工具
|
||
- **模块化设计**: 清晰的职责分离,便于维护和测试
|
||
- **轻量级**: 没有框架包袱,性能优异
|
||
- **现代化**: 使用最新的JavaScript特性和API
|
||
- **兼容性**: 支持现代浏览器(Chrome 61+, Firefox 60+, Safari 10.1+)
|
||
|
||
## 2. 模块架构图
|
||
|
||
```
|
||
DataTools Pro 前端模块架构
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 应用层 (App Layer) │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ app-main.js │ │
|
||
│ │ 主应用入口和协调器 │ │
|
||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
|
||
│ │ │ 应用初始化 │ │ 生命周期 │ │ 全局事件管理 │ │ │
|
||
│ │ │ 管理 │ │ 管理 │ │ │ │ │
|
||
│ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ 核心模块层 (Core Modules) │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||
│ │ config.js │ │ utils.js │ │ api.js │ │
|
||
│ │ 配置管理 │ │ 工具函数库 │ │ HTTP客户端 │ │
|
||
│ │ ·APP_INFO │ │ ·escapeHtml │ │ ·CassandraAPI │ │
|
||
│ │ ·API端点 │ │ ·formatDate │ │ ·RedisAPI │ │
|
||
│ │ ·UI配置 │ │ ·debounce │ │ ·统一错误处理 │ │
|
||
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
|
||
│ ┌─────────────┐ ┌─────────────────────────────────────────┐ │
|
||
│ │ ui.js │ │ navigation.js │ │
|
||
│ │ UI组件库 │ │ 导航管理模块 │ │
|
||
│ │ ·AlertMgr │ │ ·面包屑导航 ·键盘快捷键 │ │
|
||
│ │ ·ModalMgr │ │ ·活跃状态管理 ·程序化导航 │ │
|
||
│ │ ·Pagination │ │ ·页面标题管理 │ │
|
||
│ └─────────────┘ └─────────────────────────────────────────┘ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ 页面层 (Page Layer) │
|
||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
|
||
│ │ index.html │ │db_compare. │ │ redis_compare.html │ │
|
||
│ │ 首页模块 │ │html │ │ Redis比对模块 │ │
|
||
│ │ ·工具展示 │ │Cassandra │ │ ·集群配置管理 │ │
|
||
│ │ ·统计信息 │ │比对模块 │ │ ·随机采样查询 │ │
|
||
│ │ ·导航入口 │ │·查询配置 │ │ ·指定Key查询 │ │
|
||
│ └─────────────┘ │·分表查询 │ │ ·数据类型比对 │ │
|
||
│ │·多主键查询 │ └─────────────────────────┘ │
|
||
│ └─────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 3. 核心模块详解
|
||
|
||
### 3.1 主应用模块 (app-main.js)
|
||
|
||
#### 3.1.1 模块职责
|
||
- **应用初始化**: 统一管理各模块启动和配置
|
||
- **生命周期管理**: 处理应用启动、运行、销毁周期
|
||
- **全局事件管理**: 统一处理全局事件和错误
|
||
- **模块协调**: 协调各个功能模块之间的交互
|
||
|
||
#### 3.1.2 核心类设计
|
||
```javascript
|
||
/**
|
||
* 主应用程序类
|
||
*/
|
||
class DataToolsApp {
|
||
constructor() {
|
||
this.config = config; // 配置管理
|
||
this.utils = utils; // 工具函数
|
||
this.api = apiManager; // API管理
|
||
this.navigation = navigationManager; // 导航管理
|
||
this.ui = { // UI组件集合
|
||
alert: alertManager,
|
||
modal: modalManager,
|
||
loading: loadingIndicator,
|
||
confirm: confirmDialog,
|
||
prompt: promptDialog
|
||
};
|
||
this.isInitialized = false;
|
||
this.currentTool = null;
|
||
}
|
||
|
||
// 核心方法
|
||
async init() // 应用初始化
|
||
checkDependencies() // 依赖检查
|
||
initUI() // UI初始化
|
||
bindGlobalEvents() // 全局事件绑定
|
||
detectCurrentTool() // 当前工具检测
|
||
destroy() // 应用销毁
|
||
}
|
||
```
|
||
|
||
#### 3.1.3 初始化流程
|
||
```javascript
|
||
// 应用初始化流程
|
||
async function initApplication() {
|
||
try {
|
||
console.log('DataTools Pro 正在初始化...');
|
||
|
||
// 1. 检查浏览器兼容性和依赖
|
||
checkDependencies();
|
||
|
||
// 2. 初始化UI组件
|
||
initUI();
|
||
|
||
// 3. 启用导航和快捷键
|
||
navigation.enableKeyboardShortcuts();
|
||
|
||
// 4. 绑定全局事件
|
||
bindGlobalEvents();
|
||
|
||
// 5. 检测当前工具类型
|
||
detectCurrentTool();
|
||
|
||
console.log('应用程序初始化完成');
|
||
|
||
// 6. 显示欢迎消息
|
||
showWelcomeMessage();
|
||
|
||
} catch (error) {
|
||
console.error('应用程序初始化失败:', error);
|
||
ui.alert.error('应用程序初始化失败,请刷新页面重试');
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.2 配置管理模块 (config.js)
|
||
|
||
#### 3.2.1 模块结构
|
||
```javascript
|
||
// 应用基础信息
|
||
export const APP_INFO = {
|
||
name: 'DataTools Pro',
|
||
version: '2.0.0',
|
||
description: '企业级数据处理与比对工具平台'
|
||
};
|
||
|
||
// API端点配置
|
||
export const API_ENDPOINTS = {
|
||
// Cassandra相关API
|
||
CASSANDRA: {
|
||
DEFAULT_CONFIG: '/api/default-config',
|
||
QUERY: '/api/query',
|
||
SHARDING_QUERY: '/api/sharding-query'
|
||
},
|
||
|
||
// Redis相关API
|
||
REDIS: {
|
||
COMPARE: '/api/redis/compare',
|
||
CONFIG: '/api/redis/config'
|
||
},
|
||
|
||
// 配置管理API
|
||
CONFIG_GROUPS: {
|
||
LIST: '/api/config-groups',
|
||
CREATE: '/api/config-groups',
|
||
GET: (id) => `/api/config-groups/${id}`,
|
||
DELETE: (id) => `/api/config-groups/${id}`
|
||
},
|
||
|
||
// 历史记录API
|
||
QUERY_HISTORY: {
|
||
LIST: '/api/query-history',
|
||
CREATE: '/api/query-history',
|
||
GET: (id) => `/api/query-history/${id}`,
|
||
RESULTS: (id) => `/api/query-history/${id}/results`,
|
||
DELETE: (id) => `/api/query-history/${id}`
|
||
},
|
||
|
||
// 日志管理API
|
||
QUERY_LOGS: {
|
||
LIST: '/api/query-logs',
|
||
HISTORY: (id) => `/api/query-logs/history/${id}`,
|
||
CLEAR: '/api/query-logs'
|
||
}
|
||
};
|
||
|
||
// UI配置
|
||
export const UI_CONFIG = {
|
||
PAGINATION: {
|
||
DEFAULT_PAGE_SIZE: 10,
|
||
MAX_PAGE_SIZE: 100,
|
||
SHOW_SIZE_CHANGER: true
|
||
},
|
||
|
||
ALERT: {
|
||
AUTO_HIDE_DELAY: 3000,
|
||
MAX_ALERTS: 5
|
||
},
|
||
|
||
LOADING: {
|
||
MIN_DISPLAY_TIME: 500,
|
||
TIMEOUT: 30000
|
||
},
|
||
|
||
ANIMATION: {
|
||
DURATION: 300,
|
||
EASING: 'ease-in-out'
|
||
}
|
||
};
|
||
|
||
// 工具配置
|
||
export const TOOLS_CONFIG = {
|
||
CASSANDRA: {
|
||
name: 'Cassandra数据比对工具',
|
||
url: '/cassandra-compare',
|
||
icon: 'fas fa-database',
|
||
description: '企业级Cassandra数据库比对分析'
|
||
},
|
||
|
||
REDIS: {
|
||
name: 'Redis集群比对工具',
|
||
url: '/redis-compare',
|
||
icon: 'fab fa-redis',
|
||
description: '专业的Redis集群数据比对分析'
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 3.2.2 配置管理类
|
||
```javascript
|
||
class ConfigManager {
|
||
constructor() {
|
||
this.cache = new Map();
|
||
this.listeners = new Map();
|
||
}
|
||
|
||
// 获取配置值
|
||
get(key, defaultValue = null) {
|
||
const keys = key.split('.');
|
||
let value = this;
|
||
|
||
for (const k of keys) {
|
||
value = value[k];
|
||
if (value === undefined) {
|
||
return defaultValue;
|
||
}
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
// 设置配置值
|
||
set(key, value) {
|
||
this.cache.set(key, value);
|
||
this.notifyListeners(key, value);
|
||
}
|
||
|
||
// 监听配置变更
|
||
onChange(key, callback) {
|
||
if (!this.listeners.has(key)) {
|
||
this.listeners.set(key, new Set());
|
||
}
|
||
this.listeners.get(key).add(callback);
|
||
}
|
||
|
||
// 通知监听器
|
||
notifyListeners(key, value) {
|
||
if (this.listeners.has(key)) {
|
||
this.listeners.get(key).forEach(callback => {
|
||
callback(value, key);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.3 工具函数模块 (utils.js)
|
||
|
||
#### 3.3.1 核心工具函数
|
||
```javascript
|
||
/**
|
||
* HTML转义,防止XSS攻击
|
||
*/
|
||
export function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* 日期格式化
|
||
*/
|
||
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
|
||
const d = new Date(date);
|
||
const formatMap = {
|
||
'YYYY': d.getFullYear(),
|
||
'MM': String(d.getMonth() + 1).padStart(2, '0'),
|
||
'DD': String(d.getDate()).padStart(2, '0'),
|
||
'HH': String(d.getHours()).padStart(2, '0'),
|
||
'mm': String(d.getMinutes()).padStart(2, '0'),
|
||
'ss': String(d.getSeconds()).padStart(2, '0')
|
||
};
|
||
|
||
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => formatMap[match]);
|
||
}
|
||
|
||
/**
|
||
* 防抖函数
|
||
*/
|
||
export function debounce(func, wait, immediate = false) {
|
||
let timeout;
|
||
return function executedFunction(...args) {
|
||
const later = () => {
|
||
timeout = null;
|
||
if (!immediate) func.apply(this, args);
|
||
};
|
||
const callNow = immediate && !timeout;
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
if (callNow) func.apply(this, args);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 节流函数
|
||
*/
|
||
export function throttle(func, limit) {
|
||
let inThrottle;
|
||
return function(...args) {
|
||
if (!inThrottle) {
|
||
func.apply(this, args);
|
||
inThrottle = true;
|
||
setTimeout(() => inThrottle = false, limit);
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 深度克隆对象
|
||
*/
|
||
export function deepClone(obj) {
|
||
if (obj === null || typeof obj !== "object") {
|
||
return obj;
|
||
}
|
||
|
||
if (obj instanceof Date) {
|
||
return new Date(obj.getTime());
|
||
}
|
||
|
||
if (obj instanceof Array) {
|
||
return obj.map(item => deepClone(item));
|
||
}
|
||
|
||
if (typeof obj === "object") {
|
||
const clonedObj = {};
|
||
for (let key in obj) {
|
||
if (obj.hasOwnProperty(key)) {
|
||
clonedObj[key] = deepClone(obj[key]);
|
||
}
|
||
}
|
||
return clonedObj;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 剪贴板操作
|
||
*/
|
||
export const clipboard = {
|
||
async copy(text) {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
return true;
|
||
} catch (err) {
|
||
// 降级方案
|
||
const textArea = document.createElement('textarea');
|
||
textArea.value = text;
|
||
document.body.appendChild(textArea);
|
||
textArea.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(textArea);
|
||
return true;
|
||
}
|
||
},
|
||
|
||
async paste() {
|
||
try {
|
||
return await navigator.clipboard.readText();
|
||
} catch (err) {
|
||
console.warn('无法访问剪贴板');
|
||
return '';
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 文件操作工具
|
||
*/
|
||
export const fileUtils = {
|
||
// 下载文件
|
||
download(data, filename, type = 'text/plain') {
|
||
const blob = new Blob([data], { type });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = filename;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
URL.revokeObjectURL(url);
|
||
},
|
||
|
||
// 读取文件
|
||
async readFile(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
reader.onload = e => resolve(e.target.result);
|
||
reader.onerror = reject;
|
||
reader.readAsText(file);
|
||
});
|
||
}
|
||
};
|
||
```
|
||
|
||
#### 3.3.2 数据处理工具
|
||
```javascript
|
||
/**
|
||
* 数据验证工具
|
||
*/
|
||
export const validator = {
|
||
// 非空验证
|
||
required(value, message = '此字段不能为空') {
|
||
if (!value || value.toString().trim() === '') {
|
||
throw new Error(message);
|
||
}
|
||
return true;
|
||
},
|
||
|
||
// 邮箱验证
|
||
email(value, message = '请输入有效的邮箱地址') {
|
||
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!pattern.test(value)) {
|
||
throw new Error(message);
|
||
}
|
||
return true;
|
||
},
|
||
|
||
// 数字验证
|
||
number(value, min = null, max = null, message = '请输入有效的数字') {
|
||
const num = Number(value);
|
||
if (isNaN(num)) {
|
||
throw new Error(message);
|
||
}
|
||
if (min !== null && num < min) {
|
||
throw new Error(`数值不能小于${min}`);
|
||
}
|
||
if (max !== null && num > max) {
|
||
throw new Error(`数值不能大于${max}`);
|
||
}
|
||
return true;
|
||
},
|
||
|
||
// JSON验证
|
||
json(value, message = '请输入有效的JSON格式') {
|
||
try {
|
||
JSON.parse(value);
|
||
return true;
|
||
} catch (e) {
|
||
throw new Error(message);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 字符串处理工具
|
||
*/
|
||
export const stringUtils = {
|
||
// 首字母大写
|
||
capitalize(str) {
|
||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||
},
|
||
|
||
// 驼峰命名转换
|
||
camelCase(str) {
|
||
return str.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
||
},
|
||
|
||
// 截断字符串
|
||
truncate(str, maxLength, suffix = '...') {
|
||
if (str.length <= maxLength) return str;
|
||
return str.substring(0, maxLength - suffix.length) + suffix;
|
||
},
|
||
|
||
// 格式化文件大小
|
||
formatBytes(bytes, decimals = 2) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||
}
|
||
};
|
||
```
|
||
|
||
### 3.4 API管理模块 (api.js)
|
||
|
||
#### 3.4.1 HTTP客户端封装
|
||
```javascript
|
||
/**
|
||
* HTTP客户端类
|
||
*/
|
||
class HttpClient {
|
||
constructor(baseURL = '', options = {}) {
|
||
this.baseURL = baseURL;
|
||
this.defaultOptions = {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Accept': 'application/json'
|
||
},
|
||
credentials: 'same-origin',
|
||
...options
|
||
};
|
||
|
||
this.interceptors = {
|
||
request: [],
|
||
response: []
|
||
};
|
||
}
|
||
|
||
// 添加请求拦截器
|
||
addRequestInterceptor(interceptor) {
|
||
this.interceptors.request.push(interceptor);
|
||
}
|
||
|
||
// 添加响应拦截器
|
||
addResponseInterceptor(interceptor) {
|
||
this.interceptors.response.push(interceptor);
|
||
}
|
||
|
||
// 执行请求
|
||
async request(url, options = {}) {
|
||
const fullUrl = this.baseURL + url;
|
||
let config = { ...this.defaultOptions, ...options };
|
||
|
||
// 执行请求拦截器
|
||
for (const interceptor of this.interceptors.request) {
|
||
config = await interceptor(config);
|
||
}
|
||
|
||
try {
|
||
let response = await fetch(fullUrl, config);
|
||
|
||
// 执行响应拦截器
|
||
for (const interceptor of this.interceptors.response) {
|
||
response = await interceptor(response);
|
||
}
|
||
|
||
// 处理HTTP错误状态
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||
}
|
||
|
||
// 解析JSON响应
|
||
const data = await response.json();
|
||
return data;
|
||
|
||
} catch (error) {
|
||
console.error('HTTP请求失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// HTTP方法快捷方式
|
||
get(url, params = {}) {
|
||
const queryString = new URLSearchParams(params).toString();
|
||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
||
return this.request(fullUrl, { method: 'GET' });
|
||
}
|
||
|
||
post(url, data = {}) {
|
||
return this.request(url, {
|
||
method: 'POST',
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
put(url, data = {}) {
|
||
return this.request(url, {
|
||
method: 'PUT',
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
delete(url) {
|
||
return this.request(url, { method: 'DELETE' });
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.4.2 API管理器
|
||
```javascript
|
||
/**
|
||
* API管理器类
|
||
*/
|
||
class APIManager {
|
||
constructor() {
|
||
this.httpClient = new HttpClient();
|
||
this.setupInterceptors();
|
||
}
|
||
|
||
// 设置拦截器
|
||
setupInterceptors() {
|
||
// 请求拦截器 - 添加时间戳和请求ID
|
||
this.httpClient.addRequestInterceptor(async (config) => {
|
||
config.headers['X-Request-ID'] = this.generateRequestId();
|
||
config.headers['X-Timestamp'] = Date.now();
|
||
return config;
|
||
});
|
||
|
||
// 响应拦截器 - 统一错误处理
|
||
this.httpClient.addResponseInterceptor(async (response) => {
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new APIError(data.error?.message || '请求失败', data.error?.code);
|
||
}
|
||
|
||
return data;
|
||
});
|
||
}
|
||
|
||
// 生成请求ID
|
||
generateRequestId() {
|
||
return 'req_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
}
|
||
|
||
// Cassandra API
|
||
get cassandra() {
|
||
return {
|
||
getDefaultConfig: () =>
|
||
this.httpClient.get(API_ENDPOINTS.CASSANDRA.DEFAULT_CONFIG),
|
||
|
||
executeQuery: (config) =>
|
||
this.httpClient.post(API_ENDPOINTS.CASSANDRA.QUERY, config),
|
||
|
||
executeShardingQuery: (config) =>
|
||
this.httpClient.post(API_ENDPOINTS.CASSANDRA.SHARDING_QUERY, config)
|
||
};
|
||
}
|
||
|
||
// Redis API
|
||
get redis() {
|
||
return {
|
||
compareData: (config) =>
|
||
this.httpClient.post(API_ENDPOINTS.REDIS.COMPARE, config)
|
||
};
|
||
}
|
||
|
||
// 配置组API
|
||
get configGroups() {
|
||
return {
|
||
list: () =>
|
||
this.httpClient.get(API_ENDPOINTS.CONFIG_GROUPS.LIST),
|
||
|
||
create: (config) =>
|
||
this.httpClient.post(API_ENDPOINTS.CONFIG_GROUPS.CREATE, config),
|
||
|
||
get: (id) =>
|
||
this.httpClient.get(API_ENDPOINTS.CONFIG_GROUPS.GET(id)),
|
||
|
||
delete: (id) =>
|
||
this.httpClient.delete(API_ENDPOINTS.CONFIG_GROUPS.DELETE(id))
|
||
};
|
||
}
|
||
|
||
// 查询历史API
|
||
get queryHistory() {
|
||
return {
|
||
list: (params = {}) =>
|
||
this.httpClient.get(API_ENDPOINTS.QUERY_HISTORY.LIST, params),
|
||
|
||
create: (history) =>
|
||
this.httpClient.post(API_ENDPOINTS.QUERY_HISTORY.CREATE, history),
|
||
|
||
get: (id) =>
|
||
this.httpClient.get(API_ENDPOINTS.QUERY_HISTORY.GET(id)),
|
||
|
||
getResults: (id) =>
|
||
this.httpClient.get(API_ENDPOINTS.QUERY_HISTORY.RESULTS(id)),
|
||
|
||
delete: (id) =>
|
||
this.httpClient.delete(API_ENDPOINTS.QUERY_HISTORY.DELETE(id))
|
||
};
|
||
}
|
||
|
||
// 日志API
|
||
get queryLogs() {
|
||
return {
|
||
list: (params = {}) =>
|
||
this.httpClient.get(API_ENDPOINTS.QUERY_LOGS.LIST, params),
|
||
|
||
getHistory: (id) =>
|
||
this.httpClient.get(API_ENDPOINTS.QUERY_LOGS.HISTORY(id)),
|
||
|
||
clear: () =>
|
||
this.httpClient.delete(API_ENDPOINTS.QUERY_LOGS.CLEAR)
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* API错误类
|
||
*/
|
||
class APIError extends Error {
|
||
constructor(message, code = 'UNKNOWN_ERROR') {
|
||
super(message);
|
||
this.name = 'APIError';
|
||
this.code = code;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.5 UI组件模块 (ui.js)
|
||
|
||
#### 3.5.1 提示管理器
|
||
```javascript
|
||
/**
|
||
* 提示信息管理器
|
||
*/
|
||
class AlertManager {
|
||
constructor() {
|
||
this.alerts = [];
|
||
this.container = null;
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
// 创建提示容器
|
||
this.container = document.createElement('div');
|
||
this.container.className = 'alert-container';
|
||
this.container.style.cssText = `
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
max-width: 400px;
|
||
`;
|
||
document.body.appendChild(this.container);
|
||
}
|
||
|
||
show(type, message, autoHide = UI_CONFIG.ALERT.AUTO_HIDE_DELAY) {
|
||
const alertId = 'alert_' + Date.now();
|
||
const alertElement = this.createAlertElement(alertId, type, message);
|
||
|
||
this.container.appendChild(alertElement);
|
||
this.alerts.push({ id: alertId, element: alertElement });
|
||
|
||
// 限制提示数量
|
||
if (this.alerts.length > UI_CONFIG.ALERT.MAX_ALERTS) {
|
||
this.removeOldest();
|
||
}
|
||
|
||
// 自动隐藏
|
||
if (autoHide > 0) {
|
||
setTimeout(() => this.hide(alertId), autoHide);
|
||
}
|
||
|
||
return alertId;
|
||
}
|
||
|
||
createAlertElement(id, type, message) {
|
||
const alert = document.createElement('div');
|
||
alert.id = id;
|
||
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
||
alert.innerHTML = `
|
||
<i class="fas fa-${this.getIcon(type)} me-2"></i>
|
||
<span>${escapeHtml(message)}</span>
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||
`;
|
||
|
||
// 绑定关闭事件
|
||
alert.querySelector('.btn-close').addEventListener('click', () => {
|
||
this.hide(id);
|
||
});
|
||
|
||
return alert;
|
||
}
|
||
|
||
getIcon(type) {
|
||
const icons = {
|
||
success: 'check-circle',
|
||
error: 'exclamation-circle',
|
||
warning: 'exclamation-triangle',
|
||
info: 'info-circle'
|
||
};
|
||
return icons[type] || 'info-circle';
|
||
}
|
||
|
||
hide(alertId) {
|
||
const alertIndex = this.alerts.findIndex(alert => alert.id === alertId);
|
||
if (alertIndex !== -1) {
|
||
const alert = this.alerts[alertIndex];
|
||
alert.element.remove();
|
||
this.alerts.splice(alertIndex, 1);
|
||
}
|
||
}
|
||
|
||
// 便捷方法
|
||
success(message, autoHide) { return this.show('success', message, autoHide); }
|
||
error(message, autoHide) { return this.show('error', message, autoHide); }
|
||
warning(message, autoHide) { return this.show('warning', message, autoHide); }
|
||
info(message, autoHide) { return this.show('info', message, autoHide); }
|
||
}
|
||
```
|
||
|
||
#### 3.5.2 分页组件
|
||
```javascript
|
||
/**
|
||
* 分页组件
|
||
*/
|
||
class PaginationComponent {
|
||
constructor(container, options = {}) {
|
||
this.container = typeof container === 'string' ?
|
||
document.querySelector(container) : container;
|
||
|
||
this.options = {
|
||
pageSize: UI_CONFIG.PAGINATION.DEFAULT_PAGE_SIZE,
|
||
showSizeChanger: UI_CONFIG.PAGINATION.SHOW_SIZE_CHANGER,
|
||
showTotal: true,
|
||
showQuickJumper: true,
|
||
...options
|
||
};
|
||
|
||
this.currentPage = 1;
|
||
this.totalItems = 0;
|
||
this.totalPages = 0;
|
||
|
||
this.onPageChange = options.onPageChange || (() => {});
|
||
this.onSizeChange = options.onSizeChange || (() => {});
|
||
}
|
||
|
||
// 更新分页数据
|
||
update(totalItems, currentPage = 1) {
|
||
this.totalItems = totalItems;
|
||
this.currentPage = currentPage;
|
||
this.totalPages = Math.ceil(totalItems / this.options.pageSize);
|
||
this.render();
|
||
}
|
||
|
||
// 渲染分页组件
|
||
render() {
|
||
if (!this.container) return;
|
||
|
||
const pagination = this.createPaginationElement();
|
||
this.container.innerHTML = '';
|
||
this.container.appendChild(pagination);
|
||
}
|
||
|
||
createPaginationElement() {
|
||
const nav = document.createElement('nav');
|
||
nav.innerHTML = `
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div class="pagination-info">
|
||
${this.renderTotalInfo()}
|
||
</div>
|
||
<ul class="pagination mb-0">
|
||
${this.renderPaginationItems()}
|
||
</ul>
|
||
<div class="pagination-controls">
|
||
${this.renderSizeChanger()}
|
||
${this.renderQuickJumper()}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
this.bindEvents(nav);
|
||
return nav;
|
||
}
|
||
|
||
renderTotalInfo() {
|
||
if (!this.options.showTotal) return '';
|
||
|
||
const start = (this.currentPage - 1) * this.options.pageSize + 1;
|
||
const end = Math.min(this.currentPage * this.options.pageSize, this.totalItems);
|
||
|
||
return `
|
||
<small class="text-muted">
|
||
显示 ${start}-${end} 项,共 ${this.totalItems} 项
|
||
</small>
|
||
`;
|
||
}
|
||
|
||
renderPaginationItems() {
|
||
let items = [];
|
||
|
||
// 上一页
|
||
items.push(`
|
||
<li class="page-item ${this.currentPage === 1 ? 'disabled' : ''}">
|
||
<a class="page-link" href="#" data-page="${this.currentPage - 1}">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</a>
|
||
</li>
|
||
`);
|
||
|
||
// 页码
|
||
const pageNumbers = this.generatePageNumbers();
|
||
pageNumbers.forEach(page => {
|
||
if (page === '...') {
|
||
items.push('<li class="page-item disabled"><span class="page-link">...</span></li>');
|
||
} else {
|
||
items.push(`
|
||
<li class="page-item ${page === this.currentPage ? 'active' : ''}">
|
||
<a class="page-link" href="#" data-page="${page}">${page}</a>
|
||
</li>
|
||
`);
|
||
}
|
||
});
|
||
|
||
// 下一页
|
||
items.push(`
|
||
<li class="page-item ${this.currentPage === this.totalPages ? 'disabled' : ''}">
|
||
<a class="page-link" href="#" data-page="${this.currentPage + 1}">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</a>
|
||
</li>
|
||
`);
|
||
|
||
return items.join('');
|
||
}
|
||
|
||
generatePageNumbers() {
|
||
const total = this.totalPages;
|
||
const current = this.currentPage;
|
||
const pages = [];
|
||
|
||
if (total <= 7) {
|
||
for (let i = 1; i <= total; i++) {
|
||
pages.push(i);
|
||
}
|
||
} else {
|
||
if (current <= 4) {
|
||
pages.push(1, 2, 3, 4, 5, '...', total);
|
||
} else if (current >= total - 3) {
|
||
pages.push(1, '...', total - 4, total - 3, total - 2, total - 1, total);
|
||
} else {
|
||
pages.push(1, '...', current - 1, current, current + 1, '...', total);
|
||
}
|
||
}
|
||
|
||
return pages;
|
||
}
|
||
|
||
bindEvents(nav) {
|
||
// 分页点击事件
|
||
nav.addEventListener('click', (e) => {
|
||
if (e.target.matches('.page-link[data-page]')) {
|
||
e.preventDefault();
|
||
const page = parseInt(e.target.dataset.page);
|
||
if (page > 0 && page <= this.totalPages) {
|
||
this.goToPage(page);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 页面大小变更事件
|
||
const sizeSelect = nav.querySelector('.page-size-select');
|
||
if (sizeSelect) {
|
||
sizeSelect.addEventListener('change', (e) => {
|
||
this.changePageSize(parseInt(e.target.value));
|
||
});
|
||
}
|
||
|
||
// 快速跳转事件
|
||
const jumpInput = nav.querySelector('.page-jump-input');
|
||
if (jumpInput) {
|
||
jumpInput.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
const page = parseInt(e.target.value);
|
||
if (page > 0 && page <= this.totalPages) {
|
||
this.goToPage(page);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
goToPage(page) {
|
||
if (page !== this.currentPage) {
|
||
this.currentPage = page;
|
||
this.render();
|
||
this.onPageChange(page, this.options.pageSize);
|
||
}
|
||
}
|
||
|
||
changePageSize(pageSize) {
|
||
this.options.pageSize = pageSize;
|
||
this.currentPage = 1;
|
||
this.totalPages = Math.ceil(this.totalItems / pageSize);
|
||
this.render();
|
||
this.onSizeChange(pageSize, this.currentPage);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.6 导航管理模块 (navigation.js)
|
||
|
||
#### 3.6.1 导航管理器
|
||
```javascript
|
||
/**
|
||
* 导航管理器类
|
||
*/
|
||
class NavigationManager {
|
||
constructor() {
|
||
this.currentPath = window.location.pathname;
|
||
this.breadcrumbContainer = null;
|
||
this.shortcuts = new Map();
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.updateActiveNavigation();
|
||
this.createBreadcrumb();
|
||
this.bindNavigationEvents();
|
||
}
|
||
|
||
// 更新活跃导航状态
|
||
updateActiveNavigation() {
|
||
document.querySelectorAll('.nav-link').forEach(link => {
|
||
link.classList.remove('active');
|
||
});
|
||
|
||
// 根据当前路径设置活跃状态
|
||
const currentNavLink = this.findActiveNavLink();
|
||
if (currentNavLink) {
|
||
currentNavLink.classList.add('active');
|
||
}
|
||
}
|
||
|
||
findActiveNavLink() {
|
||
// 直接匹配
|
||
let link = document.querySelector(`a[href="${this.currentPath}"]`);
|
||
if (link && link.classList.contains('nav-link')) {
|
||
return link;
|
||
}
|
||
|
||
// 路由别名匹配
|
||
if (this.currentPath.includes('db-compare') ||
|
||
this.currentPath.includes('cassandra-compare')) {
|
||
return document.querySelector('a[href="/cassandra-compare"]');
|
||
}
|
||
|
||
if (this.currentPath.includes('redis-compare')) {
|
||
return document.querySelector('a[href="/redis-compare"]');
|
||
}
|
||
|
||
if (this.currentPath === '/') {
|
||
return document.querySelector('a[href="/"]');
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
// 创建面包屑导航
|
||
createBreadcrumb() {
|
||
const breadcrumbItems = this.getBreadcrumbItems();
|
||
if (breadcrumbItems.length <= 1) return;
|
||
|
||
this.breadcrumbContainer = this.findOrCreateBreadcrumbContainer();
|
||
this.renderBreadcrumb(breadcrumbItems);
|
||
}
|
||
|
||
getBreadcrumbItems() {
|
||
const items = [
|
||
{ title: '首页', url: '/', icon: 'fas fa-home' }
|
||
];
|
||
|
||
if (this.currentPath.includes('cassandra-compare') ||
|
||
this.currentPath.includes('db-compare')) {
|
||
items.push({
|
||
title: 'Cassandra数据比对',
|
||
url: '/cassandra-compare',
|
||
icon: 'fas fa-database'
|
||
});
|
||
} else if (this.currentPath.includes('redis-compare')) {
|
||
items.push({
|
||
title: 'Redis集群比对',
|
||
url: '/redis-compare',
|
||
icon: 'fab fa-redis'
|
||
});
|
||
}
|
||
|
||
return items;
|
||
}
|
||
|
||
// 启用键盘快捷键
|
||
enableKeyboardShortcuts() {
|
||
// 定义快捷键
|
||
this.shortcuts.set('ctrl+h', {
|
||
action: () => this.navigateTo('/'),
|
||
description: '返回首页'
|
||
});
|
||
|
||
this.shortcuts.set('ctrl+1', {
|
||
action: () => this.navigateTo('/cassandra-compare'),
|
||
description: 'Cassandra工具'
|
||
});
|
||
|
||
this.shortcuts.set('ctrl+2', {
|
||
action: () => this.navigateTo('/redis-compare'),
|
||
description: 'Redis工具'
|
||
});
|
||
|
||
// 绑定键盘事件
|
||
document.addEventListener('keydown', (e) => {
|
||
const key = this.getKeyString(e);
|
||
const shortcut = this.shortcuts.get(key);
|
||
|
||
if (shortcut) {
|
||
e.preventDefault();
|
||
shortcut.action();
|
||
}
|
||
});
|
||
|
||
// 显示快捷键提示
|
||
this.createShortcutsTooltip();
|
||
}
|
||
|
||
getKeyString(event) {
|
||
const keys = [];
|
||
|
||
if (event.ctrlKey || event.metaKey) keys.push('ctrl');
|
||
if (event.altKey) keys.push('alt');
|
||
if (event.shiftKey) keys.push('shift');
|
||
|
||
keys.push(event.key.toLowerCase());
|
||
|
||
return keys.join('+');
|
||
}
|
||
|
||
createShortcutsTooltip() {
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'keyboard-shortcuts-tooltip';
|
||
tooltip.style.cssText = `
|
||
position: fixed;
|
||
bottom: 100px;
|
||
right: 30px;
|
||
z-index: 1000;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
font-size: 12px;
|
||
display: none;
|
||
`;
|
||
|
||
let html = '<div class="fw-bold mb-2">键盘快捷键:</div>';
|
||
this.shortcuts.forEach((shortcut, key) => {
|
||
const displayKey = key.replace('ctrl', 'Ctrl').replace('+', ' + ');
|
||
html += `<div><kbd>${displayKey}</kbd> ${shortcut.description}</div>`;
|
||
});
|
||
|
||
tooltip.innerHTML = html;
|
||
document.body.appendChild(tooltip);
|
||
|
||
// 按?键显示/隐藏
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||
e.preventDefault();
|
||
tooltip.style.display = tooltip.style.display === 'none' ? 'block' : 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 程序化导航
|
||
navigateTo(url, replace = false) {
|
||
if (replace) {
|
||
window.history.replaceState(null, '', url);
|
||
} else {
|
||
window.location.href = url;
|
||
}
|
||
}
|
||
|
||
// 更新页面标题
|
||
updatePageTitle(title) {
|
||
const toolInfo = this.getCurrentToolInfo();
|
||
if (toolInfo) {
|
||
document.title = `${title} - ${toolInfo.name} - DataTools Pro`;
|
||
} else {
|
||
document.title = `${title} - DataTools Pro`;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 4. 模块间通信
|
||
|
||
### 4.1 事件驱动架构
|
||
```javascript
|
||
/**
|
||
* 事件总线 - 模块间通信
|
||
*/
|
||
class EventBus {
|
||
constructor() {
|
||
this.events = new Map();
|
||
}
|
||
|
||
// 监听事件
|
||
on(eventName, callback) {
|
||
if (!this.events.has(eventName)) {
|
||
this.events.set(eventName, new Set());
|
||
}
|
||
this.events.get(eventName).add(callback);
|
||
}
|
||
|
||
// 移除监听
|
||
off(eventName, callback) {
|
||
if (this.events.has(eventName)) {
|
||
this.events.get(eventName).delete(callback);
|
||
}
|
||
}
|
||
|
||
// 触发事件
|
||
emit(eventName, ...args) {
|
||
if (this.events.has(eventName)) {
|
||
this.events.get(eventName).forEach(callback => {
|
||
try {
|
||
callback(...args);
|
||
} catch (error) {
|
||
console.error(`事件处理错误 [${eventName}]:`, error);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 一次性监听
|
||
once(eventName, callback) {
|
||
const onceCallback = (...args) => {
|
||
callback(...args);
|
||
this.off(eventName, onceCallback);
|
||
};
|
||
this.on(eventName, onceCallback);
|
||
}
|
||
}
|
||
|
||
// 全局事件总线实例
|
||
const eventBus = new EventBus();
|
||
```
|
||
|
||
### 4.2 模块依赖注入
|
||
```javascript
|
||
/**
|
||
* 依赖注入容器
|
||
*/
|
||
class DIContainer {
|
||
constructor() {
|
||
this.services = new Map();
|
||
this.instances = new Map();
|
||
}
|
||
|
||
// 注册服务
|
||
register(name, factory, singleton = true) {
|
||
this.services.set(name, { factory, singleton });
|
||
}
|
||
|
||
// 获取服务实例
|
||
get(name) {
|
||
if (!this.services.has(name)) {
|
||
throw new Error(`Service '${name}' not found`);
|
||
}
|
||
|
||
const service = this.services.get(name);
|
||
|
||
if (service.singleton) {
|
||
if (!this.instances.has(name)) {
|
||
this.instances.set(name, service.factory());
|
||
}
|
||
return this.instances.get(name);
|
||
} else {
|
||
return service.factory();
|
||
}
|
||
}
|
||
|
||
// 清除实例缓存
|
||
clear(name) {
|
||
if (name) {
|
||
this.instances.delete(name);
|
||
} else {
|
||
this.instances.clear();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 全局DI容器
|
||
const container = new DIContainer();
|
||
|
||
// 注册核心服务
|
||
container.register('config', () => config);
|
||
container.register('utils', () => utils);
|
||
container.register('api', () => apiManager);
|
||
container.register('navigation', () => navigationManager);
|
||
container.register('eventBus', () => eventBus);
|
||
```
|
||
|
||
## 5. 性能优化
|
||
|
||
### 5.1 模块懒加载
|
||
```javascript
|
||
/**
|
||
* 模块懒加载管理器
|
||
*/
|
||
class LazyLoader {
|
||
constructor() {
|
||
this.loadedModules = new Set();
|
||
this.loadingPromises = new Map();
|
||
}
|
||
|
||
async loadModule(modulePath) {
|
||
if (this.loadedModules.has(modulePath)) {
|
||
return Promise.resolve();
|
||
}
|
||
|
||
if (this.loadingPromises.has(modulePath)) {
|
||
return this.loadingPromises.get(modulePath);
|
||
}
|
||
|
||
const loadPromise = import(modulePath).then(module => {
|
||
this.loadedModules.add(modulePath);
|
||
this.loadingPromises.delete(modulePath);
|
||
return module;
|
||
});
|
||
|
||
this.loadingPromises.set(modulePath, loadPromise);
|
||
return loadPromise;
|
||
}
|
||
|
||
async loadModules(modulePaths) {
|
||
return Promise.all(modulePaths.map(path => this.loadModule(path)));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.2 缓存策略
|
||
```javascript
|
||
/**
|
||
* 缓存管理器
|
||
*/
|
||
class CacheManager {
|
||
constructor(maxSize = 100) {
|
||
this.cache = new Map();
|
||
this.maxSize = maxSize;
|
||
this.accessOrder = [];
|
||
}
|
||
|
||
set(key, value, ttl = 0) {
|
||
const item = {
|
||
value,
|
||
timestamp: Date.now(),
|
||
ttl
|
||
};
|
||
|
||
if (this.cache.has(key)) {
|
||
this.updateAccessOrder(key);
|
||
} else {
|
||
if (this.cache.size >= this.maxSize) {
|
||
this.evictLRU();
|
||
}
|
||
this.accessOrder.push(key);
|
||
}
|
||
|
||
this.cache.set(key, item);
|
||
}
|
||
|
||
get(key) {
|
||
const item = this.cache.get(key);
|
||
|
||
if (!item) return null;
|
||
|
||
// 检查过期时间
|
||
if (item.ttl > 0 && Date.now() - item.timestamp > item.ttl) {
|
||
this.delete(key);
|
||
return null;
|
||
}
|
||
|
||
this.updateAccessOrder(key);
|
||
return item.value;
|
||
}
|
||
|
||
delete(key) {
|
||
this.cache.delete(key);
|
||
const index = this.accessOrder.indexOf(key);
|
||
if (index > -1) {
|
||
this.accessOrder.splice(index, 1);
|
||
}
|
||
}
|
||
|
||
updateAccessOrder(key) {
|
||
const index = this.accessOrder.indexOf(key);
|
||
if (index > -1) {
|
||
this.accessOrder.splice(index, 1);
|
||
}
|
||
this.accessOrder.push(key);
|
||
}
|
||
|
||
evictLRU() {
|
||
const oldestKey = this.accessOrder.shift();
|
||
this.cache.delete(oldestKey);
|
||
}
|
||
|
||
clear() {
|
||
this.cache.clear();
|
||
this.accessOrder = [];
|
||
}
|
||
}
|
||
```
|
||
|
||
## 6. 错误处理和调试
|
||
|
||
### 6.1 全局错误处理
|
||
```javascript
|
||
/**
|
||
* 全局错误处理器
|
||
*/
|
||
class ErrorHandler {
|
||
constructor() {
|
||
this.setupGlobalHandlers();
|
||
this.errorReporters = [];
|
||
}
|
||
|
||
setupGlobalHandlers() {
|
||
// JavaScript错误
|
||
window.addEventListener('error', (e) => {
|
||
this.handleError(e.error, 'javascript');
|
||
});
|
||
|
||
// Promise拒绝错误
|
||
window.addEventListener('unhandledrejection', (e) => {
|
||
this.handleError(e.reason, 'promise');
|
||
e.preventDefault();
|
||
});
|
||
|
||
// 资源加载错误
|
||
window.addEventListener('error', (e) => {
|
||
if (e.target !== window) {
|
||
this.handleError(new Error(`资源加载失败: ${e.target.src || e.target.href}`), 'resource');
|
||
}
|
||
}, true);
|
||
}
|
||
|
||
handleError(error, type = 'unknown') {
|
||
const errorInfo = {
|
||
message: error.message || error.toString(),
|
||
stack: error.stack,
|
||
type,
|
||
timestamp: new Date().toISOString(),
|
||
url: window.location.href,
|
||
userAgent: navigator.userAgent
|
||
};
|
||
|
||
console.error('Global Error:', errorInfo);
|
||
|
||
// 通知错误报告器
|
||
this.errorReporters.forEach(reporter => {
|
||
try {
|
||
reporter(errorInfo);
|
||
} catch (reportError) {
|
||
console.error('Error reporter failed:', reportError);
|
||
}
|
||
});
|
||
|
||
// 显示用户友好的错误信息
|
||
if (type !== 'resource') {
|
||
this.showUserError(error);
|
||
}
|
||
}
|
||
|
||
showUserError(error) {
|
||
if (window.DataToolsApp && window.DataToolsApp.ui) {
|
||
window.DataToolsApp.ui.alert.error(
|
||
'系统出现错误,请刷新页面后重试'
|
||
);
|
||
}
|
||
}
|
||
|
||
addErrorReporter(reporter) {
|
||
this.errorReporters.push(reporter);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.2 调试工具
|
||
```javascript
|
||
/**
|
||
* 开发调试工具
|
||
*/
|
||
class DebugTool {
|
||
constructor() {
|
||
this.isEnabled = this.checkDebugMode();
|
||
this.logs = [];
|
||
this.maxLogs = 1000;
|
||
|
||
if (this.isEnabled) {
|
||
this.setupDebugConsole();
|
||
}
|
||
}
|
||
|
||
checkDebugMode() {
|
||
return window.location.search.includes('debug=true') ||
|
||
localStorage.getItem('datatools_debug') === 'true';
|
||
}
|
||
|
||
log(message, data = null) {
|
||
if (!this.isEnabled) return;
|
||
|
||
const logEntry = {
|
||
timestamp: Date.now(),
|
||
message,
|
||
data,
|
||
stack: new Error().stack
|
||
};
|
||
|
||
this.logs.push(logEntry);
|
||
|
||
if (this.logs.length > this.maxLogs) {
|
||
this.logs.shift();
|
||
}
|
||
|
||
console.log(`[DEBUG] ${message}`, data);
|
||
}
|
||
|
||
setupDebugConsole() {
|
||
// 添加全局调试命令
|
||
window.DataToolsDebug = {
|
||
getLogs: () => this.logs,
|
||
clearLogs: () => { this.logs = []; },
|
||
exportLogs: () => {
|
||
const data = JSON.stringify(this.logs, null, 2);
|
||
fileUtils.download(data, 'datatools-debug.json', 'application/json');
|
||
},
|
||
getAppState: () => window.DataToolsApp,
|
||
getModules: () => ({
|
||
config,
|
||
utils,
|
||
apiManager,
|
||
navigationManager
|
||
})
|
||
};
|
||
|
||
console.log('DataTools Pro Debug Mode Enabled');
|
||
console.log('Use DataToolsDebug object for debugging');
|
||
}
|
||
}
|
||
```
|
||
|
||
## 7. 测试策略
|
||
|
||
### 7.1 单元测试框架
|
||
```javascript
|
||
/**
|
||
* 轻量级测试框架
|
||
*/
|
||
class TestFramework {
|
||
constructor() {
|
||
this.tests = [];
|
||
this.results = {
|
||
passed: 0,
|
||
failed: 0,
|
||
errors: []
|
||
};
|
||
}
|
||
|
||
describe(description, testFn) {
|
||
console.group(`📋 ${description}`);
|
||
testFn();
|
||
console.groupEnd();
|
||
}
|
||
|
||
it(description, testFn) {
|
||
try {
|
||
testFn();
|
||
this.results.passed++;
|
||
console.log(`✅ ${description}`);
|
||
} catch (error) {
|
||
this.results.failed++;
|
||
this.results.errors.push({ description, error });
|
||
console.error(`❌ ${description}:`, error);
|
||
}
|
||
}
|
||
|
||
expect(actual) {
|
||
return {
|
||
toBe: (expected) => {
|
||
if (actual !== expected) {
|
||
throw new Error(`Expected ${expected}, got ${actual}`);
|
||
}
|
||
},
|
||
toEqual: (expected) => {
|
||
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
||
}
|
||
},
|
||
toBeTruthy: () => {
|
||
if (!actual) {
|
||
throw new Error(`Expected truthy value, got ${actual}`);
|
||
}
|
||
},
|
||
toBeFalsy: () => {
|
||
if (actual) {
|
||
throw new Error(`Expected falsy value, got ${actual}`);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
async runTests() {
|
||
console.log('🚀 Running DataTools Pro Tests...');
|
||
|
||
// 运行所有测试
|
||
this.testUtils();
|
||
this.testAPI();
|
||
this.testUI();
|
||
|
||
// 输出结果
|
||
console.log(`\n📊 Test Results:`);
|
||
console.log(`✅ Passed: ${this.results.passed}`);
|
||
console.log(`❌ Failed: ${this.results.failed}`);
|
||
|
||
if (this.results.errors.length > 0) {
|
||
console.log('\n❌ Errors:');
|
||
this.results.errors.forEach(({ description, error }) => {
|
||
console.error(` - ${description}: ${error.message}`);
|
||
});
|
||
}
|
||
}
|
||
|
||
testUtils() {
|
||
this.describe('Utils Module', () => {
|
||
this.it('should escape HTML correctly', () => {
|
||
const result = utils.escapeHtml('<script>alert("xss")</script>');
|
||
this.expect(result).toBe('<script>alert("xss")</script>');
|
||
});
|
||
|
||
this.it('should format date correctly', () => {
|
||
const date = new Date('2024-08-05T10:30:00Z');
|
||
const result = utils.formatDate(date, 'YYYY-MM-DD');
|
||
this.expect(result).toBe('2024-08-05');
|
||
});
|
||
|
||
this.it('should debounce function calls', (done) => {
|
||
let callCount = 0;
|
||
const debouncedFn = utils.debounce(() => callCount++, 100);
|
||
|
||
debouncedFn();
|
||
debouncedFn();
|
||
debouncedFn();
|
||
|
||
setTimeout(() => {
|
||
this.expect(callCount).toBe(1);
|
||
done();
|
||
}, 150);
|
||
});
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 构建和部署
|
||
|
||
### 8.1 开发环境
|
||
```javascript
|
||
/**
|
||
* 开发环境配置
|
||
*/
|
||
const devConfig = {
|
||
// 热重载
|
||
enableHotReload: true,
|
||
|
||
// 调试模式
|
||
debugMode: true,
|
||
|
||
// 本地服务器配置
|
||
devServer: {
|
||
port: 3000,
|
||
host: 'localhost',
|
||
open: true
|
||
},
|
||
|
||
// 代理配置
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://localhost:5000',
|
||
changeOrigin: true
|
||
}
|
||
}
|
||
};
|
||
```
|
||
|
||
### 8.2 生产环境优化
|
||
```javascript
|
||
/**
|
||
* 生产环境优化
|
||
*/
|
||
const prodOptimizations = {
|
||
// 代码压缩
|
||
minification: true,
|
||
|
||
// 资源合并
|
||
bundling: false, // 保持模块化
|
||
|
||
// 缓存策略
|
||
caching: {
|
||
staticAssets: '1y',
|
||
api: '5m'
|
||
},
|
||
|
||
// CDN配置
|
||
cdn: {
|
||
bootstrap: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css',
|
||
fontawesome: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css'
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
**版本**: v2.0
|
||
**更新日期**: 2024-08-05
|
||
**维护者**: DataTools Pro Team |