news 2026/9/24 16:07:19

HyperDX 代码规范实战指南:grep-first 复用、Mantine 语义变体与前端工程最佳实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HyperDX 代码规范实战指南:grep-first 复用、Mantine 语义变体与前端工程最佳实践

HyperDX 代码规范实战指南:grep-first 复用、Mantine 语义变体与前端工程最佳实践

【免费下载链接】hyperdxResolve production issues, fast. An open source observability platform unifying session replays, logs, metrics, traces and errors powered by ClickHouse and OpenTelemetry.项目地址: https://gitcode.com/gh_mirrors/hy/hyperdx

本文以仓库 agent_docs/code_style.md 为骨架,结合packages/下的真实源码与 CI 脚本展开。它面向在 HyperDX 仓库中新增类型、Zod schema、辅助函数或 React 组件的开发者(含 AI 编码 Agent),系统讲解"先搜索后实现"的复用纪律、common-utils共享代码地图、Mantine 组件变体约束、语义设计 token、文案规范与重构原则。读完你可以做到:在动手写代码前用几分钟定位到仓库里已有的实现,写出与 HyperDX / ClickStack 双品牌主题完全一致的 UI,并且不踩中已被明令禁止的代码模式。

文档定位:一份给编码 Agent 的"渐进式披露"规范

agent_docs/目录采用渐进式披露设计:详细的领域知识不塞进每个会话都会加载的AGENTS.md,而是按任务按需读取。agent_docs/README.md 明确说明,code_style.md的适用时机是"在任何包中新增类型、Zod schema、辅助函数或组件之前(grep-first 规则 + common-utils 内容地图),以及任何packages/appUI 改动之前"。

开篇有一条重要提示:Pre-commit hooks 会自动处理格式化,因此本文档聚焦于实现模式而非格式细节——代码风格约束的核心是"模式正确",而不是"缩进正确"。

TypeScript:消灭any,拥抱类型推导

HyperDX 的 TypeScript 基调是用类型系统表达契约

  • 避免any,使用恰当的类型;
  • 避免as强制断言,尽可能使用satisfies或类型推导;
  • 运行时校验统一走Zod schemas
  • 为数据结构定义清晰的 interface;
  • 实现正确的错误边界(error boundaries);
  • 定义并导入可复用的具名类型,而不是到处重复冗长的类型标注。

"命名类型复用"在仓库中有直接的落地证据。packages/app/src/types.ts通过别名重导出common-utils中的类型,避免在 app 包内重复定义:

// packages/app/src/types.ts import { NumberFormat as _NumberFormat } from '@hyperdx/common-utils/dist/types'; export type NumberFormat = _NumberFormat;

这种"别名垫片(alias-shim)"模式会在下文"找到已有实现之后怎么办"一节中详细展开。

代码组织:单一职责、行数上限与强制 DRY

组织规范共四条:

  • 单一职责:每个组件/函数只做一件事;
  • 文件大小:单文件上限300 行,接近上限就要重构;
  • DRY(必选):在新增类型、schema、辅助函数或组件之前,先 grep 是否已有实现——即下一节要讲的 grep-first 规则;
  • 上下文学习:实现前先阅读仓库中相似的文件,对齐既有写法。

新增类型 / Schema / 辅助函数之前:先 grep(REQUIRED)

这是本文档唯一的强制流程,值得单独成节。规则原文:在你定义新的类型、Zod schema、辅助函数或 React 组件之前,先搜索是否已有实现。关键技巧是:

搜索的是操作(把列表切分、按时间戳分桶、校验 URL、格式化时长),而不是你正准备给它起的名字。名字各不相同,但操作是相同的。

官方给出的搜索命令(约 20ms 内完成):

# 用两三个词描述操作,作为备选关键词 grep -rniE "export (const|function|type|interface|enum) [a-zA-Z]*(bucket|interval|granular)" \ packages/common-utils/src packages/app/src packages/api/src \ --include='*.ts' --include='*.tsx'

搜索顺序:先搜你正在编辑的文件 → 再搜packages/common-utils/src/→ 最后搜你所在包的其余部分。

一个关键的架构事实:common-utils没有根 barrel(root barrel)——没有src/index.tspackage.json也不声明入口点,因此所有导入都是形如@hyperdx/common-utils/dist/...的深路径(deep import)。packages/common-utils/src/types.ts仅这一个文件就有约 2850 行、280+ 个导出(实测当前仓库为 3150 行、314 个 export 行,还在持续增长)。你无法通过读 index 发现已有什么,grep 才是索引。

