Joe主题集成小渡API(天气+一言)完整教程
Joe主题集成小渡API(天气+一言)完整教程
2026-03-13 0 评论 18 阅读 0 点赞

Joe主题集成小渡API(天气+一言)完整教程

heylie
2026-03-13 / 0 评论 / 18 阅读 / 正在检测是否收录...

功能特性

  • ✅ 实时天气显示(自动定位IP城市)
  • ✅ 每日一言(侧边栏)
  • ✅ 博主栏座右铭动态一言
  • API Key 后端代理隐藏(防泄漏)
  • ✅ 只需配置一个 Key,三功能通用

第一步:修改后台配置

文件: functions.php

位置: 第 569-596 行(侧栏设置区域)

操作: 替换原有的天气配置为新的通用配置

/* --------------------------------------- */
$JDwoApiKey = new Typecho_Widget_Helper_Form_Element_Text(
  'JDwoApiKey',
  NULL,
  NULL,
  '小渡API Key - 通用',
  '介绍:用于天气栏和一言功能的小渡API密钥 <br/>
   注意:填写时务必填写正确!不填写则不会显示天气和一言<br />
   免费申请地址:<a href="https://openapi.dwo.cc/" target="_blank">openapi.dwo.cc</a><br />
   说明:此Key同时用于天气查询、一言接口和博主栏座右铭,只需要配置这一个即可'
);
$JDwoApiKey->setAttribute('class', 'joe_content joe_aside');
$form->addInput($JDwoApiKey);
/* --------------------------------------- */
$JAside_Weather_Status = new Typecho_Widget_Helper_Form_Element_Select(
  'JAside_Weather_Status',
  array(
    'off' => '关闭(默认)',
    'on' => '开启'
  ),
  'off',
  '是否开启实时天气栏 - PC',
  '介绍:用于控制是否显示实时天气栏 <br />
   注意:需要先填写上方的小渡API Key才会生效'
);
$JAside_Weather_Status->setAttribute('class', 'joe_content joe_aside');
$form->addInput($JAside_Weather_Status->multiMode());
/* --------------------------------------- */
$JAside_Yiyan_Status = new Typecho_Widget_Helper_Form_Element_Select(
  'JAside_Yiyan_Status',
  array(
    'off' => '关闭(默认)',
    'on' => '开启'
  ),
  'off',
  '是否开启一言栏 - PC',
  '介绍:用于控制是否显示一言(每日一句)栏 <br />
   注意:需要先填写上方的小渡API Key才会生效'
);
$JAside_Yiyan_Status->setAttribute('class', 'joe_content joe_aside');
$form->addInput($JAside_Yiyan_Status->multiMode());

同时修改博主栏座右铭说明(第 488-500 行):

$JAside_Author_Motto = new Typecho_Widget_Helper_Form_Element_Textarea(
  'JAside_Author_Motto',
  NULL,
  "有钱终成眷属,没钱亲眼目睹",
  '博主栏座右铭(一言)- PC/WAP',
  '介绍:用于修改博主栏的座右铭(一言) <br />
   注意:如果配置了上方的小渡API Key,此项将失效,自动使用小渡API获取动态一言 <br />
   格式:可以填写多行也可以填写一行,填写多行时,每次随机显示其中的某一条 <br />
   其他:未配置小渡API Key时,将显示此处设置的内容'
);

第二步:添加后端代理接口

文件: core/route.php

位置: 文件末尾(第 413 行后)

操作: 添加以下 3 个代理函数

/* 小渡API代理 - 获取一言 */
function _getDwoYiyan($self)
{
    $self->response->setStatus(200);
    $apiKey = Helper::options()->JDwoApiKey;
    
    if (empty($apiKey)) {
        return $self->response->throwJson(array("error" => "未配置API Key"));
    }
    
    $url = "https://openapi.dwo.cc/api/yi?type=json&ckey=" . urlencode($apiKey);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode == 200 && $response) {
        $self->response->throwJson(json_decode($response, true));
    } else {
        $self->response->throwJson(array("error" => "请求失败", "content" => "生活不止眼前的苟且,还有诗和远方。"));
    }
}

