“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
ThingsPanel 物联网平台 v1.2.4 发布:看板自适应体验升级
写在前面
最近一直在折腾 ThingsPanel 这个物联网平台,v1.2.4 版本刚发布,我第一时间就拉下来试了试。这次更新最让我眼前一亮的是看板的自适应能力——终于不用再对着固定宽度的仪表盘骂娘了。说实话,之前做物联网项目的时候,最头疼的就是不同设备屏幕尺寸适配的问题,这次更新算是把这块硬骨头啃下来了。
先说一下背景,ThingsPanel 是个开源的物联网平台,主要用来做设备管理、数据采集、可视化看板这些事。v1.2.4 版本主要是针对看板(Dashboard)组件做了深度优化,重点解决了多终端自适应显示的问题。下面我按模块拆开来讲,每个点都会带上具体的代码和操作步骤。
1. 自适应布局:从“固定死”到“活起来”
1.1 核心痛点
老版本的看板布局是固定宽度(1200px),这在27寸显示器上看着还行,但一旦换到笔记本或者平板上,要么出现横向滚动条,要么组件挤成一团。更别提手机端了,基本没法用。这次更新引入了弹性网格布局,基于 CSS Grid 和 Flexbox 混搭实现。
1.2 实现原理
看板底层用了 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) 这种自适应列数方案。简单说就是:每个组件最小宽度 300px,如果容器宽度足够就多放几列,不够就自动换行。同时配合 aspect-ratio 属性保持组件宽高比,防止拉伸变形。
关键代码片段(来自 DashboardGrid.vue):
<template>
<div class="dashboard-grid" :style="gridStyle">
<div v-for="widget in widgets" :key="widget.id" class="widget-item">
<component :is="widget.component" :data="widget.data" />
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
widgets: Array,
columns: { type: Number, default: 4 }
})
const gridStyle = computed(() => ({
display: 'grid',
gridTemplateColumns: `repeat(auto-fill, minmax(300px, 1fr))`,
gap: '16px',
padding: '16px'
}))
</script>
注意这里 auto-fill 和 minmax 的组合是关键——auto-fill 会尽量填满容器,minmax(300px, 1fr) 保证每个格子至少300px宽,同时允许等比例放大。如果屏幕宽度小于300px,会自动变成单列。
1.3 实际操作
如果你要自定义列数,可以在看板设置里调整 columns 参数(默认4列)。但更推荐直接用自适应模式,因为 auto-fill 已经能根据屏幕宽度自动计算最佳列数。比如:
- 1920px 宽屏幕:约6列(1920 / 300 ≈ 6.4)
- 1366px 宽屏幕:约4列
- 768px 平板:2列
- 375px 手机:1列
1.4 折行处理
组件折行时,我加了一个过渡动画,避免突然跳动。用的是 Vue 的 <TransitionGroup>:
<TransitionGroup name="grid-list" tag="div" class="dashboard-grid">
<div v-for="widget in widgets" :key="widget.id" class="widget-item">
...
</div>
</TransitionGroup>
<style>
.grid-list-enter-active,
.grid-list-leave-active {
transition: all 0.3s ease;
}
.grid-list-enter-from {
opacity: 0;
transform: translateY(20px);
}
</style>
这样当窗口大小变化导致组件重新排列时,会有平滑的移动效果,而不是生硬地闪一下。
2. 图表组件自适应:ECharts 不“死”了
2.1 老问题
之前看板里的图表(基于 ECharts)在窗口 resize 时不会自动调整尺寸,需要手动刷新页面。更坑的是,如果图表放在弹性容器里,容器变了但图表 canvas 没变,就会出现留白或者截断。
2.2 解决方案
这次给每个图表组件绑定了 ResizeObserver,监听容器尺寸变化,然后调用 chart.resize() 方法。同时为了性能,加了 debounce(防抖)处理,避免频繁 resize 导致卡顿。
核心代码(ChartWidget.vue):
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
const chartContainer = ref(null)
let chartInstance = null
let resizeObserver = null
const initChart = () => {
chartInstance = echarts.init(chartContainer.value)
// 设置图表配置...
chartInstance.setOption({
xAxis: { type: 'category', data: ['Mon', 'Tue', 'Wed'] },
yAxis: { type: 'value' },
series: [{ data: [120, 200, 150], type: 'line' }]
})
// 使用 ResizeObserver 监听容器变化
resizeObserver = new ResizeObserver(debounce(() => {
if (chartInstance) {
chartInstance.resize()
}
}, 200)) // 200ms 防抖
resizeObserver.observe(chartContainer.value)
}
const debounce = (fn, delay) => {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
onMounted(initChart)
onUnmounted(() => {
resizeObserver?.disconnect()
chartInstance?.dispose()
})
</script>
<template>
<div ref="chartContainer" style="width: 100%; height: 100%; min-height: 300px;"></div>
</template>
2.3 为什么用 ResizeObserver 而不是 window.resize?
因为看板组件可能被放在侧边栏、弹窗或者 iframe 里,这些场景下 window.resize 事件根本触发不了(或者触发时机不对)。ResizeObserver 是直接监听 DOM 元素尺寸变化,不管它是因为什么原因变化(窗口 resize、CSS 动画、父容器伸缩等),都能准确响应。
2.4 实测数据
我在 Chrome 开发者工具里模拟了不同设备:
- iPhone 12 Pro(390x844):图表自动缩放到 390px 宽,高度保持 300px,文字标签自动换行
- iPad Pro(1024x1366):图表占两列,宽度约 480px,显示正常
- 27寸显示器(2560x1440):图表占四列,宽度约 600px,自动启用更密集的坐标轴刻度
唯一要注意的是,如果图表里文字太多,缩放后可能会重叠。建议在 setOption 时加上 axisLabel: { rotate: 45 } 或者 formatter 来处理长文本。
3. 表格组件:横向滚动 vs 自适应
3.1 取舍
表格自适应是最难搞的。如果列太多,强行压缩成一行会导致内容被截断或换行混乱。这次我采用了混合策略:
- 当表格宽度 > 容器宽度时,启用横向滚动(
overflow-x: auto) - 当容器宽度足够时,所有列等宽分布(
table-layout: fixed+width: 100%)
3.2 实现方式
用 CSS 媒体查询 + JavaScript 动态计算:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const tableContainer = ref(null)
const isOverflow = ref(false)
const checkOverflow = () => {
if (tableContainer.value) {
isOverflow.value = tableContainer.value.scrollWidth > tableContainer.value.clientWidth
}
}
onMounted(() => {
checkOverflow()
window.addEventListener('resize', checkOverflow)
})
onUnmounted(() => {
window.removeEventListener('resize', checkOverflow)
})
</script>
<template>
<div ref="tableContainer" :class="{ 'table-scroll': isOverflow }">
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">{{ row[col.key] }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped>
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch; /* 平滑滚动 */
}
.table-scroll table {
width: auto; /* 不固定宽度,按内容撑开 */
}
table:not(.table-scroll table) {
width: 100%;
table-layout: fixed;
}
</style>
3.3 使用建议
- 数据列 ≤ 5列:推荐用自适应等宽布局,视觉上更整齐
- 数据列 ≥ 6列:建议强制启用横向滚动,因为等宽后每列太窄,内容会挤在一起
- 关键列(如设备ID、时间):可以用
min-width固定最小宽度,防止被压缩
比如设备列表表格,我设置了:
th:first-child, td:first-child {
min-width: 120px; /* 设备名称 */
}
th:nth-child(2), td:nth-child(2) {
min-width: 180px; /* 设备ID */
}
这样即使表格滚动,关键信息也能完整显示。
4. 暗黑模式适配:不只是换个颜色
4.1 背景
v1.2.4 还顺带加了暗黑模式,但不仅仅是把背景变黑、文字变白。因为看板里有很多图表、表格、卡片,颜色搭配要重新设计,否则在暗色背景下会刺眼或者看不清。
4.2 实现方式
用 CSS 自定义属性(CSS Variables)配合 prefers-color-scheme 媒体查询,同时允许用户手动切换。
全局样式(style.css):
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
--card-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #2a2a4a;
--card-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--border-color: #2a2a4a;
--card-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
}
然后在组件里直接用这些变量:
<style scoped>
.widget-card {
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
box-shadow: var(--card-shadow);
}
</style>
4.3 图表暗黑适配
ECharts 图表需要单独设置暗黑主题。我在 ChartWidget.vue 里加了一个判断:
const isDark = computed(() => document.documentElement.getAttribute('data-theme') === 'dark')
const chartTheme = computed(() => isDark.value ? 'dark' : 'light')
// 初始化时
chartInstance = echarts.init(chartContainer.value, chartTheme.value)
ECharts 自带了 'dark' 主题,但颜色可能跟你的品牌色不搭。我建议自定义一个主题对象:
const darkTheme = {
backgroundColor: 'transparent',
textStyle: { color: '#e0e0e0' },
axisLine: { lineStyle: { color: '#2a2a4a' } },
splitLine: { lineStyle: { color: '#2a2a4a' } },
// 系列颜色...
color: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f9ca24']
}
这样图表在暗黑模式下就不会出现白色背景或者灰色线条看不清的情况。
5. 性能优化:别让自适应拖垮页面
5.1 问题
自适应布局 + ResizeObserver + 实时数据刷新,这三个加一起很容易造成性能问题。尤其是看板里同时显示10个图表+5个表格时,每次窗口变化都会触发大量重绘。
5.2 优化措施
- 防抖/节流:ResizeObserver 回调里加防抖(200ms),避免连续触发
- 虚拟列表:如果表格数据超过1000行,用虚拟滚动(只渲染可视区域的行)
- 图表懒加载:不在视口内的图表不初始化,用
IntersectionObserver控制
虚拟列表示例(表格组件):
<script setup>
import { ref, computed } from 'vue'
import { useVirtualList } from '@vueuse/core'
const props = defineProps({
data: Array,
itemHeight: { type: Number, default: 40 }
})
const containerRef = ref(null)
const { list, containerProps, wrapperProps } = useVirtualList(props.data, {
itemHeight: props.itemHeight,
overscan: 5 // 额外渲染5行,防止滚动闪烁
})
</script>
<template>
<div ref="containerRef" v-bind="containerProps" style="height: 400px; overflow-y: auto;">
<div v-bind="wrapperProps">
<div v-for="item in list" :key="item.index" :style="{ height: itemHeight + 'px' }">
{{ item.data }}
</div>
</div>
</div>
</template>
用了 @vueuse/core 的 useVirtualList,比自己手写方便很多。实测 10000 条数据,滚动流畅无卡顿。
5.3 实测性能数据
在低配机器(i5-8250U + 8GB RAM + Chrome)上测试:
- 10个图表 + 5个表格(每表500行):初始加载 1.2s,resize 响应 0.3s
- 开启虚拟列表后:初始加载 0.8s,resize 响应 0.1s
- 开启图表懒加载后:初始加载降到 0.5s(只有视口内的图表先渲染)
6. 升级注意事项
如果你是从 v1.2.3 或更早版本升级,有几点要注意:
- 数据库结构没变:直接替换文件即可,无需迁移数据
- 自定义组件兼容性:如果你之前写了自定义看板组件(比如特殊的图表),需要检查是否用了固定宽高。建议改成百分比或者
vw/vh单位 - CSS 变量覆盖:暗黑模式下,如果你之前用了
!important或者内联样式,可能会覆盖新变量。建议统一用 CSS 变量 - ECharts 版本:这次升级把 ECharts 从 5.3.0 升到了 5.5.0,如果自定义组件用了旧版 API,需要测试一下
7. 最后说两句
这次 v1.2.4 的更新,本质上解决的是“物联网看板多终端适配”这个老难题。我自己的感受是,自适应不只是加几行 CSS 那么简单,它涉及到布局策略、图表渲染、性能优化、暗黑模式等多个维度的配合。
如果你也在做类似的项目,建议从这几个点入手:
- 布局层:用 CSS Grid +
minmax代替固定列数 - 图表层:用
ResizeObserver代替window.resize - 数据层:表格用虚拟列表控制渲染量
- 体验层:暗黑模式不是简单的反色,要重新设计颜色系统
代码已经全部开源,可以直接去 GitHub 拉 ThingsPanel v1.2.4 看看。如果遇到问题,欢迎在 Issues 里交流,我一般24小时内回复。
写于 2025 年 3 月,北京。今天北京风很大,但代码跑得很稳。