共享代码已经住在哪里:common-utils 内容地图

仓库中跨包的导入约 90% 集中在九个模块(下表中.../dist均指@hyperdx/common-utils/dist):

导入路径承载内容
.../dist/types所有共享 Zod schema 及其推断类型:sources、alerts、dashboards、tiles、webhooks、chart configs,以及SQLIntervalMetricsDataTypeStacktraceFrame
.../dist/core/utils时间分桶与粒度计算(toStartOfIntervaltimeBucketByGranularityconvertGranularityToSeconds)、字符串拆分(splitAndTrimCSVsplitAndTrimWithBracketescapeSqlString)、hashCodeparseJSONformatDate
.../dist/core/metadataClickHouse 表/列内省——MetadatagetMetadataparseKeyPathunquoteIdentifier
.../dist/clickhouse/node/browser查询客户端、ChSql/chSql/concatChSql、ClickHouse ↔ JS 类型转换
.../dist/core/renderChartConfigChartConfig→ SQL
.../dist/filters过滤器状态、序列化与校验(FilterStatefiltersToQueryparseQuery
.../dist/guardsChart-config 与 source 类型守卫(isBuilderChartConfigisRawSqlChartConfigisHeatmapCompatibleSource
.../dist/validationisValidUrlisValidSlackUrl、密码校验器
.../dist/variablesDashboard 变量解析与模板展开

时间分桶函数在源码中有完整实现,例如 packages/common-utils/src/core/utils.ts#L599 的convertGranularityToSecondsSQLInterval粒度换算为秒,#L668 的timeBucketByGranularity在此基础上按起点做分桶迭代——这正是"先 grep 到它、别自己重写"的典型场景。

第二顺位的包内位置:packages/app/src/utils.tspackages/app/src/types.tspackages/app/src/ChartUtils.tsxpackages/api/src/utils/

找到已有实现之后怎么办

文档给出了一张"决策表",共五种情形:

情形做法
存在规范版本且适用直接导入,删掉你正在写的那份拷贝
存在,但想用本地名别名垫片——如 packages/app/src/types.ts#L9 的export type NumberFormat = _NumberFormat;,绝不重新输入一遍定义
存在,但形状"几乎合适"从它派生PickOmit.extend()Alert & { … }),或参数化规范版本,保持单一事实来源
两个组件间有共享逻辑提炼成兄弟模块——如 packages/app/src/components/sourceSelectUtils.tsx#L51 的useFilteredSortedSourceItems,被SourceSelectSourceMultiSelect共同使用
两个版本必须并存用 doc-comment 说明"为什么",并点名孪生版本。典型例子见 packages/common-utils/src/types.ts#L571-L582:Mongoose 的IWebhook(定义于 packages/api/src/models/webhook.ts#L17)与 JSON 序列化用的WebhookSchema成对存在

不可接受的做法:出现第二个定义,却没有别名、没有解释孪生关系的 doc-comment、也没有把两者钉在一起的测试。

整文件跨包移植:@source标记

当整个文件必须从其他包复制过来(packages/cli就为"Web Frontend Alignment"清单中的组件做了这件事,见 packages/cli/AGENTS.md),必须在文件头部用@source标签标明来源,一个来源文件一个标签

/** * Source helper functions. * * @source packages/app/src/source.ts */

CI 脚本 scripts/ci/ratchet.mjs 会统计这些标签,让总量在 CI 中保持可见。但注意文档的明确态度:该统计是建议性的,不是门禁——标注移植永远不会让构建失败,而删除标签来"变绿"只是隐藏了拷贝而非移除拷贝。想降低计数,正确做法是把共享代码迁入common-utils,然后运行yarn ratchet:update锁定基线。

React 模式:函数组件 + Hooks + 自定义 Hook

  • 使用函数组件 + hooks,不用 class 组件;
  • 小而聚焦的组件;
  • 把可复用逻辑提取为自定义 hooks
  • 为 props 定义 TypeScript interface;
  • 列表使用正确的 key,昂贵计算使用 memoization。

Mantine UI:自定义变体是唯一正解

项目以Mantine UI为组件层,但通过自定义变体(variants)在品牌主题中统一样式。注意:文档原文写的packages/app/src/theme/mantineTheme.ts在仓库中实际位于品牌子目录——packages/app/src/theme/themes/hyperdx/mantineTheme.ts 与 packages/app/src/theme/themes/clickstack/mantineTheme.ts,两个品牌主题各自构建MantineThemeOverride

两条总原则:

  • 优先用 Mantine 组件,而不是自写样式的元素;
  • 优先用 Mantine 单个 style props(如m='xs'),而不是原始 styles(如style={{ margin: '4px' }})。

Button 与 ActionIcon 变体(REQUIRED)

Button 和 ActionIcon 只能用下表这五种变体,这是硬性约束:

变体用途示例
variant="primary"主操作(Submit、Save、Create、Run)<Button variant="primary">Save</Button>
variant="secondary"次操作(Cancel、Clear、辅助动作)<Button variant="secondary">Cancel</Button>
variant="danger"破坏性操作(Delete、Remove、Rotate API Key)<Button variant="danger">Delete</Button>
variant="link"无背景无边框的链接式操作(View Details、导航式 CTA)<Button variant="link">View Details</Button>
variant="subtle"透明背景 + hover 高亮;工具栏/工具控件(折叠开关、关闭按钮)<Button variant="subtle">Filter</Button>

正确用法

<Button variant="primary">Save</Button> <Button variant="secondary">Cancel</Button> <Button variant="danger">Delete</Button> <Button variant="subtle">Filter</Button> <Button variant="link">View Details</Button> <ActionIcon variant="primary">...</ActionIcon> <ActionIcon variant="secondary">...</ActionIcon> <ActionIcon variant="danger">...</ActionIcon> <ActionIcon variant="link">...</ActionIcon> <ActionIcon variant="subtle">...</ActionIcon>

禁止模式light/outline/filled/default+ 调色板颜色均不可用于 Button/ActionIcon):