/* 小渡API代理 - 获取IP定位 */
function _getDwoIp($self)
{
    $self->response->setStatus(200);
    $apiKey = Helper::options()->JDwoApiKey;
    $ip = $self->request->ip;
    
    if (empty($apiKey)) {
        return $self->response->throwJson(array("error" => "未配置API Key"));
    }
    
    // 判断IPv6
    $isIPv6 = strpos($ip, ':') !== false;
    $apiUrl = $isIPv6 ? 
        "https://openapi.dwo.cc/api/ipv6?ip=" . urlencode($ip) . "&ckey=" . urlencode($apiKey) :
        "https://openapi.dwo.cc/api/ip?ip=" . urlencode($ip) . "&ckey=" . urlencode($apiKey);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode == 200 && $response) {
        $data = json_decode($response, true);
        
        // 统一返回格式:提取城市名称(处理IPv4和IPv6不同格式)
        $cityName = '未知';
        if ($isIPv6) {
            // IPv6 格式:data.city
            if (!empty($data['data']['city'])) {
                $cityName = $data['data']['city'];
            }
        } else {
            // IPv4 格式:data.data.city_name
            if (!empty($data['data']['data']['city_name'])) {
                $cityName = $data['data']['data']['city_name'];
            }
        }
        
        // 移除"市"后缀,天气API可能不需要
        $cityName = str_replace(['市', '县', '区'], '', $cityName);
        
        $self->response->throwJson(array(
            "success" => true,
            "city_name" => $cityName,
            "is_ipv6" => $isIPv6,
            "raw_data" => $data
        ));
    } else {
        $self->response->throwJson(array("error" => "请求失败", "city_name" => "未知"));
    }
}

/* 小渡API代理 - 获取天气 */
function _getDwoWeather($self)
{
    $self->response->setStatus(200);
    $apiKey = Helper::options()->JDwoApiKey;
    $location = $self->request->location;
    
    if (empty($apiKey)) {
        return $self->response->throwJson(array("error" => "未配置API Key"));
    }
    
    if (empty($location)) {
        return $self->response->throwJson(array("error" => "缺少城市参数"));
    }
    
    $url = "https://openapi.dwo.cc/api/weather_xz?location=" . urlencode($location) . "&language=zh-Hans&unit=c&ckey=" . urlencode($apiKey);
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    if ($httpCode == 200 && $response) {
        $self->response->throwJson(json_decode($response, true));
    } else {
        $self->response->throwJson(array("error" => "请求失败"));
    }
}

第三步:注册路由

文件: core/core.php

位置: 第 69-72 行(在 article_filing case 后面)

操作: 添加路由注册

      case 'article_filing':
        _getArticleFiling($self);
        break;
      case 'dwo_yiyan':
        _getDwoYiyan($self);
        break;
      case 'dwo_ip':
        _getDwoIp($self);
        break;
      case 'dwo_weather':
        _getDwoWeather($self);
        break;
    };

第四步:修改侧边栏显示

文件: public/aside.php

4.1 修改博主栏座右铭部分(第 7 行)

替换为:

      <p class="motto joe_motto">
        <?php if (!empty($this->options->JDwoApiKey)) : ?>
          <span class="dwo-motto">加载中...</span>
        <?php else : ?>
          <?php echo $this->options->JAside_Author_Motto ? $this->options->JAside_Author_Motto() : '有钱终成眷属,没钱亲眼目睹'; ?>
        <?php endif; ?>
      </p>

4.2 替换天气模块(第 181-195 行)

