1. 赣州智慧城市大屏为什么选 Vue 3 ECharts Express SQLite做智慧城市大屏最怕两件事一是图表堆了一屏但数据是写死的二是后端接口一改前端就崩。我这次用 Vue 3 ECharts Express SQLite 搭了一套赣州智慧城市数据可视化平台前端负责大屏渲染和数据管理页后端用 Express 提供聚合接口SQLite 存 7 张业务表首次启动自动写入演示数据。整套东西跑起来只需要两个终端适合想快速落地一个可演示、可增删改查的大屏项目的同学。先说清楚这套东西是什么、能做什么、适合谁。它是一个前后端分离的城市数据可视化平台大屏首页展示赣州市基本信息、区县 GDP、产业结构、交通、环境、人口、旅游等指标数据管理页可以对任意业务表做查询、新增、编辑、删除、批量删除、重置初始数据还支持 Excel/CSV 导入导出。适合三类人一是要做课程设计或毕业设计的学生二是想给客户快速出可视化 Demo 的开发者三是想练手 Vue 3 组合式 API ECharts 封装 Express 接口设计的工程师。技术栈分工很明确。前端用 Vue 3 Vite ECharts Element Plus Vue Router后端用 Express better-sqlite3数据库文件放在server/data/ganzhou.db。为什么选 better-sqlite3 而不是 sqlite3因为它是同步 API写起来更直观配合 Express 做 CRUD 几乎不用处理回调地狱本地开发体验好很多。数据源支持两种sqlite 和 sheetsheet 对应server/data/sheets/*.json大屏顶部和数据管理页都能切换数据源切换后图表和地图会读取对应源的数据。项目目录结构大致是这样你可以照着建ganzhou-dashboard/ ├── server/ │ ├── data/ │ │ ├── ganzhou.db │ │ └── sheets/ │ │ ├── city_info.json │ │ ├── districts.json │ │ └── ... │ ├── routes/ │ │ ├── dashboard.js │ │ ├── data.js │ │ └── importExport.js │ ├── db.js │ └── index.js └── client/ ├── src/ │ ├── views/ │ │ ├── Dashboard.vue │ │ └── Manage.vue │ ├── components/ │ │ └── charts/ │ ├── api/ │ └── main.js └── vite.config.js7 张业务表分别是city_info城市基本信息、districts区县信息、economy历年经济指标、traffic交通基础设施、environment空气质量等环境数据、tourism旅游景点、population_trend人口与城镇化趋势。演示数据是公开统计口径的近似整理值用于可视化展示不是正式官方报表这点在项目说明里要写清楚避免误导。大屏的核心难点不在单个图表而在布局和自适应。ECharts 默认是固定尺寸渲染窗口一缩放就糊或者错位。我的做法是封装一个useEcharts组合式函数统一处理实例创建、resize 监听和销毁图表组件只负责传 option。这样大屏里十几个图表不会各写各的 resize 逻辑。2. TaoToken 前置给大屏项目接入模型能力大屏做完之后很多同学会想加一点智能的东西比如让模型根据当前数据生成一段解读文案或者做一个自然语言查询数据的入口。这时候就需要一个稳定的模型调用通道。我用的是 TaoToken官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。先说清楚它在项目里的定位TaoToken 提供的是模型调用能力不是替代你的 Express 后端也不是替代 ECharts。你的数据还是存在 SQLite 里图表还是 ECharts 渲染TaoToken 只是在你需要用自然语言问数据或自动生成数据解读时提供一个可调用的模型接口。把它理解成项目里的一个外部服务就行。接入前需要准备三样东西这三件套在任何模型接入场景里都通用Base URL、API Key、Model ID。Base URL 填https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面创建Model ID 根据你要用的模型填。这三个值建议放在后端的环境变量里不要写死在前端代码里否则 Key 会暴露在浏览器里。具体操作路径先打开 https://taotoken.net/api-keys 创建 Key然后到 https://taotoken.net/doc 看接口文档确认请求格式。如果你要做的是长期编码或 Agent 类任务可以看 https://taotoken.net/coding-plan 如果只是想先验证模型能不能通用 https://taotoken.net/chat 直接对话测试最快。在 Express 里接入的方式很简单加一个路由用 fetch 或 axios 转发请求。注意不要在浏览器端直接调模型接口一是 Key 会泄露二是跨域和配额都不好控制。正确做法是前端调你自己的/api/ai/interpret后端再去调 TaoToken把结果返回给前端。这样 Key 只存在服务端前端拿到的只是文本结果。这里有个容易踩的坑很多人把模型调用写成同步阻塞导致大屏加载时卡住。正确做法是模型解读做成按需触发比如用户点生成解读按钮才调或者后端做缓存同一份数据短时间内不重复请求。大屏首屏渲染不应该依赖模型接口否则模型一慢整个大屏就白屏。还有一个细节模型返回的文本要经过处理再展示。直接塞进 DOM 有 XSS 风险用textContent或者做转义。如果返回的是 Markdown用轻量解析库渲染别用v-html直接怼。3. 可复制配置Express SQLite 接口与 ECharts 封装这一节给你可以直接复制的配置片段。先看后端数据库初始化server/db.js// server/db.js const Database require(better-sqlite3); const path require(path); const fs require(fs); const dbPath path.join(__dirname, data, ganzhou.db); const db new Database(dbPath); db.pragma(journal_mode WAL); function initTables() { db.exec( CREATE TABLE IF NOT EXISTS city_info ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, area REAL, population REAL, gdp REAL ); CREATE TABLE IF NOT EXISTS districts ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, gdp REAL, industry TEXT ); CREATE TABLE IF NOT EXISTS economy ( id INTEGER PRIMARY KEY AUTOINCREMENT, year INTEGER, gdp REAL, growth REAL ); CREATE TABLE IF NOT EXISTS traffic ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT, mileage REAL, year INTEGER ); CREATE TABLE IF NOT EXISTS environment ( id INTEGER PRIMARY KEY AUTOINCREMENT, month TEXT, aqi INTEGER, pm25 REAL ); CREATE TABLE IF NOT EXISTS tourism ( id INTEGER PRIMARY KEY AUTOINCREMENT, spot TEXT, visitors REAL, revenue REAL ); CREATE TABLE IF NOT EXISTS population_trend ( id INTEGER PRIMARY KEY AUTOINCREMENT, year INTEGER, total REAL, urban_rate REAL ); ); } function seedIfEmpty() { const count db.prepare(SELECT COUNT(*) AS c FROM city_info).get().c; if (count 0) { db.prepare(INSERT INTO city_info (name, area, population, gdp) VALUES (?, ?, ?, ?)) .run(赣州市, 39379, 898, 4523); // 其余表按同样方式写入演示数据 } } initTables(); seedIfEmpty(); module.exports db;再看 Express 入口和聚合接口server/index.js// server/index.js const express require(express); const cors require(cors); const db require(./db); const app express(); app.use(cors()); app.use(express.json()); app.get(/api/dashboard, (req, res) { const source req.query.source || sqlite; if (source sheet) { const sheets require(./data/sheets); return res.json(sheets.getDashboard()); } const cityInfo db.prepare(SELECT * FROM city_info LIMIT 1).get(); const districts db.prepare(SELECT * FROM districts).all(); const economy db.prepare(SELECT * FROM economy ORDER BY year).all(); const traffic db.prepare(SELECT * FROM traffic).all(); const environment db.prepare(SELECT * FROM environment).all(); const tourism db.prepare(SELECT * FROM tourism).all(); const population db.prepare(SELECT * FROM population_trend ORDER BY year).all(); res.json({ cityInfo, districts, economy, traffic, environment, tourism, population }); }); app.get(/api/tables, (req, res) { const tables db.prepare( SELECT name FROM sqlite_master WHERE typetable AND name NOT LIKE sqlite_% ).all(); res.json(tables.map(t t.name)); }); app.get(/api/data/:table, (req, res) { const { table } req.params; const page parseInt(req.query.page) || 1; const size parseInt(req.query.size) || 20; const offset (page - 1) * size; const rows db.prepare(SELECT * FROM ${table} LIMIT ? OFFSET ?).all(size, offset); const total db.prepare(SELECT COUNT(*) AS c FROM ${table}).get().c; res.json({ rows, total, page, size }); }); app.listen(3001, () console.log(server on 3001));注意:table这种动态表名有 SQL 注入风险生产环境要做白名单校验只允许 7 张业务表。演示项目里可以先用但心里要有数。前端 ECharts 封装client/src/components/charts/useEcharts.js// client/src/components/charts/useEcharts.js import { ref, onMounted, onBeforeUnmount, watch } from vue; import * as echarts from echarts; export function useEcharts(optionRef) { const chartRef ref(null); let chart null; function init() { if (!chartRef.value) return; chart echarts.init(chartRef.value); chart.setOption(optionRef.value); } function resize() { chart chart.resize(); } onMounted(() { init(); window.addEventListener(resize, resize); }); onBeforeUnmount(() { window.removeEventListener(resize, resize); chart chart.dispose(); chart null; }); watch(optionRef, (val) { chart chart.setOption(val, true); }, { deep: true }); return { chartRef }; }大屏组件里这样用template div refchartRef classchart-box/div /template script setup import { ref, computed } from vue; import { useEcharts } from ./useEcharts; const props defineProps({ data: Array }); const option computed(() ({ tooltip: { trigger: axis }, xAxis: { type: category, data: props.data.map(d d.name) }, yAxis: { type: value }, series: [{ type: bar, data: props.data.map(d d.gdp) }] })); const { chartRef } useEcharts(option); /script style scoped .chart-box { width: 100%; height: 100%; } /style这套封装的好处是每个图表组件只管 optionresize 和销毁统一处理大屏里放十几个图表也不会互相干扰。4. 验证请求本地启动与数据联调配置写完接下来验证能不能跑通。分三步装依赖、起后端、起前端。第一步装依赖cd server npm install cd ../client npm install第二步启动后端端口 3001cd server npm run dev启动后你应该看到server on 3001。这时候先别急着开前端用 curl 验证接口curl http://localhost:3001/api/dashboard?sourcesqlite正常返回是一坨 JSON包含 cityInfo、districts、economy 等字段。如果返回空对象或者报错先看 SQLite 文件有没有生成server/data/ganzhou.db存在说明初始化成功。再验证分页查询curl http://localhost:3001/api/data/districts?page1size5返回{ rows: [...], total: 11, page: 1, size: 5 }这种结构就对了。第三步新开一个终端启动前端端口 5173cd client npm run dev浏览器访问http://localhost:5173大屏首页是/数据管理页是/manage。大屏顶部有数据源切换切到 sheet 后图表数据应该变成 JSON 文件里的内容。数据管理页选一张表试试新增一条、编辑、删除、批量删除再点重置初始数据看能不能恢复。联调时重点看两个地方一是 Network 面板里/api/dashboard的返回结构和你前端解构的字段是否一致二是切换数据源后请求参数source有没有正确带上。我见过最常见的联调失败是前端写死了sourcesqlite切换按钮点了没反应其实是请求没带参数。导入导出也验证一下。数据管理页导出 Excel看文件能不能打开再改几个单元格导入回去看数据有没有覆盖。导入支持中文列名或英文字段名表头匹配不上会报错这个在文档里要提示用户。如果你要接模型解读后端加一个路由app.post(/api/ai/interpret, async (req, res) { const { data } req.body; const resp await fetch(https://taotoken.net/api/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${process.env.TAOTOKEN_KEY} }, body: JSON.stringify({ model: process.env.MODEL_ID, messages: [{ role: user, content: 根据以下数据写一段解读${JSON.stringify(data)} }] }) }); const json await resp.json(); res.json({ text: json.choices?.[0]?.message?.content || }); });前端点按钮调这个接口拿到文本展示。Key 和 Model ID 放.env别提交到仓库。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节把真实会遇到的报错列出来对照着查。401 Unauthorized。这个基本是 Key 的问题。三种可能Key 没填、Key 填错、Key 前面多了Bearer又重复加了。检查你的请求头正确格式是Authorization: Bearer sk-xxx如果你在环境变量里已经写了Bearer代码里就别再加。还有一种情况是 Key 被禁用或额度用完去控制台确认状态。local proxy failed。这个报错通常出现在你本地起了代理或者请求地址写错的时候。先确认 Base URL 是https://taotoken.net/api不要多加斜杠或者写成https://taotoken.net/api/v1又在代码里拼/v1导致路径变成/api/v1/v1/...。另外检查你的开发环境有没有配置全局代理有时候系统代理会拦截本地请求把localhost和127.0.0.1加到 NO_PROXY 里。reading choices或Cannot read properties of undefined (reading choices)。这个说明你拿到的响应里没有choices字段通常是请求失败了但你没检查状态码就直接读json.choices。正确写法是先判断resp.ok不 ok 就把resp.text()打出来看真实错误。常见原因是请求体格式不对比如messages写成了字符串或者model字段为空。OAuth 相关报错。如果你用的是 Claude Code 这类工具可能会遇到 OAuth 认证失败。这类工具通常需要配置 Base URL、API Key、Model ID 三件套缺一个都会报认证错误。检查配置文件里这三项是否都填了Base URL 是不是https://taotoken.net/apiModel ID 是不是控制台里显示的完整名称。如果是 Codex 的auth.json确认字段名和格式跟文档一致别自己改字段名。图表不显示但接口有数据。这个不是接口问题是 ECharts 容器高度为 0。大屏布局里如果父容器没给高度图表初始化时拿不到尺寸就渲染不出来。给图表容器显式设置height: 100%并且父级有确定高度或者用chart.resize()在数据更新后手动触发。切换数据源后图表没变。检查请求有没有带source参数以及后端 sheet 分支有没有正确读取 JSON 文件。如果 JSON 文件路径不对require会报错但你可能没看后端日志。养成看后端终端的习惯报错都在那里。导入 Excel 报错。检查表头是否匹配中文列名和英文字段名都支持但拼写要一致。另外.xls老格式和.xlsx新格式解析库不同确认你装的库支持你上传的格式。6. 继续把大屏做扎实从演示到可维护这套项目跑通之后下一步可以往几个方向做扎实。第一是数据源抽象现在 sqlite 和 sheet 是两套分支可以抽成统一的 repository 接口切换时只换实现不换调用。第二是图表配置抽离把每个图表的 option 写成独立模块大屏组件只负责布局和数据注入这样改图表不用动组件逻辑。第三是加一层简单的缓存/api/dashboard这种聚合接口数据变化不频繁可以缓存几十秒减少 SQLite 查询压力。如果你要接模型能力做数据解读建议把调用封装成独立服务加上超时和降级。模型接口慢的时候不能拖垮大屏超时就返回默认文案。Key 和 Model ID 用环境变量管理本地开发用.env部署时用平台的环境变量配置。最后提醒一句演示数据是近似整理值正式项目要接真实数据源并且做好数据校验和权限控制。数据管理页的增删改查接口在生产环境必须加鉴权不能裸奔。动态表名一定要做白名单只允许预定义的 7 张表否则 SQL 注入风险很大。这套 Vue 3 ECharts Express SQLite 的组合胜在轻量和可复制。你不需要买服务器、不需要配复杂的环境本地两个终端就能跑起来一个完整的大屏平台。把接口和图表封装好之后换城市、换指标只是改数据和 option 的事骨架不用动。