<Button variant="light" color="green">Save</Button> <Button variant="outline" color="red">Delete</Button> <Button variant="filled" color="gray">Cancel</Button> <Button variant="default">Cancel</Button> <ActionIcon variant="light" color="red">...</ActionIcon> <ActionIcon variant="filled" color="gray">...</ActionIcon>

两个变体的行为细节(在主题源码中均有对应实现):

  • link:无背景、无边框、文字用 muted 色(--color-text-secondary),hover 时文字提亮到全对比度,适合混入周围内容的链接式 CTA(如 "View Details"、"View Full Trace")——见 mantineTheme.ts#L323-L329 的Buttonvars 实现。
  • subtle:透明背景 + 标准文字色,hover 出现--color-bg-hover背景高亮。它是ActionIcon 的默认变体defaultProps: { variant: 'subtle' }),适合工具栏图标、折叠开关、关闭按钮。与 link 的区别是:subtle 显示 hover 背景,link 改变文字颜色。

注意variant="filled"表单输入(Select、TextInput 等)仍然有效,只是不能用于 Button/ActionIcon。

纯图标按钮必须用 ActionIcon

如果 Button 里只有图标、没有文字,就必须换成 ActionIcon

// ❌ 错误 —— Button 只装了一个图标 <Button variant="secondary" px="xs"> <IconRefresh size={18} /> </Button> // ✅ 正确 —— 纯图标按钮用 ActionIcon <ActionIcon variant="secondary" size="input-sm"> <IconRefresh size={18} /> </ActionIcon>

文档特别说明:这条规则无法被 ESLint 强制执行,必须靠人工代码审查

语义组件变体:Alert / Text / 危险控件

项目为AlertTextButtonActionIcon提供主题化的语义变体,让提示框和状态文字由设计 token 驱动,在 HyperDX 与 ClickStack 两个品牌以及明/暗模式下保持一致。优先使用这些变体,而不是裸的 Mantine 调色板颜色color="yellow"color="red"c="green"等)。

变体 → token 的映射集中定义在 packages/app/src/theme/themes/semanticVariants.ts(两个品牌主题共用的唯一事实来源):SEMANTIC_TEXT_COLORSSEMANTIC_CONTROL_COLORSSEMANTIC_ALERT_VARS分别驱动 Text、Button/ActionIcon 与 Alert。

Alert—— 支持info|success|warning|danger,渲染带色调的-subtle背景,标题、图标以及正文都使用语义色 token(Mantine 默认会把 message 强制成黑/白,主题通过styles覆盖让正文跟随--alert-color):

