下面给出一份“可运行的教育示例”:
- 主题紧贴《微尘聚智・平安护航:2023 公安数智化公共服务实践报告》核心思想——
“把散落在群众手里的‘微尘数据’汇聚成智,形成对公共安全的‘护航’能力”。 - 形式为“Web 互动课件”,既能投在教室大屏,也能嵌入微信公众号/警务小程序。
- 前端仅用 HTML+CSS+原生 JS,一行框架都不引,方便警务教官二次改造。
- 后端用 Node.js 写最小化接口,同样可替换为 Java/SpringBoot,逻辑零耦合。
教育目标(可在课件首页一键展开)
- 让群众/学员在 3 分钟交互里直观体会:
“当我随手上传一张占用盲道的照片,公安如何把它汇入‘城市治理一张网’并闭环处置”。 - 把“数智化”翻译成可感知的动画:数据微粒 → 聚智 → 事件分派 → 处置反馈 → 安全指数回升。
- 培养“数据贡献即平安共建”意识,降低“多一事不如少一事”的沉默成本。
文件结构 public-safety-lab/ ├─ index.html // 单页课件(前端) ├─ server.js // 微型后端(Node + Express) ├─ data/ // 模拟数据库(json 文件) └─ README.md // 如何把后端换成 Java
- 前端:index.html(直接双击可预览静态版)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>微尘聚智 · 平安护航——互动体验室</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root{
--c1:#0052d9; --c2:#f5222d; --c3:#52c41a;
--shadow:0 4px 16px rgba(0,0,0,.08);
}
*{box-sizing:border-box;margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif;}
body{background:#f3f5f9;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;}
.card{background:#fff;border-radius:12px;box-shadow:var(--shadow);max-width:800px;width:100%;padding:40px;}
h1{text-align:center;color:var(--c1);margin-bottom:30px;}
.step{display:none;animation:fade .6s;}
.step.active{display:block;}
@keyframes fade{from{opacity:0;transform:translateY(20px);}}
textarea{width:100%;min-height:100px;padding:10px;border:1px solid #d9d9d9;border-radius:4px;font-size:14px;resize:vertical;}
.btn{background:var(--c1);color:#fff;border:none;border-radius:4px;padding:10px 24px;font-size:16px;cursor:pointer;transition:.3s;}
.btn:hover{filter:brightness(1.1);}
.preview{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px;}
.preview img{width:100px;height:100px;object-fit:cover;border-radius:4px;border:1px solid #d9d9d9;}
.bar{background:#e5e5e5;height:20px;border-radius:10px;overflow:hidden;margin:20px 0;position:relative;}
.fill{background:var(--c3);height:100%;width:0%;transition:width .4s;}
.txt-center{text-align:center;margin-top:20px;color:#666;font-size:14px;}
</style>
</head>
<body>
<div class="card">
<h1>🌐 微尘聚智 · 平安护航 互动体验室</h1>
<!-- Step1 群众随手拍 -->
<div class="step active" id="step1">
<h2>① 你发现道路隐患?</h2>
<p style="margin:10px 0;">拍照+描述,即可成为“平安数据”的一粒微尘。</p>
<input type="file" id="uploader" accept="image/*" multiple>
<textarea id="desc" placeholder="请输入具体问题描述,如‘盲道被占用’"></textarea>
<button class="btn" onclick="upload()">提交</button>
<div class="preview" id="preview"></div>
</div>
<!-- Step2 数据汇入 -->
<div class="step" id="step2">
<h2>② 数据汇入城市大脑</h2>
<p>每上传一份有效数据,聚智进度 +1。</p>
<div class="bar"><div class="fill" id="fill"></div></div>
<p class="txt-center">聚智中…<span id="num">0</span> 条有效数据</p>
</div>
<!-- Step3 事件闭环 -->
<div class="step" id="step3">
<h2>③ 处置闭环 & 安全指数回升</h2>
<p>民警已到达现场,问题处理完毕→安全指数回升。</p>
<div class="bar"><div class="fill" id="safeUp"></div></div>
<p class="txt-center">城市实时安全指数:<b id="score">92.3</b></p>
<button class="btn" onclick="location.reload()">再体验一次</button>
</div>
</div>
<script>
const API='http://localhost:3000'; // 后端地址
let fileList=[];
/* 前端压缩+预览 */
document.getElementById('uploader').addEventListener('change',e=>{
const files=[...e.target.files];
const preview=document.getElementById('preview');
preview.innerHTML='';
files.forEach(f=>{
const reader=new FileReader();
reader.onload=ev=>{
const img=new Image();
img.src=ev.target.result;
preview.appendChild(img);
};
reader.readAsDataURL(f);
});
fileList=files;
});
/* 上传&动画 */
async function upload(){
if(!fileList.length||!document.getElementById('desc').value.trim())return alert('请补全照片或描述');
const fd=new FormData();
fileList.forEach(f=>fd.append('pics',f));
fd.append('desc',document.getElementById('desc').value);
await fetch(API+'/report',{method:'POST',body:fd});
/* 进入 Step2 */
['step1','step2','step3'].forEach(id=>document.getElementById(id).classList.remove('active'));
document.getElementById('step2').classList.add('active');
animateBar();
}
/* 进度条动画 + 数字增长 */
function animateBar(){
let n=0;
const fill=document.getElementById('fill');
const num=document.getElementById('num');
const fakeTotal=10+Math.floor(Math.random()*10); // 模拟聚智所需条数
const t=setInterval(async()=>{
n++;
fill.style.width=(n/fakeTotal*100)+'%';
num.textContent=n;
if(n>=fakeTotal){
clearInterval(t);
await fetch(API+'/close'); // 通知后端“已聚智”
showClose();
}
},400);
}
/* 处置闭环动画 */
function showClose(){
['step1','step2','step3'].forEach(id=>document.getElementById(id).classList.remove('active'));
document.getElementById('step3').classList.add('active');
let w=0;
const bar=document.getElementById('safeUp');
const score=document.getElementById('score');
const start=92.3;
const end=Math.min(100,start+3.2);
const t=setInterval(()=>{
w+=2;
bar.style.width=w+'%';
score.textContent=(start+(end-start)*(w/100)).toFixed(1);
if(w>=100)clearInterval(t);
},60);
}
</script>
</body>
</html>
- 后端:server.js(Node ≥ 14,仅 60 行)
const express=require('express');
const multer=require('multer');
const fs=require('fs');
const path=require('path');
const app=express();
const PORT=3000;
/* 允许前端跨域 */
app.use((req,res,next)=>{res.header('Access-Control-Allow-Origin','*');next();});
/* 静态数据目录 */
const DATA_DIR=path.join(__dirname,'data');
if(!fs.existsSync(DATA_DIR))fs.mkdirSync(DATA_DIR);
/* 文件上传中间件 */
const upload=multer({dest:DATA_DIR});
/* 接口 1:群众上报 */
app.post('/report',upload.array('pics'),(req,res)=>{
const {desc}=req.body;
const pics=req.files.map(f=>f.path);
const id=Date.now();
const json={id,desc,pics,time:new Date(),status:'received'};
fs.writeFileSync(path.join(DATA_DIR,`${id}.json`),JSON.stringify(json));
console.log('[上报]',id,desc);
res.json({code:0,id});
});
/* 接口 2:聚智完成(前端 Step3 通知) */
app.post('/close',(req,res)=>{
/* 这里可以调用真实警务中台 API,演示仅改状态 */
const files=fs.readdirSync(DATA_DIR).filter(f=>f.endsWith('.json'));
const latest=files.sort().reverse()[0];
if(latest){
const file=path.join(DATA_DIR,latest);
const data=JSON.parse(fs.readFileSync(file));
data.status='closed';
data.closeTime=new Date();
fs.writeFileSync(file,JSON.stringify(data));
console.log('[闭环]',data.id);
}
res.json({code:0});
});
app.listen(PORT,()=>console.log(`API 已启动 http://localhost:${PORT}`));
- 运行步骤(2 分钟)
# 1. 装好 Node
node -v # ≥14 即可
# 2. 一键依赖 & 启动
npm install express multer
node server.js
# 3. 前端
# 方法 A:直接用 LiveServer 打开 index.html
# 方法 B:把 index.html 丢到 Nginx/警务已有静态服务器
- 如何把后端换成 Java(SpringBoot)——核心逻辑对照
-
接口
/report
→ 用@PostMapping("/report")+@RequestPart("pics") MultipartFile[] pics
→ 文件存到 MinIO/阿里云 OSS,返回id。 -
接口
/close
→ 根据最新 ID 把状态改 closed,可写 MySQL,也可调已有警务事件处置中台。 -
跨域
→ 加@CrossOrigin(origins = "*")或网关统一处理。
- 课堂/展厅使用脚本(3 分钟版)
① 大屏打开 index.html → ② 学员手机扫码(同页)→ ③ 拍照上传 → ④ 大屏实时看进度条涨 → ⑤ 安全指数回升动画结束,教官总结:
“你的一次随手拍,就是城市治理的‘事务消息’;
当消息被消费(民警处置),系统整体安全指数 Rebalance,
这就是公安数智化‘微尘聚智’的真实含义。”
- 可继续扩展的“教育彩蛋”
- 把聚智所需条数改成“真实在线人数”,让全班一起上传,体验“并发写”与“流量突刺”。
- 把进度条换成中国地图热力图,展示“隐患分布”→“处置后消失”。
- 加入 Websocket,后端每闭环一条就推送,让大屏像“股票行情”一样实时跳动。
- 让学员用 Postman 直接调
/report,理解“接口即服务”——哪怕不用前端页面,数据照样能进来。