使用workbuddy制作一个历史气象灾害预警查询工具

基于历史气象灾害预警接口的前端实践:微信小程序 + H5端实现

本文是一篇纯技术教程,记录如何基于「历史气象灾害预警查询接口(alarmsearch)」,分别用单文件 H5微信小程序实现「按城市 + 日期回看当日灾害预警」的小工具。文中仅讨论接口调用与前端实现,不涉及任何商业推广。


一、背景与接口简介

在保险理赔佐证、农业防灾复盘、城市应急台账、出行安全回顾等场景下,常常需要回答一个问题:"某天某地到底发布过什么气象灾害预警?"

目标接口提供了一个轻量的查询能力:

  • 请求方式GET

  • 核心参数appidappsecretcity(城市名,如 青岛)、dateYYYY-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 小程序运行说明

  1. 用微信开发者工具导入工程,填写自己的 AppID(或测试号)。
  2. 本地调试直连 HTTP 测试接口:开发者工具 → 详情 → 本地设置 → 勾选「不校验合法域名、TLS 版本以及 HTTPS 证书」。
  3. 真机 / 生产 :把 utils/request.jsqueryAlarm 改成 queryByProxy,部署一个 HTTPS 代理(可参考第三节 H5 部署到 HTTPS 站点时的服务端代理转发思路),并把 appid/appsecret 放到服务端环境变量。

五、合规与安全注意事项(发布前自查)

  1. 密钥安全appid/appsecret 属于凭证,演示可放前端,但生产务必收敛到服务端,避免泄露与盗刷。
  2. HTTPS 限制 :小程序 wx.request 只支持 HTTPS;H5 部署到 HTTPS 站点后也无法直接请求 HTTP 接口,两者都需要服务端代理转发。
  3. 跨域 :H5 直连依赖接口返回 Access-Control-Allow-Origin: *;若接口方调整策略,需改走代理。
  4. 数据范围 :接口数据自 2025-07-06 起,早于该日期的查询返回空属正常。
  5. 内容合规:本文仅作技术实现示例,未在文中做任何商业推广、价格展示或外部引流,发布时请保持技术中立,按需补充自己的项目说明即可。

相关推荐
一个处女座的程序猿5 天前
Agent之Harness:WorkBuddy的简介、安装和使用方法、案例应用之详细攻略
agent·workbuddy·harness
VIP_CQCRE7 天前
在 WorkBuddy 里一键接入 Claude、GPT、Gemini、DeepSeek:Ace Data Cloud 让 AI 模型调用更简单
ai·大模型·openai·workbuddy·ace data cloud
Lalolander8 天前
WorkBuddy App 怎么发起任务?输入栏与发送全流程
大数据·人工智能·金蝶erp·workbuddy
Lalolander8 天前
WorkBuddy App 可以连接电脑吗?远程操控桌面端怎么用?
运维·电脑·腾讯云ai代码助手·workbuddy
VIP_CQCRE10 天前
用 Ace Data Cloud 给 WorkBuddy 接入多模型:一个 API Token 打通 GPT、Claude、Gemini、DeepSeek
ai·大模型·openai·workbuddy·ace data cloud
枫彩10 天前
WorkBuddy 做A股自动复盘实战:数据校验、工具调用与定时报告
大数据·网络·数据库·a股·mcp·workbuddy
熊猫钓鱼>_>15 天前
从“串数据“焦虑到 Space 自由,我用 Agent Bucket 智能体桶管游戏素材
开发语言·人工智能·游戏·agent·bucket·workbuddy·space
制造数据与AI践行者老蒋18 天前
MES 工时异常检测:WorkBuddy 自动排查,2 小时干完 2 天的活(附完整脚本)
python数据分析·制造业数字化·数据洞察·workbuddy·mes系统 异常检测·工时分析
workbuddy小能手18 天前
实战:封装一个WorkBuddy Skill,自动拉取腾讯会议AI录音笔的线下沟通上下文
人工智能·ai·workbuddy