// ✅ token 驱动,两个品牌 + 明暗模式都正确,满足 WCAG AA <Alert variant="warning" title="Heads up">This may take a while.</Alert> <Alert variant="danger" title="Failed">Could not save the alert.</Alert> // ❌ 硬编码 Mantine 调色板 —— 不感知主题、对比度不一致 <Alert color="yellow" title="Heads up">...</Alert> <Alert color="red" title="Failed">...</Alert>

Text—— 支持danger|warning|success,用于行内状态/校验文字:

<Text variant="danger">Required field</Text> <Text variant="success">Connection verified</Text> // ❌ 语义状态文字不要用裸调色板 <Text c="red.5">Required field</Text>

Button/ActionIconvariant="danger"是"软"控件:色调化的--color-bg-danger-subtle背景(带 hover)加语义前景色,不是实心红填充。warning/success刻意不开放为控件变体,只用于TextAlert

兼容性说明:已有的<Alert color="...">调用点不会被改动,语义变体是 opt-in 的;但新增的提示框应优先用变体,动到附近代码时顺手迁移color="..."的旧写法。

确认对话框:一律使用useConfirm(REQUIRED)

任何"你确定吗?"步骤都必须用useConfirm@/useConfirm),禁止手搓带 Cancel/Confirm 按钮的<Modal>。Provider 已在 packages/app/pages/_app.tsx 全应用挂载,调用点零配置:

const confirm = useConfirm(); const handleDelete = async () => { if ( await confirm( <> Deleting {name} is <b>not reversible</b>. </>, 'Delete', { variant: 'danger' }, ) ) { await deleteThing.mutateAsync({ id }); } };

要点:

  • 消息是ReactNode,可以携带强调与多句话;
  • 破坏性操作传{ variant: 'danger' };确认按钮文案默认Confirm
  • 它恰好 resolve 一次:源码 packages/app/src/useConfirm.tsx#L44-L63 用 Promise 封装,onConfirm/onClose各自 resolve 后立即setState(null),所以关闭动画期间双击 Confirm 不会触发两次操作——手搓 Modal 必须自己防重;
  • 测试 id 是共享且已存在的:confirm-modalconfirm-confirm-buttonconfirm-cancel-button不要为每个流程发明新的 confirm/cancel 测试 id——E2E 页面对象依赖这些共享 id。

已知限制:它不向 Modal 传title,正文按size="sm" opacity={0.7}渲染,且 CSS opacity 作用于整个子树,嵌套的<Text>无法单独恢复全对比度。如果某个流程确实需要标题或全对比度正文,请扩展useConfirm(加可选 prop,作用于所有调用点),而不是另起一个一次性 Modal。

组件测试中要 mock 它——ConfirmProvider依赖next/router,jsdom 里不可用:

jest.mock('@/useConfirm', () => ({ useConfirm: jest.fn() }));

对参数做断言(需要检查文案时把消息ReactNode渲染出来);真正的对话框行为交给 E2E 验证。

空状态:一律使用EmptyState组件(REQUIRED)

所有空/无数据状态都用EmptyState@/components/EmptyState),禁止临时内联的空状态 div。组件实现在 packages/app/src/components/EmptyState.tsx。