替换为:

  <?php if (!empty($this->options->JDwoApiKey)) : ?>
    <style>
    .joe-dwo-weather, .joe-dwo-yiyan {
      background: var(--background) !important;
      backdrop-filter: none !important;
    }
    .joe-dwo-weather .weather-item {
      display: flex;
      justify-content: space-between;
      padding: 10px 0;
      border-bottom: 1px solid var(--classA);
      font-size: 14px;
      color: var(--theme);
    }
    .joe-dwo-weather .weather-item:last-child {
      border-bottom: none;
    }
    .joe-dwo-weather .weather-item span {
      color: var(--minor);
      font-weight: 500;
    }
    .joe-dwo-weather .loading, .joe-dwo-yiyan .loading {
      color: var(--minor);
      font-size: 14px;
      text-align: center;
      padding: 15px 0;
    }
    .joe-dwo-yiyan .yiyan-content {
      font-size: 14px;
      line-height: 1.8;
      text-align: center;
      padding: 15px 10px;
      font-style: italic;
      color: var(--theme);
      font-weight: 500;
    }
    </style>
    
    <?php if ($this->options->JAside_Weather_Status == 'on') : ?>
    <section class="joe_aside__item joe-dwo-weather">
      <div class="joe_aside__item-title">
        <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
          <path d="M773.12 757.76h-79.872c-15.36 0-29.696-15.36-29.696-29.696s15.36-29.696 29.696-29.696h79.872c100.352 0 180.224-79.872 180.224-180.224S873.472 337.92 773.12 337.92c-25.6 0-50.176 5.12-74.752 15.36-10.24 5.12-20.48 5.12-25.6 0-10.24-5.12-15.36-15.36-15.36-20.48-15.36-100.352-100.352-175.104-200.704-175.104C346.112 155.648 256 245.76 250.88 356.352c0 15.36-10.24 29.696-29.696 29.696-79.872 5.12-145.408 74.752-145.408 160.768 0 90.112 70.656 160.768 160.768 160.768h75.776c15.36 0 29.696 15.36 29.696 29.696S326.656 768 311.296 768h-79.872C110.592 757.76 10.24 662.528 10.24 541.696c0-105.472 75.776-195.584 175.104-216.064 15.36-130.048 130.048-235.52 266.24-235.52 120.832 0 225.28 79.872 256 195.584 20.48-5.12 45.056-10.24 65.536-10.24 135.168 1.024 240.64 111.616 240.64 241.664S903.168 757.76 773.12 757.76z" />
          <path d="M437.248 933.888c-10.24 0-15.36-5.12-20.48-10.24-10.24-10.24-10.24-29.696 0-45.056l79.872-79.872h-60.416c-10.24 0-25.6-5.12-29.696-20.48-5.12-10.24 0-24.576 5.12-34.816l130.048-130.048c10.24-10.24 29.696-10.24 45.056 0 10.24 10.24 10.24 29.696 0 45.056L512 742.4h55.296c10.24 0 24.576 5.12 29.696 20.48 5.12 10.24 0 24.576-5.12 34.816L461.824 928.768c-10.24 5.12-20.48 5.12-24.576 5.12z" />
        </svg>
        <span class="text">实时天气</span>
        <span class="line"></span>
      </div>
      <div class="joe_aside__item-contain" id="dwo-weather-content">
        <div class="loading">加载中...</div>
      </div>
    </section>
    <?php endif; ?>
    
    <?php if ($this->options->JAside_Yiyan_Status == 'on') : ?>
    <section class="joe_aside__item joe-dwo-yiyan">
      <div class="joe_aside__item-title">
        <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
          <path d="M512 938.667A426.667 426.667 0 0 1 85.333 512a421.12 421.12 0 0 1 131.2-306.133 58.88 58.88 0 0 1 42.667-16.64c33.28 1.066 58.027 28.16 84.267 56.96 7.893 8.533 19.626 21.333 28.373 29.013a542.933 542.933 0 0 0 24.533-61.867c18.134-52.266 35.414-101.76 75.307-121.6 55.04-27.733 111.573 37.974 183.253 121.6 16.214 18.774 38.614 44.8 53.547 59.52 1.707-4.48 3.2-8.96 4.48-12.373 8.533-24.32 18.987-54.613 51.2-61.653a57.813 57.813 0 0 1 55.68 20.053A426.667 426.667 0 0 1 512 938.667zM260.693 282.453A336.64 336.64 0 0 0 170.667 512a341.333 341.333 0 1 0 614.826-203.733 90.24 90.24 0 0 1-42.666 50.56 68.267 68.267 0 0 1-53.547 1.706c-25.6-9.173-51.627-38.4-99.2-93.226a826.667 826.667 0 0 0-87.253-91.734 507.733 507.733 0 0 0-26.24 64c-18.134 52.267-35.414 101.76-75.947 119.254-48.853 21.333-88.32-21.334-120.107-56.96-5.76-4.694-13.226-13.014-19.84-19.414z" />
        </svg>
        <span class="text">每日一言</span>
        <span class="line"></span>
      </div>
      <div class="joe_aside__item-contain" id="dwo-yiyan-content">
        <div class="loading">加载中...</div>
      </div>
    </section>
    <?php endif; ?>
    
    <script>
    (function() {
      const PROXY_API = '<?php echo Helper::options()->index; ?>/joe/api';
      
      function getClientIp() {
        return fetch('https://ipapi.co/json/').then(r => r.json());
      }
      
      <?php if ($this->options->JAside_Weather_Status == 'on') : ?>
      async function loadWeather() {
        const dom = document.getElementById('dwo-weather-content');
        if (!dom) return;
        try {
          const ipData = await getClientIp();
          const clientIp = ipData.ip;
          
          const locRes = await fetch(PROXY_API + '?routeType=dwo_ip&ip=' + encodeURIComponent(clientIp));
          const locData = await locRes.json();
          const cityName = locData.city_name ? locData.city_name : '未知';
          
          const weatherRes = await fetch(PROXY_API + '?routeType=dwo_weather&location=' + encodeURIComponent(cityName));
          const result = await weatherRes.json();
          
          if (result.results && result.results[0] && result.results[0].now) {
            const now = result.results[0].now;
            dom.innerHTML = `
              <div class="weather-item"><span>城市</span>${cityName}</div>
              <div class="weather-item"><span>天气</span>${now.text}</div>
              <div class="weather-item"><span>温度</span>${now.temperature}℃</div>
            `;
          } else {
            dom.innerHTML = '<div class="loading">暂无天气数据</div>';
          }
        } catch (e) {
          console.error('天气加载失败:', e);
          dom.innerHTML = '<div class="loading">加载失败</div>';
        }
      }
      <?php endif; ?>
      
      <?php if ($this->options->JAside_Yiyan_Status == 'on') : ?>
      async function loadYiyan() {
        const dom = document.getElementById('dwo-yiyan-content');
        if (!dom) return;
        try {
          const res = await fetch(PROXY_API + '?routeType=dwo_yiyan');
          const result = await res.json();
          
          if (result.data && result.data.content) {
            dom.innerHTML = '<div class="yiyan-content">' + result.data.content + '</div>';
          } else if (result.content) {
            dom.innerHTML = '<div class="yiyan-content">' + result.content + '</div>';
          } else {
            dom.innerHTML = '<div class="yiyan-content">生活不止眼前的苟且,还有诗和远方。</div>';
          }
        } catch (e) {
          console.error('一言加载失败:', e);
          dom.innerHTML = '<div class="yiyan-content">生活不止眼前的苟且,还有诗和远方。</div>';
        }
      }
      <?php endif; ?>
      
      async function loadMottoYiyan() {
        const mottoDom = document.querySelector('.dwo-motto');
        if (!mottoDom) return;
        try {
          const res = await fetch(PROXY_API + '?routeType=dwo_yiyan');
          const result = await res.json();
          
          if (result.data && result.data.content) {
            mottoDom.textContent = result.data.content;
          } else if (result.content) {
            mottoDom.textContent = result.content;
          } else {
            mottoDom.textContent = '生活不止眼前的苟且,还有诗和远方。';
          }
        } catch (e) {
          console.error('座右铭一言加载失败:', e);
          mottoDom.textContent = '生活不止眼前的苟且,还有诗和远方。';
        }
      }
      
      if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function() {
          loadMottoYiyan();
          <?php if ($this->options->JAside_Weather_Status == 'on') : ?>loadWeather();<?php endif; ?>
          <?php if ($this->options->JAside_Yiyan_Status == 'on') : ?>loadYiyan();<?php endif; ?>
        });
      } else {
        loadMottoYiyan();
        <?php if ($this->options->JAside_Weather_Status == 'on') : ?>loadWeather();<?php endif; ?>
        <?php if ($this->options->JAside_Yiyan_Status == 'on') : ?>loadYiyan();<?php endif; ?>
      }
    })();
    </script>
  <?php endif; ?>

