ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

副屏信息终端搭建指南:让闲置显示器变身高价值仪表盘

副屏信息终端搭建指南:让闲置显示器变身高价值仪表盘 把电脑的第二块屏幕只是用来放桌面壁纸或者偶尔拖一个聊天窗口过去确实有些浪费。副屏真正适合做的事是成为一块常驻的信息展示面板时间、日期、待办事项、天气、系统资源占用、日程倒计时都可以在大字号、深色背景下持续显示。这种形态英文里通常叫 dashboard中文可以叫个人信息终端或副屏仪表盘。这篇文章会用一套可复现的方法把普通浏览器页面变成副屏上的常驻信息终端内容包括页面布局、数据源接入、开机自启、断网降级和常见问题排查。适合手里有多余显示器、旧平板、树莓派或低功耗小主机的开发者也适合第一次接触这类项目、想积累完整实践的新手。先说结论个人信息终端不复杂但涉及的环节不少。它不是简单写一个网页而是要把“页面展示、数据聚合、异常降级、开机守护”这四件事一起做完整才能真正长期放在副屏上。下面按从需求拆解到生产运行的方式逐步展开。1. 先把需求拆清楚信息终端到底要显示什么1.1 副屏的常见用法和真实痛点很多人的副屏实际承担的角色很单一。有人用它放聊天窗口有人用来挂视频有人把监控页面、代码文档拖过去但大多数情况下这些窗口总会被其他操作覆盖或者关闭电脑后就恢复成一张静态壁纸。副屏的利用率并不高。真正的问题不是副屏没有用而是缺少一个能够长期驻留、免打扰、不需要主动打开的信息入口。桌面小组件会被遮挡普通网页需要手动点开并且容易被切走如果让副屏专门承载“扫一眼就能拿到结论”的信息价值会比偶发使用高得多。所以这个项目要解决的是让副屏变成一块信息面板无论你在主屏处理什么任务副屏都能持续显示当前时间、待办、天气、系统负荷这些高频信息。不需要交互不需要点击只需要稳定、清晰、长时间在线。1.2 信息终端的信息层级设计在动手写代码前先想清楚哪些信息应该出现在副屏上。不是越多越好。副屏的核心价值是“一瞥即得”信息密度要控制在一次余光阅读的范围内。可以把信息按优先级分成三层信息级别典型内容更新频率数据来源离线影响基本盘时间、日期、星期、待办秒级本地时间、本地接口影响严重增效信息天气、节假日、日程、系统资源分钟级第三方接口、本地监控可降级显示扩展信息服务器状态、仓库动态、NAS 用量分钟级或小时级内部 API、爬虫、SDK可延迟显示基本盘必须保证可用。即使断网、接口异常时钟和待办也不能消失。增效信息允许出现“接口离线”这样的占位状态但页面不能因此白屏。扩展信息要有明确的缓存策略不能在断网时拖垮整块屏幕。设计原则只有一条遇到任何数据异常页面都应该降级而不是崩溃。降级方案可以是一个默认值、一块缓存数据、一行提示文字但绝不能是 JavaScript 报错后整页空白。1.3 三条实现路线对比实现“副屏信息终端”至少有三种常见路线网页仪表盘用 HTML、CSS、JavaScript 做页面再搭配一个轻量后端提供数据接口浏览器全屏化展示。桌面小组件使用 Windows 小部件、Linux 桌面组件框架实现系统级悬浮定制能力受平台限制。原生应用用 Electron、Qt、Flutter 等做独立应用可以访问系统级能力但工程量大。从开发成本、样式自由度和跨平台能力来看网页仪表盘是性价比最高的方案。它的调试方式直观改完代码刷新浏览器就能看到效果页面样式完全可控数据接口可以通过后端统一收敛而且同一套页面可以同时跑在 PC 副屏、旧平板、树莓派上。桌面小组件在特定系统里体验不错但换一个系统就要重写一套。原生应用适合要做成商业产品或需要深度系统交互的场景对个人信息面板来说偏重。所以下面整套方案围绕“浏览器全屏页面 轻量后端接口”来实现。2. 环境准备硬件形态、软件底座和目录结构2.1 根据手里硬件选择终端形态先明确副屏跑在什么硬件上再决定自启动和资源管理的方式。不同硬件形态的差异主要集中在“如何让页面保持全屏、如何开机自启、如何防止休眠”。硬件形态运行方式优点需要注意电脑的第二块显示器主系统启动后自动打开浏览器全屏开发调试最方便无额外成本主屏被误关或切走时容易打扰旧平板 / 旧手机浏览器全屏应用加载网页功耗低形态紧凑触摸屏需关闭自动锁屏浏览器进程容易被回收树莓派 / 迷你主机专用 Linux 系统 systemd 守护最接近生产环境可长期独立运行需要外接屏幕硬件调试成本略高如果只是个人桌面环境优先选择电脑第二块屏幕先在现有系统里把页面跑通再考虑独立硬件。如果目标是长期挂在墙上或角落旧平板和树莓派更合适。2.2 软件底座怎么选这里有两套方案可选。方案 A纯静态页面 JSON 数据文件。前端直接通过 fetch 读取本地 JSON逻辑最简单但没有后端无法聚合第三方接口也无法隐藏 API Key。方案 B轻量后端聚合接口 静态页面托管。后端用 FastAPI 或 Flask 提供一组/api/*接口静态页面统一从这些接口取数。这个方案多写一点代码但好处非常明显第三方天气、日历、节假日服务的 Key 可以放在环境变量里不需要写进前端页面。后端可以加缓存避免副屏每几秒钟就把请求打向外部服务。后续接入数据库、局域网内其他 API、甚至消息推送时前端结构不用大改。推荐使用方案 B。后文以 FastAPI 为例因为它的路由声明直观自带 OpenAPI 文档对调试很有帮助。2.3 初始目录结构与依赖安装先建立一个项目目录本文示例放在/opt/dashboard下实际项目中按自己的部署路径调整。dashboard/ ├── main.py # FastAPI 应用提供接口并托管静态页面 ├── requirements.txt ├── data/ │ └── todos.json # 待办数据持久化文件 ├── static/ │ ├── index.html # 副屏页面 │ ├── app.js # 页面逻辑与数据刷新 │ └── style.css # 样式也可内联在 index.html 中 └── scripts/ ├── start.sh # 启动后端服务 └── browser-kiosk.sh # 启动浏览器全屏模式创建 Python 虚拟环境并安装依赖python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txtrequirements.txt内容如下fastapi0.110 uvicorn[standard]0.29 psutil5.9 requests2.31安装完成后快速验证基础导入是否正常python -c import fastapi, psutil, requests; print(deps ok)如果能输出deps ok说明依赖安装完成。注意本文代码基于 Python 3.10 编写项目落地前要确认本机 Python 版本同时兼容 FastAPI 和 pydantic 的版本要求。3. 从零实现信息终端页面3.1 先搭页面骨架深色、大字、卡片布局副屏通常距离人眼有一定距离页面必须满足三个基本要求深色背景减少夜间刺眼感、大字号保证可读性、卡片布局让信息分区清晰。static/index.html的结构如下。这里把样式内联在style中是为了让读者最快跑起来实际项目中可以根据需要拆成独立 CSS 文件。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title副屏信息终端/title style :root { --bg: #0f172a; --card: #1e293b; --text: #e2e8f0; --muted: #94a3b8; --accent: #38bdf8; --ok: #10b981; --warn: #f59e0b; --danger: #ef4444; } * { margin: 0; padding: 0; box-sizing: border-box; } body { background: var(--bg); color: var(--text); font-family: PingFang SC, Microsoft YaHei, Segoe UI, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; } .dashboard { width: 100%; max-width: 1600px; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; } .card { background: var(--card); border-radius: 16px; padding: 24px; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); } .card h2 { font-size: 15px; color: var(--muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 16px; } .clock-time { font-size: 64px; font-weight: 700; letter-spacing: 0.02em; } .clock-date { font-size: 20px; color: var(--muted); margin-top: 8px; } .greeting { font-size: 18px; margin-top: 12px; color: var(--accent); } .value { font-size: 36px; font-weight: 600; } .label { font-size: 13px; color: var(--muted); } .todo-list { list-style: none; max-height: 260px; overflow-y: auto; } .todo-list li { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .todo-list li.done span { text-decoration: line-through; color: var(--muted); } .stat-row { display: flex; align-items: baseline; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); } #offline-banner { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); background: rgba(245, 158, 11, 0.15); color: var(--warn); padding: 8px 16px; border-radius: 8px; font-size: 14px; } /style /head body main classdashboard section classcard h2时钟/h2 div idclock-time classclock-time--:--:--/div div idclock-date classclock-date----/div div idgreeting classgreeting/div /section section classcard h2天气/h2 div idweather-city classlabel--/div div idweather-temp classvalue--/div div idweather-text classlabel--/div div idweather-humidity classlabel--/div /section section classcard h2待办/h2 ul idtodo-list classtodo-list/ul form idtodo-form stylemargin-top:12px; display:flex; gap:8px; input idtodo-input typetext placeholder添加待办 autocompleteoff button typesubmit添加/button /form /section section classcard h2系统状态/h2 div classstat-rowspan classlabelCPU/spanspan idcpu classvalue--/span/div div classstat-rowspan classlabel内存/spanspan idmemory classvalue--/span/div div classstat-rowspan classlabel磁盘/spanspan iddisk classvalue--/span/div /section /main div idoffline-banner styledisplay:none;接口离线正在显示缓存数据/div script srcapp.js/script /body /html页面布局使用的是grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))副屏分辨率较宽时四张卡片并排窄屏或竖屏时自动换行。这样同一套页面既能适应横向副屏也能适应旧平板竖屏。3.2 时钟、日期、问候语终端最基础的“活”信息时钟模块是整块屏幕的信息锚点需要放在最容易看到的位置。具体功能由static/app.js完成核心逻辑如下function updateClock() { const now new Date(); const time now.toLocaleTimeString(zh-CN, { hour12: false }); const date now.toLocaleDateString(zh-CN, { year: numeric, month: long, day: numeric, weekday: long }); document.getElementById(clock-time).textContent time; document.getElementById(clock-date).textContent date; const hour now.getHours(); let greeting 早上好; if (hour 12 hour 14) greeting 中午好; else if (hour 14 hour 18) greeting 下午好; else if (hour 18) greeting 晚上好; document.getElementById(greeting).textContent greeting; } setInterval(updateClock, 1000); updateClock();时钟每秒刷新一次日期和问候语虽然也在变化但频率很低直接每秒钟重写文本不会产生明显负担。这里使用浏览器本地时间前提是系统时间已经通过 NTP 同步。如果副屏设备长期不联网建议在系统层配置自动校时否则时间会越走越偏。3.3 待办事项从 JSON 接口读取并支持新增待办是副屏使用频率最高的数据之一。它不仅要展示还需要支持快速添加和勾选完成。后端提供三个接口GET /api/todos读取全部待办。POST /api/todos新增待办。PUT /api/todos/{id}切换完成状态。在main.py中先实现待办接口from datetime import datetime from pathlib import Path import json import time from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles import os import psutil import requests app FastAPI(titlePersonal Dashboard API) app.add_middleware( CORSMiddleware, allow_origins[*], allow_methods[*], allow_headers[*], ) DATA_FILE Path(__file__).parent / data / todos.json DATA_FILE.parent.mkdir(exist_okTrue) def read_todos(): if not DATA_FILE.exists(): return [] return json.loads(DATA_FILE.read_text(encodingutf-8)) def write_todos(todos): DATA_FILE.write_text( json.dumps(todos, ensure_asciiFalse, indent2), encodingutf-8, ) app.get(/api/todos) def list_todos(): return read_todos() app.post(/api/todos) async def add_todo(request: Request): payload await request.json() todos read_todos() todos.append({ id: int(payload.get(id, int(time.time()))), title: payload.get(title, ), done: bool(payload.get(done, False)), }) write_todos(todos) return {ok: True, count: len(todos)} app.put(/api/todos/{todo_id}) def toggle_todo(todo_id: int): todos read_todos() for item in todos: if item[id] todo_id: item[done] not item[done] write_todos(todos) return {ok: True}待办持久化直接采用 JSON 文件。它适合个人终端的轻量场景不需要安装数据库。如果后续待办数量变大或者多人共享再把文件替换成 SQLite 或真实数据库。前端待办逻辑let todos []; async function loadTodos() { todos await fetchJson(/api/todos, []); renderTodos(); } function renderTodos() { const ul document.getElementById(todo-list); ul.innerHTML ; todos.forEach((item) { const li document.createElement(li); li.className item.done ? done : ; const checkbox document.createElement(input); checkbox.type checkbox; checkbox.checked item.done; checkbox.addEventListener(change, () toggleTodo(item.id)); const span document.createElement(span); span.textContent item.title; li.appendChild(checkbox); li.appendChild(span); ul.appendChild(li); }); } document.getElementById(todo-form).addEventListener(submit, async (e) { e.preventDefault(); const input document.getElementById(todo-input); const title input.value.trim(); if (!title) return; await fetch(/api/todos, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id: Date.now(), title, done: false }), }); input.value ; loadTodos(); }); async function toggleTodo(id) { await fetch(/api/todos/${id}, { method: PUT }); loadTodos(); }这里要注意todos.json文件不能被多个进程同时写入否则可能出现数据丢失。个人终端只有一个后端进程问题不大如果以后改成多机共享需要考虑数据库事务。3.4 天气和节假日接入第三方公开数据源天气信息需要从外部服务获取。这里的关键问题不是能不能接入而是接入后如何保证页面稳定。第三方接口可能超时、限流、变更字段、需要 Key这些都不能让末端页面承担。推荐做法是让后端代理天气接口。main.py新增/api/weather由后端调用第三方天气服务做 300 秒缓存再用统一结构返回给前端。第三方服务商变更时只需要改后端一处。WEATHER_API_URL os.getenv(WEATHER_API_URL, ) WEATHER_API_KEY os.getenv(WEATHER_API_KEY, ) WEATHER_CITY os.getenv(WEATHER_CITY, 深圳) weather_cache {data: None, ts: 0} app.get(/api/weather) def get_weather(): now time.time() if weather_cache[data] and now - weather_cache[ts] 300: return weather_cache[data] if not WEATHER_API_URL: return { city: WEATHER_CITY, temp: --, text: 未配置天气服务, humidity: --, } try: resp requests.get( WEATHER_API_URL, params{ key: WEATHER_API_KEY, city: WEATHER_CITY, }, timeout5, ) resp.raise_for_status() data resp.json() # 不同天气服务的字段名不同这里需要按服务商文档调整 weather_cache[data] { city: WEATHER_CITY, temp: data[now][temp], text: data[now][text], humidity: data[now][humidity], } weather_cache[ts] now return weather_cache[data] except Exception: return { city: WEATHER_CITY, temp: --, text: 天气接口异常, humidity: --, }实际项目中选择哪家天气服务以自己网络环境和接口文档为准。有的服务商要求申请应用后拿到 Key有的服务商有免费和付费档位还有字段命名差异落地前要先去服务商文档确认请求参数和返回字段。不要直接把 Key 写进前端代码也不要写进 git 仓库统一通过环境变量注入。前端展示逻辑async function loadWeather() { const data await fetchJson(/api/weather, { city: 未知, temp: --, text: 离线, humidity: --, }); document.getElementById(weather-city).textContent data.city; document.getElementById(weather-temp).textContent ${data.temp}°C; document.getElementById(weather-text).textContent data.text; document.getElementById(weather-humidity).textContent 湿度 ${data.humidity}%; }这里故意把降级数据放在fetchJson的 fallback 参数里而不是在 catch 分支里依赖某个全局变量。只要接口异常页面上就会显示“离线”和--不会出现空白卡片。节假日数据的接入方式和天气类似主要是判断今天是否工作日、最近一个假期还有多少天。可以用开源节假日数据集也可以找提供 HTTP 接口的服务。字段不统一建议也在后端做一次字段映射再返回给前端。3.5 系统监控用 psutil 报告 CPU、内存、磁盘当终端跑在树莓派或迷你主机上时系统状态很有参考价值。/api/system接口返回 CPU、内存、磁盘使用率和主机名app.get(/api/system) def system_status(): return { cpu_percent: psutil.cpu_percent(interval1), memory_percent: psutil.virtual_memory().percent, disk_percent: psutil.disk_usage(/).percent, hostname: os.uname().nodename, time: datetime.now().isoformat(timespecseconds), }前端保持 5 秒刷新一次async function loadSystem() { const data await fetchJson(/api/system, { cpu_percent: --, memory_percent: --, disk_percent: --, hostname: --, }); document.getElementById(cpu).textContent ${data.cpu_percent}%; document.getElementById(memory).textContent ${data.memory_percent}%; document.getElementById(disk).textContent ${data.disk_percent}%; }解析这个接口时有个细节psutil.cpu_percent(interval1)第一次调用会做一次采样但因为缺少上一次参考值通常会返回 0.0。社区常见做法是让后端启动后在后台先做一次采样或者在前端首次加载后等下一次 5 秒轮询再看真实值。图上偶尔出现一次 0%通常不是 bug不要被它误导。最后把静态页面挂载到 FastAPI 上这样浏览器打开http://127.0.0.1:8000时直接看到页面app.mount(/, StaticFiles(directorystatic, htmlTrue), namestatic)完整后端代码跑起来uvicorn main:app --host 0.0.0.0 --port 8000浏览器访问http://127.0.0.1:8000可以看到四张卡片并排展示。4. 让终端开机自启、自动刷新、异常降级4.1 浏览器全屏模式与开机自启页面开发完成后副屏不能每次开机都手动打开浏览器。要让它像一块真正的信息屏一样通电即自动启动并保持全屏。Linux 系统使用 Chromium 的 kiosk 模式可以屏蔽地址栏和所有浏览器周边元素效果最接近“信息终端”。安装 Chromium 后先用命令行验证chromium --kiosk \ --noerrdialogs \ --disable-infobars \ --disable-session-crashed-bubble \ http://127.0.0.1:8000如果使用 systemd可以创建两个服务一个守护后端一个守护浏览器。后端服务/etc/systemd/system/dashboard-api.service[Unit] DescriptionDashboard Backend API Afternetwork.target [Service] WorkingDirectory/opt/dashboard ExecStart/opt/dashboard/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restartalways RestartSec5 Userdash EnvironmentFile/opt/dashboard/.env [Install] WantedBymulti-user.target浏览器服务/etc/systemd/system/dashboard-kiosk.service[Unit] DescriptionDashboard Kiosk Browser Afternetwork-online.target [Service] EnvironmentDISPLAY:0 EnvironmentXAUTHORITY/home/dash/.Xauthority ExecStart/usr/bin/chromium --kiosk --noerrdialogs http://127.0.0.1:8000 Restartalways RestartSec5 Userdash [Install] WantedBygraphical.target启用服务sudo systemctl enable dashboard-api.service sudo systemctl enable dashboard-kiosk.service sudo systemctl start dashboard-api.service sudo systemctl start dashboard-kiosk.service上面的Userdash是示例用户实际项目里要改成运行服务和浏览器的真实用户并且确认该用户有显示服务器的访问权限。如果副屏只是电脑的第二块屏幕没必要引入 systemd直接用操作系统的登录启动脚本更简单。Windows 环境下可以用任务计划程序。先启动 PowerShell
返回列表