Prop类型默认值说明
iconReactNode主题圆形内的图标(不传则隐藏)
titlestring标题文字(headline 风格,不加句号
descriptionReactNode标题下方的说明文字
childrenReactNode说明下方的操作(按钮、链接)
variant"default" \| "card""default""card"时包一层带边框的 Paper
// ❌ 差 —— 随手内联的空状态 <div className="text-center my-4 fs-8">No data</div> <Text ta="center" c="dimmed">Nothing here</Text> // ✅ 好 —— 使用 EmptyState 组件 <EmptyState icon={<IconBell size={32} />} title="No alerts created yet" description="Create alerts from dashboard charts or saved searches." variant="card" />

标题文案title当作短 headline(类似 UI 里的Title),不要以句号结尾;完整句子放进description,使用正常标点(需要时以句号结尾)。与列表页保持平行措辞(如 dashboards 和 saved searches 使用 "No matching … yet" / "No … yet" 且不带句号)。

代码片段:用 Mantine<Code>CopySnippet(REQUIRED)

禁止渲染裸<pre>、临时Paper+ 等宽字体、或未加样式的<code>。统一使用既有组件,与 Terraform export、onboarding 及 Storybook 指南保持一致:

种类组件使用时机
行内代码Mantine<Code>散文中的短 token(Session、列名、flag)
围栏 / 多行 / 可复制CopySnippet@/components/ClickStackOnboarding/CopySnippet安装命令、HCL、JSX 示例等用户可能复制的块,内部是<Code block>+ Copy 按钮
// ✅ 行内 —— Mantine Code Create a source with <Code>Session</Code> type. // ✅ 围栏 / 可复制 —— CopySnippet(Code + Copy) <CopySnippet label="Import block" snippet={`import { clickstack_dashboard } from "clickhouse/clickstack"`} /> // 周围标题已说明内容时,label 可省略 <CopySnippet snippet={USAGE} /> // ❌ 差 —— 裸 pre / 手写 Paper 外壳 <pre>{snippet}</pre> <Paper bg="var(--color-bg-code)"><Text component="pre" ff="monospace">{snippet}</Text></Paper>

CopySnippet的实现在 packages/app/src/components/ClickStackOnboarding/CopySnippet.tsx,它还支持accessKey属性——设置后片段会被脱敏,直到用户显式 Reveal(内部借助RevealSnippet)。SQL 查询预览(渲染高亮的 ClickHouse SQL)仍用SQLPreview/ChartSQLPreview——那些是编辑器,不是片段外壳。

图表卡片:用ChartCard而不是手搓边框

ChartCard@/components/charts/ChartCard)包图表卡片,实现见 packages/app/src/components/charts/ChartCard.tsx。它是带边框的表面,标题下方有与自定义 dashboard tile 相同的全宽分隔线,取代了旧的ChartBox不要手搓带边框的<div>/<Paper>包图表

ChartCard只渲染卡片外壳。头部分隔线只有在后代渲染了带title(或toolbarItems)的ChartContainer时才会出现——ChartCard提供ChartContainerCardHeaderProvider把头部切换到卡片模式——所以要在里面放一个渲染ChartContainer的图表(DBTimeChartDBTableChartDBHeatmapChartDBListBarChart等)。自带标题的内容(如定制表格卡片)也应让标题走带titleChartContainer而不是裸Text,从而获得同样的卡片头部(含分隔线与顶部内边距),而不是紧贴顶边框。tile 级控件(全屏、线/柱切换、kebab 菜单)属于 dashboard tile,刻意不属于ChartCard

Prop类型说明
childrenReactNode图表,通常是DB*Chart(或带标题的ChartContainer
styleCSSProperties尺寸/溢出覆盖——传固定height,或用flex: 1; height: 100%填满 flex 行(paddingInline被钉住以保持分隔线对齐)
data-testidstring测试钩子
// ✅ 好 —— 共享卡片外壳,与 dashboard tile 一致 <ChartCard style={{ height: 350 }}> <DBTimeChart title="Request Latency" config={config} /> </ChartCard> // ❌ 差 —— 手搓卡片,与 dashboard 观感脱节 <Box style={{ border: '1px solid var(--color-border)', borderRadius: 4 }}> <DBTimeChart title="Request Latency" config={config} /> </Box>

务必给它一个高度ChartCardwidth: 100%并填满父容器,所以父容器(或style={{ height }})必须定义高度。并排等宽图表(如 RED 行)在Flex内使用style={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}

UI 文案:一律 sentence case

所有用户可见文本使用句子式大小写(sentence case)——只大写首词以及专有名词/缩写,禁止 Title Case(每个实词都大写)。适用于用户读到的每个字符串:字段标签、按钮、tab/菜单项、标题、节标题、弹窗标题、占位符、tooltip、表格列头、空状态、toast/通知文案。

Title Case(避免)Sentence case(使用)
Data SourceData source
Chart NameChart name
Add SeriesAdd series
Count of EventsCount of events
Save ChangesSave changes
Delete DashboardDelete dashboard

专有名词、产品名、缩写保持原样——sentence case 只改变它们周围的普通词:HyperDX、ClickHouse、ClickStack、OpenTelemetry/OTel、Lucene、SQL、PromQL、MongoDB、Kubernetes、JSON、CSV、URL、ID、API、MCP、CPU、P95。例如:Search your events w/ LuceneEdit SQLCopy as cURLView in ClickHouse

只有静态 UI 外壳需要 sentence case;永远不要改写动态/用户数据(列名、tag 值、日志/trace 内容、用户输入的资源名)——按原样渲染。

语义设计 token:优先于裸 Mantine 颜色

UI 用 Mantine 组件搭建,但颜色和表面应遵循主题中的语义 CSS 自定义属性--color-*等),而不是临时 Mantine 调色板值。token 定义在packages/app/src/theme/themes/**/_tokens.scss(hyperdx 与 clickstack 各有 hyperdx/_tokens.scss 和 clickstack/_tokens.scss,另有共享的_base-tokens.scss),对齐 Click UI 风格系统,让 HyperDX 与 ClickStack 视觉一致,是通往统一设计系统的路径。

  • Do:布局、组件、间距用 Mantine;主题化背景、文字色、边框、状态用语义 token,如style={{ color: 'var(--color-text-muted)' }}style={{ border: '1px solid var(--color-border)' }}
  • Do not:存在语义 token 时依赖裸 Mantine 颜色 props 处理应用外壳与内容——如c="gray.5"bg="dark.7"、任意color="blue.4"用于应该与产品其余部分一致的表层;
  • 参考:packages/app/src/theme/semanticColorsGrouped.ts(token 名清单)、packages/app/src/theme/themes/下的主题 SCSS,以及 Storybook 的SemanticColors等主题故事。