第五步:使用方法

1. 申请小渡API Key

2. 后台配置

  • 进入 Typecho 后台 → 控制台 → 外观设置 → 侧栏设置
  • 填写「小渡API Key - 通用」
  • 开启「实时天气栏」和/或「一言栏」
  • 保存设置

3. 查看效果

  • 刷新博客页面
  • 侧边栏会显示实时天气和每日一言
  • 博主栏座右铭自动显示动态一言
  • API Key 完全隐藏,不会出现在前端请求中

文件修改清单

文件路径修改类型说明
functions.php修改替换天气配置为通用小渡API配置
core/route.php新增添加3个代理接口函数
core/core.php新增注册代理路由
public/aside.php修改替换天气/一言显示代码

API Key 安全性对比

方式请求示例安全性
❌ 直连https://openapi.dwo.cc/api/yi?ckey=密钥Key 暴露
✅ 代理/joe/api?routeType=dwo_yiyanKey 隐藏

前端请求只显示 /joe/api?routeType=xxx,API Key 只在后端 PHP 中使用!


注意事项

  1. 确保服务器支持 curl 扩展
  2. 如果天气/一言加载失败,检查浏览器控制台网络请求
  3. 代理接口返回错误时,会显示默认文本
  4. 建议定期更换 API Key 以提高安全性
0
joe

我来讲讲 (0)

取消