## 架构设计 **前端技术栈** - React 18 + TypeScript - 提供类型安全的现代化前端开发 - Tailwind CSS + shadcn/ui - 实现美观一致的用户界面 - Recharts - 专业的数据可视化图表库 - React Router - 单页应用路由管理 - React Hook Form - 高效的表单状态管理 **后端数据层** - Supabase - 现代化的后端即服务平台 - PostgreSQL - 可靠的关系型数据库 - 实时订阅 - 支持数据变更的实时推送 ## 数据库设计 创建了完整的数据库架构,包含5个核心表: 1. **devices** - 设备管理表,存储BBox设备信息、状态和配置 2. **battery_data** - 电池数据表,记录电压、电流、温度等实时数据 3. **ota_tasks** - OTA升级任务表,管理固件升级流程和状态 4. **mqtt_logs** - MQTT通信日志表,记录设备通信历史 5. **system_config** - 系统配置表,存储报警阈值和系统参数 ## 功能实现 **设备管理模块** - 实现了完整的CRUD操作,支持设备的创建、查看、编辑和删除 - 设备状态实时监控,包括在线/离线/维护/故障四种状态 - 设备详情页面展示最新电池数据和设备信息 - 智能搜索和状态筛选功能 **实时监控模块** - 多维度数据图表展示,支持电压、电流、温度、电量趋势分析 - 可配置时间范围查询(1小时到7天) - 实时数据更新机制,支持自动刷新 - 数据趋势指示器,显示数值变化方向 **OTA管理模块** - 升级任务创建和管理,支持批量设备升级 - 实时进度跟踪,模拟真实的下载和安装过程 - 详细的升级日志查看,便于故障排查 - 任务状态管理,支持重试和删除操作 **MQTT管理模块** - 连接状态监控和自动重连功能 - 消息日志实时查看和筛选 - 通信统计分析,包括消息总数和错误率 - 日志导出功能,支持CSV格式 **系统设置模块** - 分类配置管理,包括常规设置、MQTT配置、报警设置 - 表单验证和错误处理 - 系统信息展示,包括运行状态和存储统计 - 通知设置和权限管理 ## 代码质量 - 完整的TypeScript类型定义,确保类型安全 - 模块化的组件设计,便于维护和扩展 - 统一的错误处理和用户反馈机制 - 响应式设计,适配各种屏幕尺寸 - 代码通过ESLint检查,符合最佳实践 ## 数据初始化 系统包含丰富的示例数据: - 5个示例BBox设备,涵盖各种状态 - 历史电池数据,用于图表展示 - OTA升级任务示例,展示不同阶段状态 - MQTT通信日志,模拟真实设备通信 - 完整的系统配置参数 应用已完全实现需求文档中的所有功能,提供了专业级的电池管理解决方案。
197 lines
5.9 KiB
TypeScript
197 lines
5.9 KiB
TypeScript
|
||
import { defineConfig, loadConfigFromFile } from "vite";
|
||
import type { Plugin, ConfigEnv } from "vite";
|
||
import tailwindcss from "tailwindcss";
|
||
import autoprefixer from "autoprefixer";
|
||
import fs from "fs/promises";
|
||
import path from "path";
|
||
import {
|
||
makeTagger,
|
||
injectedGuiListenerPlugin,
|
||
injectOnErrorPlugin
|
||
} from "miaoda-sc-plugin";
|
||
|
||
const tailwindConfig = {
|
||
plugins: [
|
||
function ({ addUtilities }) {
|
||
addUtilities(
|
||
{
|
||
".border-t-solid": { "border-top-style": "solid" },
|
||
".border-r-solid": { "border-right-style": "solid" },
|
||
".border-b-solid": { "border-bottom-style": "solid" },
|
||
".border-l-solid": { "border-left-style": "solid" },
|
||
".border-t-dashed": { "border-top-style": "dashed" },
|
||
".border-r-dashed": { "border-right-style": "dashed" },
|
||
".border-b-dashed": { "border-bottom-style": "dashed" },
|
||
".border-l-dashed": { "border-left-style": "dashed" },
|
||
".border-t-dotted": { "border-top-style": "dotted" },
|
||
".border-r-dotted": { "border-right-style": "dotted" },
|
||
".border-b-dotted": { "border-bottom-style": "dotted" },
|
||
".border-l-dotted": { "border-left-style": "dotted" },
|
||
},
|
||
["responsive"]
|
||
);
|
||
},
|
||
],
|
||
};
|
||
|
||
export async function tryLoadConfigFromFile(
|
||
filePath: string,
|
||
env: ConfigEnv = { command: "serve", mode: "development" }
|
||
): Promise<any | null> {
|
||
try {
|
||
const result = await loadConfigFromFile(env, filePath);
|
||
return result ? result.config : null;
|
||
} catch (error) {
|
||
console.warn(`加载配置文件失败: ${filePath},尝试加载cjs版本`);
|
||
console.warn(error);
|
||
|
||
// 👇 创建 .cjs 临时文件重试
|
||
const tempFilePath =
|
||
filePath.replace(/\.(js|ts|mjs|mts)$/, "") + `.temp.cjs`;
|
||
try {
|
||
const originalContent = await fs.readFile(filePath, "utf-8");
|
||
|
||
// 补充逻辑:如果是 ESM 语法,无法直接 require,会失败
|
||
if (/^\s*import\s+/m.test(originalContent)) {
|
||
console.error(
|
||
`配置文件包含 import 语法,无法自动转为 CommonJS: ${filePath}`
|
||
);
|
||
return null;
|
||
}
|
||
|
||
await fs.writeFile(tempFilePath, originalContent, "utf-8");
|
||
|
||
const result = await loadConfigFromFile(env, tempFilePath);
|
||
return result ? result.config : null;
|
||
} catch (innerError) {
|
||
console.error(`重试加载临时 .cjs 文件失败: ${tempFilePath}`);
|
||
console.error(innerError);
|
||
return null;
|
||
} finally {
|
||
// 🧹 尝试删除临时文件
|
||
try {
|
||
await fs.unlink(tempFilePath);
|
||
} catch (_) {}
|
||
}
|
||
}
|
||
}
|
||
|
||
const env: ConfigEnv = { command: "serve", mode: "development" };
|
||
const configFile = path.resolve(__dirname, "vite.config.ts");
|
||
const result = await loadConfigFromFile(env, configFile);
|
||
const userConfig = result?.config;
|
||
const tailwindConfigFile = path.resolve(__dirname, "tailwind.config.js");
|
||
const tailwindResult = await tryLoadConfigFromFile(tailwindConfigFile, env);
|
||
const root = path.resolve(__dirname);
|
||
|
||
export default defineConfig({
|
||
...userConfig,
|
||
plugins: [
|
||
makeTagger(),
|
||
injectedGuiListenerPlugin({
|
||
path: 'https://resource-static.cdn.bcebos.com/common/v2/injected.js'
|
||
}),
|
||
injectOnErrorPlugin(),
|
||
...(userConfig?.plugins || []),
|
||
|
||
{
|
||
name: 'hmr-toggle',
|
||
configureServer(server) {
|
||
let hmrEnabled = true;
|
||
|
||
// 包装原来的 send 方法
|
||
const _send = server.ws.send;
|
||
server.ws.send = (payload) => {
|
||
if (hmrEnabled) {
|
||
return _send.call(server.ws, payload);
|
||
} else {
|
||
console.log('[HMR disabled] skipped payload:', payload.type);
|
||
}
|
||
};
|
||
|
||
// 提供接口切换 HMR
|
||
server.middlewares.use('/innerapi/v1/sourcecode/__hmr_off', (req, res) => {
|
||
hmrEnabled = false;
|
||
let body = {
|
||
status: 0,
|
||
msg: 'HMR disabled'
|
||
};
|
||
res.setHeader('Content-Type', 'application/json');
|
||
res.end(JSON.stringify(body));
|
||
});
|
||
|
||
server.middlewares.use('/innerapi/v1/sourcecode/__hmr_on', (req, res) => {
|
||
hmrEnabled = true;
|
||
let body = {
|
||
status: 0,
|
||
msg: 'HMR enabled'
|
||
};
|
||
res.setHeader('Content-Type', 'application/json');
|
||
res.end(JSON.stringify(body));
|
||
});
|
||
|
||
// 注册一个 HTTP API,用来手动触发一次整体刷新
|
||
server.middlewares.use('/innerapi/v1/sourcecode/__hmr_reload', (req, res) => {
|
||
if (hmrEnabled) {
|
||
server.ws.send({
|
||
type: 'full-reload',
|
||
path: '*', // 整页刷新
|
||
});
|
||
}
|
||
res.statusCode = 200;
|
||
let body = {
|
||
status: 0,
|
||
msg: 'Manual full reload triggered'
|
||
};
|
||
res.setHeader('Content-Type', 'application/json');
|
||
res.end(JSON.stringify(body));
|
||
});
|
||
},
|
||
load(id) {
|
||
if (id === 'virtual:after-update') {
|
||
return `
|
||
if (import.meta.hot) {
|
||
import.meta.hot.on('vite:afterUpdate', () => {
|
||
window.postMessage(
|
||
{
|
||
type: 'editor-update'
|
||
},
|
||
'*'
|
||
);
|
||
});
|
||
}
|
||
`;
|
||
}
|
||
},
|
||
transformIndexHtml(html) {
|
||
return {
|
||
html,
|
||
tags: [
|
||
{
|
||
tag: 'script',
|
||
attrs: {
|
||
type: 'module',
|
||
src: '/@id/virtual:after-update'
|
||
},
|
||
injectTo: 'body'
|
||
}
|
||
]
|
||
};
|
||
}
|
||
},
|
||
|
||
],
|
||
css: {
|
||
postcss: {
|
||
plugins: [
|
||
tailwindcss({
|
||
...(tailwindResult as any),
|
||
content: [`${root}/index.html`, `${root}/src/**/*.{js,ts,jsx,tsx}`],
|
||
}),
|
||
autoprefixer(),
|
||
],
|
||
},
|
||
}
|
||
});
|
||
|