packages/app/src/theme/**中的 Mantine 主题覆盖可能把 Mantine 的色阶映射到项目调色板(例如 mantineTheme.ts 里Button/ActionIcon/Alert的 vars 都指向--color-*token),但这不取代新样式中显式使用var(--color-...)

图表与可视化颜色是另一套更具体的契约——它们有独立的 categorical、semantic、heatmap 调色板,通过packages/app/src/utils.ts的辅助函数接线。不要硬编码系列颜色,也不要用--color-text-*系列给图表上色。动手渲染任何数据之前,先读 agent_docs/data_viz_colors.md。

重构:直接编辑,不留 v2 副本

  • 直接编辑文件——不要创建component-v2.tsx之类的副本
  • 检查受影响区域的重复代码;
  • 改动后验证所有调用方与集成点
  • 重构是为了提升清晰度或降低复杂度,不是为了改而改。

文件命名与组织

  • 遵循包内约定的清晰、描述性命名;
  • 永久文件名中避免"temp"、"refactored"、"improved" 等字眼;
  • 相关组件放进同一个目录

小结:落地清单

写代码前快速过一遍这份清单:新增类型/schema/helper 前先按"操作" grep(顺序:当前文件 →common-utils→ 包内),命中就导入或派生而不是重写;Button/ActionIcon 只用五种变体、纯图标用ActionIcon;确认弹窗走useConfirm、空状态走EmptyState、代码块走<Code>/CopySnippet、图表卡片走ChartCard;新文案用 sentence case 并保留专有名词原样;颜色优先--color-*语义 token,图表颜色另看data_viz_colors.md。把这些纪律内化后,你的改动会在视觉、类型与复用三个维度上都与现有代码库融为一体。

【免费下载链接】hyperdxResolve production issues, fast. An open source observability platform unifying session replays, logs, metrics, traces and errors powered by ClickHouse and OpenTelemetry.项目地址: https://gitcode.com/gh_mirrors/hy/hyperdx

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 16:06:30

变频水泵控制柜可以远程改压力吗?远程控制安全要点

变频水泵恒压供水控制柜可以远程控制&#xff0c;远程分两大类&#xff1a;简单远程(开关量)、物联网远程(手机/电脑看压力、调参数)&#xff0c;很多成品变频供水柜预留接口&#xff0c;加模块就能实现。一、两种远程方案 方案1&#xff1a;简易远程(仅启停&#xff0c;不能看…

作者头像 李华
网站建设 2026/9/24 16:03:57

Python个人博客项目-1.项目分析与环境配置

在不断探索Python的道路上,经历了数据分析、算法和爬虫工程的深入后,开启全栈开发的学习与实践之旅,逐步构建起属于个人的博客系统,不仅为工作日常提供记录和展示的平台,也为自我积累搭建起一个综合性展示窗口。此次项目建立在先前个人主页项目的基础上,继承了一部分功能…

作者头像 李华