基于历史气象灾害预警接口的前端实践:微信小程序 + H5端实现
本文是一篇纯技术教程,记录如何基于「历史气象灾害预警查询接口(alarmsearch)」,分别用单文件 H5 和微信小程序实现「按城市 + 日期回看当日灾害预警」的小工具。文中仅讨论接口调用与前端实现,不涉及任何商业推广。
一、背景与接口简介
在保险理赔佐证、农业防灾复盘、城市应急台账、出行安全回顾等场景下,常常需要回答一个问题:"某天某地到底发布过什么气象灾害预警?"
目标接口提供了一个轻量的查询能力:

-
请求方式 :
GET -
核心参数 :
appid、appsecret、city(城市名,如青岛)、date(YYYY-MM-DD) -
返回结构 :
json{ "iscache": 1, "cityid": "101120201", "list": [ { "type": "高温", "level": "橙色", "title": "山东省发布高温橙色预警", "content": "......", "time": "2026-08-04 06:00" } ] } -
数据范围 :自
2025-07-06起至今(早于该日期的查询会返回空列表)。 -
协议说明 :该接口当前提供的是 HTTP 地址。
安全提示:文中
appid / appsecret为接口方提供的公开测试凭证,仅用于本地联调演示。真实项目请替换为自己的凭证,并放在服务端,切勿把密钥打包进前端代码。
二、整体方案
两个端共用同一个数据源,差异主要在网络层:
-
微信小程序 :
wx.request只支持 HTTPS,而接口是 HTTP,因此生产环境必须经自有 HTTPS 服务端转发(密钥留在服务端,前端只调自己的接口)。本地调试可临时关闭域名校验直连测试。 -
H5(浏览器) :接口返回
Access-Control-Allow-Origin: *,浏览器可直接fetch,无需代理。小程序 ──HTTPS──> 自有服务端代理 ──HTTP──> 预警接口
H5 ──HTTP/HTTPS(CORS *)──> 预警接口
三、H5 版本(单文件,浏览器直接打开)
H5 版本做成一个零依赖、零构建的单 HTML 文件,功能与后文的微信小程序版本对齐:省→市两级联、日期控件、列表配色。
3.1 完整 index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>历史气象预警查询</title>
<style>
:root { --bg:#f2f2f7; --card:#fff; --text:#1c1c1e; --sub:#8e8e93; --line:#e5e5ea; --blue:#007aff; --red:#ff3b30; }
* { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
body { margin:0; background:var(--bg); color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Helvetica Neue",Arial,sans-serif;
-webkit-font-smoothing:antialiased; }
.wrap { max-width:480px; margin:0 auto; min-height:100vh; padding:0 0 48px; }
.header { padding:48px 24px 20px; }
.title { font-size:26px; font-weight:600; }
.subtitle { margin-top:6px; font-size:14px; color:var(--sub); }
.card { margin:12px 16px; background:var(--card); border-radius:14px; overflow:hidden; }
.field { display:flex; align-items:center; min-height:56px; padding:0 16px; }
.label { width:56px; font-size:16px; flex:none; }
.divider { height:1px; background:var(--line); margin-left:72px; }
select, input[type=date] { flex:1; border:none; background:transparent; font-size:16px;
color:var(--blue); text-align:right; outline:none; }
.query-btn { display:flex; align-items:center; justify-content:center; margin:20px 16px 0;
height:50px; background:var(--blue); color:#fff; font-size:17px; border:none; border-radius:12px; cursor:pointer; }
.query-btn[disabled] { opacity:.6; }
.state { margin:16px 28px; font-size:14px; text-align:center; color:var(--sub); }
.state.error { color:var(--red); }
.cache-tip { margin:16px 20px 4px; font-size:13px; color:#aeaeb2; }
.item { margin:14px 16px; padding:16px; background:var(--card); border-radius:14px; }
.item-top { display:flex; align-items:center; }
.badge { color:#fff; font-size:12px; padding:2px 10px; border-radius:999px; margin-right:10px; }
.type { font-size:15px; font-weight:600; }
.time { margin-left:auto; font-size:12px; color:#aeaeb2; }
.item-title { margin:10px 0 6px; font-size:16px; font-weight:600; }
.item-content { font-size:14px; line-height:1.6; color:#636366; }
.note { margin:28px 24px 0; font-size:12px; line-height:1.6; color:#aeaeb2; }
</style>
</head>
<body>
<div class="wrap">
<div class="header">
<div class="title">历史气象预警</div>
<div class="subtitle">按城市与日期,回看当日发布的所有灾害预警</div>
</div>
<div class="card">
<div class="field"><span class="label">省份</span><select id="province"></select></div>
<div class="field"><span class="label">城市</span><select id="city"></select></div>
<div class="divider"></div>
<div class="field"><span class="label">日期</span><input type="date" id="date" /></div>
</div>
<button class="query-btn" id="queryBtn">查询预警</button>
<div id="msg" class="state" style="display:none;"></div>
<div id="result"></div>
<div class="note">
纯前端演示,接口密钥为公开测试凭证;本地双击或经本地服务器打开即可使用(接口支持跨域)。
若部署到 HTTPS 站点,浏览器会拦截 HTTP 请求,需改用服务端代理转发该接口,并把密钥放到服务端。
</div>
</div>
<script>
const CITY_DATA = {
"北京":["北京","东城","西城"],
"上海":["上海","黄浦","长宁","静安","普陀","虹口","杨浦"],
"山东":["济南","青岛","淄博","德州","烟台","潍坊","济宁","泰安","临沂","菏泽","滨州","东营","威海","枣庄","日照","聊城"],
"台湾":["台北","高雄","台中"]
/* 其余省份按相同结构补全 */
};
const LEVEL_COLOR = { "蓝色":"#007aff","黄色":"#ff9500","橙色":"#ff6b00","红色":"#ff3b30" };
function levelColor(l){ return LEVEL_COLOR[l] || "#8e8e93"; }
function fmtDate(d){ const p=n=>String(n).padStart(2,"0"); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}`; }
const provSel=document.getElementById("province"), citySel=document.getElementById("city");
const dateInput=document.getElementById("date"), queryBtn=document.getElementById("queryBtn");
const msgEl=document.getElementById("msg"), resultEl=document.getElementById("result");
const provinces=Object.keys(CITY_DATA);
provinces.forEach(p=>{ const o=document.createElement("option"); o.value=p; o.textContent=p; provSel.appendChild(o); });
function fillCities(prov){ citySel.innerHTML=""; (CITY_DATA[prov]||[]).forEach(c=>{ const o=document.createElement("option"); o.value=c; o.textContent=c; citySel.appendChild(o); }); }
let pIdx=provinces.indexOf("山东"); if(pIdx<0)pIdx=0;
provSel.value=provinces[pIdx]; fillCities(provinces[pIdx]);
if(citySel.querySelector("option[value='青岛']")) citySel.value="青岛";
provSel.addEventListener("change",()=>fillCities(provSel.value));
dateInput.value="2026-08-04"; dateInput.min="2025-01-01"; dateInput.max=fmtDate(new Date());
function showMsg(t,err){ msgEl.textContent=t; msgEl.style.display=t?"block":"none"; msgEl.className="state"+(err?" error":""); }
queryBtn.addEventListener("click",async()=>{
const city=citySel.value, date=dateInput.value;
if(!city){ showMsg("请选择城市",true); return; }
if(!date){ showMsg("请选择日期",true); return; }
resultEl.innerHTML=""; showMsg("查询中...",false); queryBtn.disabled=true;
try{
const url="http://pddfps.tianqiapi.com/free/alarmsearch?appid=55556666&appsecret=12341234"
+"&date="+encodeURIComponent(date)+"&city="+encodeURIComponent(city);
const resp=await fetch(url,{mode:"cors",cache:"no-store"});
if(!resp.ok) throw new Error("接口返回 "+resp.status);
const data=await resp.json();
if(data&&data.errmsg) throw new Error(data.errmsg);
const list=(data&&data.list?data.list:[]).map(it=>({...it,color:levelColor(it.level)}));
if(!list.length){ showMsg("该城市在该日期暂无灾害预警记录",false); return; }
if(data&&data.iscache===1){ const tip=document.createElement("div"); tip.className="cache-tip"; tip.textContent="以下为缓存数据"; resultEl.appendChild(tip); }
list.forEach(it=>{
const card=document.createElement("div"); card.className="item";
card.innerHTML='<div class="item-top"><span class="badge" style="background:'+it.color+'">'+(it.level||"---")+'</span>'
+'<span class="type">'+(it.type||"")+'</span><span class="time">'+(it.time||"")+'</span></div>'
+'<div class="item-title">'+(it.title||"")+'</div><div class="item-content">'+(it.content||"")+'</div>';
resultEl.appendChild(card);
});
showMsg("",false);
}catch(e){
const isNet=/Failed to fetch|NetworkError|Load failed/i.test(e&&e.message||"");
if(isNet) showMsg("请求被浏览器拦截(Failed to fetch)",true);
else showMsg(e&&e.message?e.message:"查询失败,请稍后重试",true);
}finally{ queryBtn.disabled=false; }
});
</script>
</body>
</html>
示例中
CITY_DATA为节选,实际使用时按接口文档补全全部省份即可(小程序端同理)。
四、微信小程序版本
4.1 工程结构
miniprogram/
├── app.js / app.json / app.wxss
├── config.js # 接口地址与测试凭证
├── utils/request.js # 请求层(直连 / 代理 切换)
└── pages/index/
├── index.js # 逻辑:省→市级联、日期、查询、渲染
├── index.wxml
├── index.wxss
└── index.json
4.2 配置 config.js
js
// config.js
const RAW_CITY_LIST = [ "北京","东城","西城","上海","黄浦", /* ......其余城市省略... */ "台中" ]
module.exports = {
// 测试接口地址(HTTP)。生产请改为你自己的 HTTPS 代理地址。
API_BASE: 'http://pddfps.tianqiapi.com/free/alarmsearch',
APPID: '55556666',
APPSECRET: '12341234',
// 去重后的城市列表
CITY_LIST: Array.from(new Set(RAW_CITY_LIST))
}
4.3 请求层 utils/request.js
把"数据来源"收敛到这一个文件,方便在直连与代理之间切换:
js
// utils/request.js
const config = require('../config.js')
// 方式一:直连测试接口(本地调试用,需关闭域名校验)
function queryByDirect(city, date) {
return new Promise((resolve, reject) => {
wx.request({
url: config.API_BASE,
method: 'GET',
data: { appid: config.APPID, appsecret: config.APPSECRET, city, date },
success(res) {
if (res.statusCode === 200 && res.data) resolve(res.data)
else reject(new Error('接口返回异常(statusCode=' + res.statusCode + ')'))
},
fail(err) { reject(new Error('网络请求失败:' + (err.errMsg || '未知错误'))) }
})
})
}
// 方式二:调用自有 HTTPS 代理(生产推荐)
const PROXY_URL = '' // 例如 'https://your-domain.com/api/alarmsearch'
function queryByProxy(city, date) {
return new Promise((resolve, reject) => {
wx.request({
url: PROXY_URL,
method: 'GET',
data: { city, date },
success(res) {
if (res.statusCode === 200 && res.data) resolve(res.data.data || res.data)
else reject(new Error('代理返回异常'))
},
fail(err) { reject(new Error('网络请求失败:' + (err.errMsg || '未知错误'))) }
})
})
}
// 切换入口:默认直连;生产把这里改成 queryByProxy
function queryAlarm(city, date) { return queryByDirect(city, date) }
module.exports = { queryAlarm }
4.4 页面逻辑 pages/index/index.js
城市采用省→市两级联 (multiSelector),默认选中「山东 / 青岛」;日期默认 2026-08-04、最小可选到 2025-01-01。
js
// pages/index/index.js
const { queryAlarm } = require('../../utils/request.js')
// 省 -> 市 级联数据(节选,完整数据见接口文档)
const CITY_DATA = {
"北京": ["北京", "东城", "西城"],
"上海": ["上海", "黄浦", "长宁", "静安", "普陀", "虹口", "杨浦"],
"山东": ["济南", "青岛", "淄博", "德州", "烟台", "潍坊", "济宁", "泰安", "临沂", "菏泽", "滨州", "东营", "威海", "枣庄", "日照", "聊城"],
// ......其余省份省略,结构与上文一致
"台湾": ["台北", "高雄", "台中"]
}
const PROVINCES = Object.keys(CITY_DATA)
function levelColor(level) {
return ({ '蓝色': '#007AFF', '黄色': '#FF9500', '橙色': '#FF6B00', '红色': '#FF3B30' })[level] || '#8E8E93'
}
function fmtDate(d) {
const p = (n) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
}
Page({
data: {
multiRange: [PROVINCES, CITY_DATA[PROVINCES[0]]],
cityValue: [0, 0],
cityText: PROVINCES[0],
cityLabel: PROVINCES[0],
date: '', today: '', list: [], iscache: 0, loading: false, errorMsg: ''
},
onLoad() {
const today = fmtDate(new Date())
let pIdx = PROVINCES.indexOf('山东'); if (pIdx < 0) pIdx = 0
const cities = CITY_DATA[PROVINCES[pIdx]]
let cIdx = cities.indexOf('青岛'); if (cIdx < 0) cIdx = 0
const province = PROVINCES[pIdx], city = cities[cIdx]
this.setData({
date: '2026-08-04', today,
cityValue: [pIdx, cIdx],
multiRange: [PROVINCES, cities],
cityText: city,
cityLabel: province === city ? city : `${province} ${city}`
})
},
// 第一列(省)变化时,联动刷新第二列(市)
onCityColumnChange(e) {
const { column, value } = e.detail
if (column !== 0) return
this.setData({ multiRange: [PROVINCES, CITY_DATA[PROVINCES[value]]], cityValue: [value, 0] })
},
onCityChange(e) {
const [pIdx, cIdx] = e.detail.value
const province = PROVINCES[pIdx]
const city = CITY_DATA[province][cIdx]
this.setData({ cityValue: [pIdx, cIdx], cityText: city, cityLabel: province === city ? city : `${province} ${city}` })
},
onDateChange(e) { this.setData({ date: e.detail.value }) },
async onQuery() {
const { cityText, date } = this.data
const city = cityText.trim()
if (!city) return this.setData({ errorMsg: '请选择城市' })
if (!date) return this.setData({ errorMsg: '请选择日期' })
this.setData({ loading: true, list: [], errorMsg: '', iscache: 0 })
try {
const result = await queryAlarm(city, date)
const list = (result.list || []).map((it) => ({ ...it, color: levelColor(it.level) }))
this.setData({ list, iscache: result.iscache || 0, errorMsg: list.length ? '' : '该城市在该日期暂无灾害预警记录' })
} catch (err) {
this.setData({ errorMsg: (err && err.message) || '查询失败,请稍后重试' })
} finally {
this.setData({ loading: false })
}
}
})
4.5 页面结构 index.wxml
xml
<view class="page">
<view class="header">
<view class="title">历史气象预警</view>
<view class="subtitle">按城市与日期,回看当日发布的所有灾害预警</view>
</view>
<view class="card form">
<view class="field">
<text class="label">城市</text>
<picker class="picker" mode="multiSelector" range="{{multiRange}}" value="{{cityValue}}"
bindchange="onCityChange" bindcolumnchange="onCityColumnChange">
<view class="picker-value">{{cityLabel}}</view>
</picker>
<text class="chevron">›</text>
</view>
<view class="divider"></view>
<view class="field">
<text class="label">日期</text>
<picker class="picker" mode="date" value="{{date}}" start="2025-01-01" end="{{today}}" bindchange="onDateChange">
<view class="picker-value">{{date}}</view>
</picker>
<text class="chevron">›</text>
</view>
</view>
<button class="query-btn" loading="{{loading}}" bindtap="onQuery">
{{loading ? '查询中' : '查询预警'}}
</button>
<view wx:if="{{errorMsg}}" class="state error">{{errorMsg}}</view>
<view wx:if="{{list.length}}" class="result">
<view wx:if="{{iscache === 1}}" class="cache-tip">以下为缓存数据</view>
<view class="card item" wx:for="{{list}}" wx:key="time">
<view class="item-top">
<text class="badge" style="background:{{item.color}}">{{item.level}}</text>
<text class="type">{{item.type}}</text>
<text class="time">{{item.time}}</text>
</view>
<view class="item-title">{{item.title}}</view>
<view class="item-content">{{item.content}}</view>
</view>
</view>
<view wx:if="{{!list.length && !errorMsg && !loading}}" class="state empty">
选择城市与日期,查询当日气象灾害预警
</view>
</view>
4.6 小程序运行说明
- 用微信开发者工具导入工程,填写自己的
AppID(或测试号)。 - 本地调试直连 HTTP 测试接口:开发者工具 → 详情 → 本地设置 → 勾选「不校验合法域名、TLS 版本以及 HTTPS 证书」。
- 真机 / 生产 :把
utils/request.js的queryAlarm改成queryByProxy,部署一个 HTTPS 代理(可参考第三节 H5 部署到 HTTPS 站点时的服务端代理转发思路),并把appid/appsecret放到服务端环境变量。
五、合规与安全注意事项(发布前自查)
- 密钥安全 :
appid/appsecret属于凭证,演示可放前端,但生产务必收敛到服务端,避免泄露与盗刷。 - HTTPS 限制 :小程序
wx.request只支持 HTTPS;H5 部署到 HTTPS 站点后也无法直接请求 HTTP 接口,两者都需要服务端代理转发。 - 跨域 :H5 直连依赖接口返回
Access-Control-Allow-Origin: *;若接口方调整策略,需改走代理。 - 数据范围 :接口数据自
2025-07-06起,早于该日期的查询返回空属正常。 - 内容合规:本文仅作技术实现示例,未在文中做任何商业推广、价格展示或外部引流,发布时请保持技术中立,按需补充自己的项目说明即可。