小组件:掘金精选

经验创意 · 209 次浏览
我的梦想捐钱修路建学校 创建于 2026-05-26 15:21

GPT老师改的Scriptable脚本 by:iMarkr

AI生成不一定完美,有朋友手动调整过的可把修改更完美的版本发出来给大伙用用

排序类型

sort_type: 200

  • 200 = 推荐
  • 300 = 最新
  • 3xx = 热门

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">

  <!--
    视口配置:
    width=device-width      页面宽度跟随设备宽度
    initial-scale=1         初始缩放比例
    maximum-scale=1         最大缩放比例
    user-scalable=no        禁止用户缩放
  -->
  <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

  <style>
    /*
      html/body 全屏铺满
      overflow:hidden 防止页面整体滚动
      background:transparent 方便嵌入浮岛/卡片容器
    */
    html,
    body {
      margin: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: transparent;
      font-family: "Microsoft YaHei", sans-serif;
    }

    /*
      全局盒模型:
      width/height 包含 padding 和 border
    */
    * {
      box-sizing: border-box;
    }

    /*
      主卡片容器
      整个组件 UI 的主体
    */
    .card {
      position: relative;
      width: 100%;
      height: 100%;
      padding: 16px;

      /* 圆角 */
      border-radius: 18px;

      /* 半透明边框 */
      border: 1px solid rgba(138, 190, 255, 0.3);

      /* 文字颜色 */
      color: rgba(245, 249, 255, 0.96);

      /*
        多层背景:
        1. 顶部高光
        2. 左上蓝色光斑
        3. 右上绿色光斑
        4. 深色渐变底色
      */
      background:
        linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.03) 28%, rgba(255, 255, 255, 0) 56%),
        radial-gradient(circle at 10% -10%, rgba(76, 151, 255, 0.32), transparent 35%),
        radial-gradient(circle at 96% 8%, rgba(61, 214, 184, 0.16), transparent 28%),
        linear-gradient(145deg, #171d29 0%, #111821 47%, #0a0f17 100%);

      /*
        阴影:
        inset 为内部高光
        最后一层为外部投影
      */
      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.2),
        inset 0 -1px 0 rgba(255, 255, 255, 0.04),
        0 18px 48px rgba(0, 0, 0, 0.28);

      /*
        flex 纵向布局
      */
      display: flex;
      flex-direction: column;

      /* 子元素间距 */
      gap: 11px;

      overflow: hidden;
    }

    /*
      顶部发光细线
    */
    .card::before {
      content: "";
      position: absolute;
      left: 18px;
      right: 18px;
      top: 0;
      height: 1px;

      background: linear-gradient(
        90deg,
        transparent,
        rgba(214, 235, 255, 0.75),
        transparent
      );

      opacity: 0.68;
      pointer-events: none;
    }

    /*
      底部暗色渐变遮罩
    */
    .card::after {
      content: "";
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      height: 54px;

      background: linear-gradient(
        180deg,
        transparent,
        rgba(8, 13, 20, 0.58)
      );

      pointer-events: none;
    }

    /*
      顶部栏
      包含标题、分类选择、刷新按钮
    */
    .topbar {
      display: flex;
      align-items: center;
      justify-content: space-between;

      gap: 10px;
      min-height: 48px;

      /* 固定高度,不参与 flex 拉伸 */
      flex: 0 0 auto;

      position: relative;
      z-index: 1;
    }

    /*
      标题区域
      使用 grid 布局:
      左边图标 + 右边文字
    */
    .title {
      min-width: 0;

      display: grid;

      grid-template-columns: 36px minmax(0, 1fr);
      grid-template-rows: auto auto;

      column-gap: 10px;
      align-items: center;
    }

    /*
      Logo 容器
    */
    .mark {
      grid-row: 1 / 3;

      width: 36px;
      height: 36px;

      border-radius: 12px;
      border: 1px solid rgba(165, 205, 255, 0.34);

      background:
        linear-gradient(
          160deg,
          rgba(81, 157, 255, 0.95),
          rgba(77, 218, 181, 0.88)
        );

      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.38),
        0 10px 24px rgba(31, 128, 255, 0.18);

      display: flex;
      align-items: center;
      justify-content: center;

      overflow: hidden;
      padding: 6px;
    }

    /*
      Logo 图片
    */
    .mark img {
      width: 100%;
      height: 100%;
      display: block;

      /*
        contain 保持比例完整显示
      */
      object-fit: contain;
    }

    /*
      主标题
    */
    .title strong {
      font-size: 18px;
      line-height: 22px;
      font-weight: 700;

      color: #f7fbff;

      /* 禁止换行 */
      white-space: nowrap;
    }

    /*
      副标题
    */
    .title span {
      font-size: 11px;
      line-height: 15px;

      color: rgba(204, 222, 245, 0.72);

      /*
        超出隐藏省略号
      */
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /*
      操作区:
      下拉框 + 按钮
    */
    .actions {
      display: flex;
      align-items: center;
      gap: 8px;

      flex: 0 0 auto;
    }

    /*
      select 和 button 共用样式
    */
    select,
    button {
      height: 32px;

      border: 1px solid rgba(150, 193, 255, 0.32);
      border-radius: 11px;

      background:
        linear-gradient(
          180deg,
          rgba(255, 255, 255, 0.1),
          rgba(255, 255, 255, 0.02)
        ),
        rgba(13, 20, 31, 0.72);

      color: rgba(245, 249, 255, 0.94);

      font-family: "Microsoft YaHei", sans-serif;
      font-size: 12px;

      outline: none;

      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);

      /*
        hover/active 动画过渡
      */
      transition:
        border-color 160ms ease,
        background 160ms ease,
        transform 120ms ease,
        color 160ms ease,
        box-shadow 160ms ease;
    }

    /*
      分类下拉框
    */
    select {
      max-width: 122px;
      padding: 0 28px 0 10px;
      cursor: pointer;
    }

    /*
      刷新按钮
    */
    button {
      width: 32px;
      padding: 0;

      display: inline-flex;
      align-items: center;
      justify-content: center;

      cursor: pointer;

      font-size: 15px;
      line-height: 1;
    }

    /*
      hover 高亮效果
    */
    select:hover,
    button:hover {
      border-color: rgba(168, 209, 255, 0.62);

      background:
        linear-gradient(
          180deg,
          rgba(255, 255, 255, 0.15),
          rgba(255, 255, 255, 0.04)
        ),
        rgba(27, 43, 63, 0.84);

      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.18),
        0 8px 22px rgba(16, 98, 203, 0.16);
    }

    /*
      按下效果
    */
    select:active,
    button:active {
      transform: translateY(1px);
      background: rgba(17, 28, 43, 0.9);
    }

    /*
      状态栏
      显示当前加载状态
    */
    .status {
      height: 26px;

      flex: 0 0 auto;

      display: flex;
      align-items: center;
      justify-content: space-between;

      gap: 8px;

      color: rgba(202, 219, 241, 0.74);

      font-size: 11px;
      line-height: 16px;

      overflow: hidden;

      position: relative;
      z-index: 1;

      padding: 0 8px;

      border-radius: 10px;
      border: 1px solid rgba(141, 183, 239, 0.13);

      background: rgba(7, 12, 19, 0.24);
    }

    /*
      状态文本
    */
    .statusText {
      min-width: 0;

      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    /*
      状态圆点
    */
    .pulse {
      width: 7px;
      height: 7px;

      flex: 0 0 auto;

      border-radius: 50%;

      background: #4ed9a7;

      box-shadow: 0 0 12px rgba(78, 217, 167, 0.8);
    }

    /*
      加载状态
    */
    .pulse.loading {
      background: #73adff;

      box-shadow: 0 0 12px rgba(115, 173, 255, 0.86);

      animation: breathe 900ms ease-in-out infinite alternate;
    }

    /*
      错误状态
    */
    .pulse.error {
      background: #ff6f7d;

      box-shadow: 0 0 12px rgba(255, 111, 125, 0.72);
    }

    /*
      呼吸动画
    */
    @keyframes breathe {
      from {
        opacity: 0.42;
        transform: scale(0.86);
      }

      to {
        opacity: 1;
        transform: scale(1.08);
      }
    }

    /*
      文章列表区域
    */
    .list {
      position: relative;
      z-index: 1;

      /*
        flex:1
        占满剩余空间
      */
      flex: 1 1 auto;

      min-height: 0;

      overflow-y: auto;
      overflow-x: hidden;

      padding: 1px 4px 18px 0;

      /*
        CSS 计数器初始化
      */
      counter-reset: article;
    }

    /*
      WebKit 滚动条宽度
    */
    .list::-webkit-scrollbar {
      width: 6px;
    }

    /*
      滚动条轨道
    */
    .list::-webkit-scrollbar-track {
      background: rgba(255, 255, 255, 0.04);
      border-radius: 999px;
    }

    /*
      滚动条滑块
    */
    .list::-webkit-scrollbar-thumb {
      background: rgba(143, 173, 213, 0.36);
      border-radius: 999px;
    }

    /*
      单篇文章按钮
    */
    .article {
      position: relative;

      min-height: 40px;
      width: 100%;

      border: 1px solid transparent;
      border-radius: 12px;

      /*
        左边留空间给编号
      */
      padding: 8px 8px 8px 34px;

      display: grid;

      /*
        左边标题
        右边标签
      */
      grid-template-columns: minmax(0, 1fr) auto;

      align-items: center;

      gap: 9px;

      background: transparent;

      color: inherit;
      text-align: left;

      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0);

      /*
        CSS 计数器递增
      */
      counter-increment: article;

      transition:
        border-color 160ms ease,
        background 160ms ease,
        transform 120ms ease,
        box-shadow 160ms ease;
    }

    /*
      左侧自动编号
    */
    .article::before {
      /*
        decimal-leading-zero:
        01 02 03
      */
      content: counter(article, decimal-leading-zero);

      position: absolute;

      left: 8px;
      top: 50%;

      width: 18px;
      height: 18px;

      transform: translateY(-50%);

      border-radius: 7px;

      display: flex;
      align-items: center;
      justify-content: center;

      color: rgba(224, 239, 255, 0.82);

      background: rgba(126, 174, 238, 0.12);

      border: 1px solid rgba(151, 195, 255, 0.16);

      font-size: 9px;
      line-height: 18px;
    }

    /*
      hover 高亮
    */
    .article:hover {
      border-color: rgba(148, 196, 255, 0.22);

      background:
        linear-gradient(
          90deg,
          rgba(90, 162, 255, 0.16),
          rgba(84, 213, 184, 0.06) 64%,
          transparent
        ),
        rgba(255, 255, 255, 0.035);

      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
    }

    /*
      点击按下效果
    */
    .article:active {
      transform: translateY(1px);
      background: rgba(118, 171, 236, 0.16);
    }

    /*
      文章标题
    */
    .articleTitle {
      min-width: 0;

      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;

      font-size: 13.5px;
      line-height: 19px;
      font-weight: 600;

      color: rgba(248, 251, 255, 0.94);
    }

    /*
      标题浮层提示框
      鼠标悬停时显示完整标题
    */
    .titlePopover {
      position: fixed;
      z-index: 20;

      max-width: min(360px, calc(100vw - 28px));

      padding: 9px 11px;

      border-radius: 11px;
      border: 1px solid rgba(154, 202, 255, 0.32);

      background:
        linear-gradient(
          180deg,
          rgba(255, 255, 255, 0.12),
          rgba(255, 255, 255, 0.04)
        ),
        rgba(10, 15, 23, 0.96);

      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.14),
        0 16px 38px rgba(0, 0, 0, 0.36);

      color: rgba(248, 251, 255, 0.96);

      font-size: 12px;
      line-height: 18px;

      word-break: break-word;

      /*
        初始隐藏
      */
      opacity: 0;
      transform: translateY(4px);

      pointer-events: none;

      transition:
        opacity 120ms ease,
        transform 120ms ease;
    }

    /*
      浮层显示状态
    */
    .titlePopover.visible {
      opacity: 1;
      transform: translateY(0);
    }

    /*
      标签容器
    */
    .tags {
      display: flex;
      align-items: center;
      justify-content: flex-end;

      gap: 4px;

      width: 142px;
      max-width: 142px;

      overflow: hidden;
    }

    /*
      单个标签
    */
    .tag {
      max-width: 72px;
      height: 20px;

      padding: 0 7px;

      border-radius: 999px;

      display: inline-flex;
      align-items: center;
      justify-content: center;

      color: #fff;

      font-size: 10px;
      line-height: 20px;

      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;

      border: 1px solid rgba(255, 255, 255, 0.18);

      box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.22),
        0 6px 18px rgba(0, 0, 0, 0.18);
    }

    /*
      空状态
      没有文章时显示
    */
    .empty {
      height: 100%;
      min-height: 160px;

      display: flex;
      flex-direction: column;

      align-items: center;
      justify-content: center;

      gap: 8px;

      color: rgba(214, 227, 244, 0.78);

      text-align: center;
    }

    .empty strong {
      font-size: 14px;
      color: rgba(248, 251, 255, 0.94);
    }

    .empty span {
      max-width: 300px;

      font-size: 12px;
      line-height: 18px;

      color: rgba(189, 207, 232, 0.68);
    }

    /*
      小屏适配
    */
    @media (max-width: 520px) {

      .card {
        padding: 14px;
      }

      .topbar {
        min-height: 44px;
      }

      .title {
        grid-template-columns: 34px minmax(0, 1fr);
      }

      .mark {
        width: 34px;
        height: 34px;
        border-radius: 11px;
      }

      .title strong {
        font-size: 17px;
        line-height: 20px;
      }

      select {
        max-width: 108px;
      }

      .article {
        min-height: 38px;
        padding-left: 32px;
      }

      .articleTitle {
        font-size: 13px;
        line-height: 18px;
        font-weight: 600;
      }

      .tags {
        width: 112px;
        max-width: 112px;
      }

      .tag {
        max-width: 54px;
        padding: 0 6px;
      }
    }

    /*
      超小屏适配
    */
    @media (max-width: 360px), (max-height: 260px) {

      .card {
        padding: 10px;
        gap: 7px;
      }

      .topbar {
        min-height: 38px;
      }

      .title {
        grid-template-columns: 30px minmax(0, 1fr);
        column-gap: 8px;
      }

      .mark {
        width: 30px;
        height: 30px;

        border-radius: 10px;
        padding: 5px;
      }

      .title strong {
        font-size: 15px;
        line-height: 18px;
      }

      .title span,
      .status {
        font-size: 10px;
      }

      select {
        max-width: 88px;
      }

      .tags {
        max-width: 92px;
      }

      .tag {
        max-width: 44px;
      }
    }
  </style>
</head>

<body>

  <!--
    主卡片容器
  -->
  <main class="card">

    <!-- 顶部栏 -->
    <header class="topbar">

      <!-- 标题区域 -->
      <div class="title">

        <!-- Logo -->
        <span class="mark epdff-fixed-highlight">

          <!--
            base64 内嵌图片
            优点:
            1. 单文件
            2. 无需外部资源
            3. 适合组件化
          -->
          <img
            alt="掘金"
            src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAAOSSURBVFhH7VdJaBRBFB33HZeDC7jiijDdkwwxxngQRMGLihIQRVG8uB1cENGuHho0MVHwoGLiRVG8aBQ9KYpeVNST28Wo4HpQISqauHZ1vu/3/On0TGZioplAIA8Kqn79/37V/7+quiPd6LIoc6ivaenVMUuvih+nPiLuHMQcGmYot8KwdGNU6Qb098UraahM5xcxRVMNpWvhXEctl7j5faXPFjo0RdTyh6jytsHZr5TzoLEMc6KWPxgHaZCpvB1RS9c3L0DXs4znRK3jMM+h3o5DPWXog8dIw2LDcp8i/3Xcz6bDtjL8NxiKJiG/1VFbr81W6QUOzeQmwwCsayq9DmmpYQ4Rtw8xm0qxu3uSX670iuh+Gi7TOcE6UeVW+icEtszBXDLdNiRD7FnI7c9UnrnSEeoLfApErQVMh6ZhsRfDJ4Q5DMvbk5mivwL562/Y3iYQvG8m83d030i484moh6hGuB9LuAuw84dhXSzmHTaykblENTegNLjsHPWSoY/WiE3b27DoMPXjlnWhlvsgc6EMLkz2JcMkDJuKEbbLINpacogGiDhAttCi/x2FdhRpOcb9kDxnqpjbtLztmL9iODTbF2KwHu1FkoDzpauLHBrtT4aQWVyi35RsMm6lWOPlNAbzNeyDddkn+8bN5lZC+DVMCifXcd/HxDYAHy8+kjB826wvTek3hq3XZDuu2G0hbG5kLPYLLzZFuhJOX4YJMfkMIV6WWRcM06K5fLwCXcu9m+2osa2R0Mvh+Hk6N3Zv6xVpi/XrQLm3woow/IQq3t2iaABEaCJ2cQZkpwssmiDiAKVVNISPMuY/hznh4yZqapaopaPIonF4408iGr8DA/RBcornRC0A3/1xhwbKMABk43lhYR4sFo+VPoF0jBW17GBSvGq7ePeBMS9EubeDym0FMYvmoK7uhG3h+COisbPNjxXnLqb0UjitCxNhV69yfQGxjL+Q4Ox12AbtSTShl7T7JmQU2GQiEtfSqhcnxrTdvfxVJGrJLyTbLYfzhpBeE8ZX8ZFiiNq/obicRvGFA7IfKXLkli+b2riiyfwVhPF5lgXOfV19BCkbKTT/B77LcfVugdMPgZNke4Q0PU6X4UpW3ma2EfOOAd/ppnIXstN0h6GGN4Pfjsz7v0OBIzYD4b6EMHspx374+a1QNF3U8osSh0Yg9AeQkm9w3oiirGKZTHcOUj8m3Lgv4m50NUQifwDPxUa7RSCE4AAAAABJRU5ErkJggg==">
        </span>

        <!-- 主标题 -->
        <strong>掘金精选</strong>

        <!-- 副标题 -->
        <span id="subtitle">前端 · 推荐文章流</span>
      </div>

      <!-- 操作区域 -->
      <div class="actions">

        <!-- 分类下拉 -->
        <select
          id="category"
          aria-label="文章分类">
        </select>

        <!-- 刷新按钮 -->
        <button
          id="refresh"
          type="button"
          aria-label="刷新文章"
          title="刷新文章">
          ↻
        </button>
      </div>
    </header>

    <!-- 状态栏 -->
    <section class="status">

      <!-- 状态文本 -->
      <span
        id="statusText"
        class="statusText">
        正在准备浮岛组件...
      </span>

      <!-- 状态圆点 -->
      <span
        id="pulse"
        class="pulse loading">
      </span>
    </section>

    <!-- 文章列表 -->
    <section
      id="list"
      class="list"
      aria-label="掘金文章列表">
    </section>

    <!-- 标题浮层 -->
    <div
      id="titlePopover"
      class="titlePopover"
      aria-hidden="true">
    </div>
  </main>

  <script>
    /*
      IIFE:
      Immediately Invoked Function Expression
      立即执行函数

      作用:
      1. 避免全局变量污染
      2. 形成私有作用域
    */
    (function () {

      /*
        本地状态存储 key
      */
      var STATE_KEY = 'juejinSettings';

      /*
        掘金推荐 API
      */
      var API_URL =
        'https://api.juejin.cn/recommend_api/v1/article/recommend_cate_feed';

      /*
        每篇文章最多显示几个标签
      */
      var MAX_TAGS = 2;

      /*
        分类配置
        label = 显示文本
        value = 掘金分类 ID
      */
      var categories = [
        { label: '前端', value: '6809637767543259144' },
        { label: '后端', value: '6809637769959178254' },
        { label: '安卓', value: '6809635626879549454' },
        { label: 'iOS', value: '6809635626661445640' },
        { label: '人工智能', value: '6809637773935378440' },
        { label: '开发工具', value: '6809637771511070734' },
        { label: '代码人生', value: '6809637776263217160' },
        { label: '阅读', value: '6809637772874219534' }
      ];

      /*
        兜底文章

        当:
        1. API 未返回
        2. 宿主未连接
        3. 页面刚启动

        会先显示这些内容
      */
      var fallbackArticles = [
        {
          article_info: {
            article_id: '7316421427778940943',
            title: '浮岛加载中:已先展示本地兜底文章'
          },

          tags: [
            {
              tag_name: '掘金',
              color: '#1e80ff'
            },

            {
              tag_name: '推荐',
              color: '#2f9d75'
            }
          ]
        },

        {
          article_info: {
            article_id: '7316421427778940943',
            title: '宿主就绪后会自动读取分类并刷新'
          },

          tags: [
            {
              tag_name: 'WebView2',
              color: '#7267f0'
            }
          ]
        }
      ];

      /*
        当前设置
      */
      var settings = {
        cate: categories[0].value
      };

      /*
        浮岛宿主是否已连接
      */
      var fudaoReady = false;

      /*
        等待次数计数
      */
      var fudaoWaitCount = 0;

      /*
        缓存 DOM 元素
        避免频繁 getElementById
      */
      var els = {
        category: document.getElementById('category'),
        refresh: document.getElementById('refresh'),
        subtitle: document.getElementById('subtitle'),
        statusText: document.getElementById('statusText'),
        pulse: document.getElementById('pulse'),
        list: document.getElementById('list'),
        popover: document.getElementById('titlePopover')
      };

      /*
        获取当前分类对象
      */
      function currentCategory () {

        return categories.find(function (item) {

          return item.value === settings.cate;

        }) || categories[0];
      }

      /*
        更新状态栏
      */
      function setStatus (text, mode) {

        /*
          设置状态文本
        */
        els.statusText.textContent = text;

        /*
          切换圆点样式

          mode:
          loading
          error
          空字符串
        */
        els.pulse.className =
          'pulse' + (mode ? ' ' + mode : '');
      }

      /*
        渲染分类下拉框
      */
      function renderCategories () {

        /*
          map 生成 option HTML
        */
        els.category.innerHTML = categories.map(function (item) {

          return '<option value="' +
            escapeHTML(item.value) +
            '">' +
            escapeHTML(item.label) +
            '</option>';

        }).join('');

        /*
          设置当前选中项
        */
        els.category.value = settings.cate;

        /*
          更新副标题
        */
        updateSubtitle();
      }

      /*
        更新副标题
      */
      function updateSubtitle () {

        els.subtitle.textContent =
          currentCategory().label +
          ' · 推荐文章流';
      }

      /*
        渲染文章列表
      */
      function renderArticles (articles) {

        /*
          确保 articles 为数组
        */
        var list =
          Array.isArray(articles)
            ? articles
            : [];

        /*
          空状态
        */
        if (!list.length) {

          els.list.innerHTML =
            '<div class="empty">' +
              '<strong>暂时没有文章</strong>' +
              '<span>可以切换分类,或稍后刷新掘金推荐流。</span>' +
            '</div>';

          return;
        }

        /*
          渲染文章 HTML
        */
        els.list.innerHTML = list.map(function (article) {

          /*
            文章信息
          */
          var info = article.article_info || {};

          /*
            截取标签数量
          */
          var tags =
            Array.isArray(article.tags)
              ? article.tags.slice(0, MAX_TAGS)
              : [];

          /*
            渲染标签 HTML
          */
          var tagHTML = tags.map(function (tag) {

            /*
              规范化颜色
            */
            var color =
              normalizeColor(tag.color);

            return '<span class="tag" style="background:' +
              color +
              '">' +
              escapeHTML(tag.tag_name || '标签') +
              '</span>';

          }).join('');

          /*
            返回单篇文章 HTML
          */
          return [
            '<button class="article" type="button" data-id="' +
              escapeHTML(info.article_id || '') +
              '">',

              '<span class="articleTitle epdff-fixed-highlight">' +
                escapeHTML(info.title || '未命名文章') +
              '</span>',

              '<span class="tags">' +
                tagHTML +
              '</span>',

            '</button>'
          ].join('');

        }).join('');
      }

      /*
        显示标题浮层
      */
      function showTitlePopover (target) {

        /*
          获取标题文字
        */
        var text =
          (target.textContent || '').trim();

        if (!text) return;

        /*
          设置浮层内容
        */
        els.popover.textContent = text;

        /*
          添加 visible 类
        */
        els.popover.classList.add('visible');

        /*
          ARIA 无障碍属性
        */
        els.popover.setAttribute(
          'aria-hidden',
          'false'
        );

        /*
          定位浮层
        */
        positionTitlePopover(target);
      }

      /*
        计算浮层位置
      */
      function positionTitlePopover (target) {

        /*
          浮层未显示直接返回
        */
        if (
          !els.popover.classList.contains('visible')
        ) return;

        /*
          获取目标元素位置
        */
        var rect =
          target.getBoundingClientRect();

        /*
          获取浮层尺寸
        */
        var popRect =
          els.popover.getBoundingClientRect();

        var margin = 10;

        /*
          默认位置:
          标题下方
        */
        var left = rect.left;
        var top = rect.bottom + 8;

        /*
          防止超出右边界
        */
        if (
          left + popRect.width >
          window.innerWidth - margin
        ) {

          left =
            window.innerWidth -
            popRect.width -
            margin;
        }

        /*
          防止超出左边界
        */
        if (left < margin) {
          left = margin;
        }

        /*
          防止超出底部
          自动改到上方显示
        */
        if (
          top + popRect.height >
          window.innerHeight - margin
        ) {

          top =
            rect.top -
            popRect.height -
            8;
        }

        /*
          防止超出顶部
        */
        if (top < margin) {
          top = margin;
        }

        /*
          应用位置
        */
        els.popover.style.left =
          left + 'px';

        els.popover.style.top =
          top + 'px';
      }

      /*
        隐藏浮层
      */
      function hideTitlePopover () {

        els.popover.classList.remove('visible');

        els.popover.setAttribute(
          'aria-hidden',
          'true'
        );
      }

      /*
        获取当前鼠标悬停的标题元素
      */
      function getHoveredTitle (event) {

        /*
          优先直接命中标题
        */
        var title = event.target.closest(
          '.articleTitle.epdff-fixed-highlight'
        );

        if (title) return title;

        /*
          获取文章元素
        */
        var article =
          event.target.closest('.article');

        if (!article) return null;

        /*
          从文章内查找标题
        */
        title = article.querySelector(
          '.articleTitle.epdff-fixed-highlight'
        );

        if (!title) return null;

        /*
          获取标题区域
        */
        var rect =
          title.getBoundingClientRect();

        /*
          当前鼠标位置
        */
        var x = event.clientX;
        var y = event.clientY;

        /*
          判断是否位于标题区域
        */
        if (
          x >= rect.left &&
          x <= rect.right &&
          y >= rect.top &&
          y <= rect.bottom
        ) {
          return title;
        }

        return null;
      }

      /*
        校验颜色值
      */
      function normalizeColor (value) {

        /*
          非字符串返回默认蓝色
        */
        if (typeof value !== 'string') {
          return '#1e80ff';
        }

        var text = value.trim();

        /*
          #RGB
          #RRGGBB
          #RRGGBBAA
        */
        if (/^#[0-9a-fA-F]{3,8}$/.test(text)) {
          return text;
        }

        /*
          纯 6 位 hex
          自动补 #
        */
        if (/^[0-9a-fA-F]{6}$/.test(text)) {
          return '#' + text;
        }

        /*
          默认颜色
        */
        return '#1e80ff';
      }

      /*
        HTML 转义
        防止 XSS 注入
      */
      function escapeHTML (value) {

        return String(value == null ? '' : value)

          /*
            转义 &
          */
          .replace(/&/g, '&amp;')

          /*
            转义 <
          */
          .replace(/</g, '&lt;')

          /*
            转义 >
          */
          .replace(/>/g, '&gt;')

          /*
            转义 "
          */
          .replace(/"/g, '&quot;')

          /*
            转义 '
          */
          .replace(/'/g, '&#39;');
      }

      /*
        调用浮岛宿主 API
      */
      function invoke (method, args) {

        return window.fudao.invoke(
          method,
          args || {}
        );
      }

      /*
        加载本地设置
      */
      function loadSettings () {

        /*
          宿主未连接
          直接跳过
        */
        if (!fudaoReady) {
          return Promise.resolve();
        }

        /*
          从宿主读取状态
        */
        return invoke('state.read', {

          key: STATE_KEY,

          defaultValue:
            JSON.stringify(settings)

        }).then(function (res) {

          /*
            兼容字符串/对象
          */
          var raw =
            typeof res === 'string'
              ? res
              : JSON.stringify(res || {});

          var saved = {};

          /*
            JSON 解析保护
          */
          try {

            saved =
              JSON.parse(raw || '{}') || {};

          } catch (err) {

            saved = {};
          }

          /*
            校验分类合法性
          */
          if (
            saved.cate &&
            categories.some(function (item) {
              return item.value === saved.cate;
            })
          ) {

            settings.cate = saved.cate;
          }

          /*
            更新 UI
          */
          els.category.value =
            settings.cate;

          updateSubtitle();
        });
      }

      /*
        保存设置
      */
      function saveSettings () {

        if (!fudaoReady) {
          return Promise.resolve();
        }

        /*
          写入宿主状态
        */
        return invoke('state.write', {

          key: STATE_KEY,

          value:
            JSON.stringify(settings)

        }).catch(function () {

          /*
            忽略保存错误
          */
        });
      }

      /*
        请求文章
      */
      function requestArticles () {

        /*
          更新状态栏
        */
        setStatus(
          '正在刷新 ' +
          currentCategory().label +
          ' 推荐...',
          'loading'
        );

        /*
          禁用刷新按钮
          防止重复点击
        */
        els.refresh.disabled = true;

        /*
          请求体
        */
        var body = JSON.stringify({

          /*
            文章类型
          */
          id_type: 2,

          /*
            分类 ID
          */
          cate_id: settings.cate,

          /*
            排序类型
          */
          sort_type: 300,

          /*
            分页游标
          */
          cursor: '0',

          /*
            每次加载数量
          */
          limit: 20
        });

        var request;

        /*
          浮岛宿主环境
        */
        if (fudaoReady) {

          /*
            使用宿主 http.post
          */
          request = invoke('http.post', {

            url: API_URL,

            headers: {
              'Content-Type':
                'application/json; encoding=utf-8'
            },

            body: body,

            /*
              10 秒超时
            */
            timeoutMs: 10000

          }).then(function (result) {

            /*
              HTTP 状态校验
            */
            if (!result || !result.ok) {

              throw new Error(
                'HTTP ' +
                (
                  result && result.status
                    ? result.status
                    : '请求失败'
                )
              );
            }

            /*
              解析 JSON
            */
            return JSON.parse(
              result.text || '{}'
            );
          });

        /*
          普通浏览器环境
        */
        } else if (window.fetch) {

          /*
            使用 fetch
          */
          request = fetch(API_URL, {

            method: 'POST',

            headers: {
              'Content-Type':
                'application/json; encoding=utf-8'
            },

            body: body

          }).then(function (res) {

            if (!res.ok) {

              throw new Error(
                'HTTP ' + res.status
              );
            }

            return res.json();
          });

        } else {

          /*
            fetch 不支持
          */
          request = Promise.reject(
            new Error('浮岛宿主尚未就绪')
          );
        }

        /*
          请求成功
        */
        return request.then(function (json) {

          /*
            提取文章数据
          */
          var articles =
            Array.isArray(json.data)
              ? json.data
              : [];

          /*
            渲染列表
          */
          renderArticles(articles);

          /*
            更新状态栏
          */
          setStatus(
            '已更新 ' +
            articles.length +
            ' 篇 · ' +
            formatTime(new Date()),
            ''
          );

        }).catch(function (err) {

          /*
            请求失败
          */
          setStatus(
            (
              err && err.message
                ? err.message
                : '刷新失败'
            ) +
            ',已保留当前内容',
            'error'
          );

        }).then(function () {

          /*
            恢复刷新按钮
          */
          els.refresh.disabled = false;
        });
      }

      /*
        格式化时间
        输出 HH:mm
      */
      function formatTime (date) {

        /*
          padStart:
          不足两位补 0
        */
        var h =
          String(date.getHours())
            .padStart(2, '0');

        var m =
          String(date.getMinutes())
            .padStart(2, '0');

        return h + ':' + m;
      }

      /*
        打开文章
      */
      function openArticle (articleId) {

        if (!articleId) return;

        /*
          拼接文章 URL
        */
        var url =
          'https://juejin.cn/post/' +
          articleId;

        /*
          浮岛宿主环境
        */
        if (fudaoReady) {

          /*
            调用宿主打开链接
          */
          invoke('url.open', {

            url: url,

            /*
              打开后关闭浮岛
            */
            closeAfterOpen: true

          }).catch(function () {

            /*
              忽略错误
            */
          });

        } else {

          /*
            浏览器环境提示
          */
          setStatus(
            '需要浮岛宿主打开文章链接',
            'error'
          );
        }
      }

      /*
        绑定事件
      */
      function bindEvents () {

        /*
          分类切换
        */
        els.category.addEventListener(
          'change',
          function () {

            /*
              更新分类
            */
            settings.cate =
              els.category.value;

            /*
              更新副标题
            */
            updateSubtitle();

            /*
              保存设置
              然后刷新文章
            */
            saveSettings()
              .then(requestArticles);
          }
        );

        /*
          刷新按钮
        */
        els.refresh.addEventListener(
          'click',
          function () {

            requestArticles();
          }
        );

        /*
          点击文章
        */
        els.list.addEventListener(
          'click',
          function (event) {

            var item =
              event.target.closest('.article');

            if (item) {

              openArticle(
                item.getAttribute('data-id')
              );
            }
          }
        );

        /*
          pointer 系列事件:
          现代指针事件
          同时支持鼠标/触摸笔/触控
        */

        /*
          鼠标移入
        */
        els.list.addEventListener(
          'pointerover',
          function (event) {

            var title =
              getHoveredTitle(event);

            if (title) {

              showTitlePopover(title);
            }
          }
        );

        /*
          鼠标移动
        */
        els.list.addEventListener(
          'pointermove',
          function (event) {

            var title =
              getHoveredTitle(event);

            if (title) {

              showTitlePopover(title);

            } else {

              hideTitlePopover();
            }
          }
        );

        /*
          鼠标移出
        */
        els.list.addEventListener(
          'pointerout',
          function (event) {

            var title =
              getHoveredTitle(event);

            /*
              relatedTarget:
              当前移向的元素
            */
            if (
              title &&
              !title.contains(event.relatedTarget)
            ) {

              hideTitlePopover();
            }
          }
        );

        /*
          mouse 系列事件
          用于兼容旧环境
        */

        els.list.addEventListener(
          'mouseover',
          function (event) {

            var title =
              getHoveredTitle(event);

            if (title) {

              showTitlePopover(title);
            }
          }
        );

        els.list.addEventListener(
          'mousemove',
          function (event) {

            var title =
              getHoveredTitle(event);

            if (title) {

              showTitlePopover(title);

            } else {

              hideTitlePopover();
            }
          }
        );

        els.list.addEventListener(
          'mouseout',
          function (event) {

            var title =
              getHoveredTitle(event);

            if (
              title &&
              !title.contains(event.relatedTarget)
            ) {

              hideTitlePopover();
            }
          }
        );

        /*
          窗口大小变化
          隐藏浮层
        */
        window.addEventListener(
          'resize',
          hideTitlePopover
        );

        /*
          滚动列表时隐藏浮层
        */
        els.list.addEventListener(
          'scroll',
          hideTitlePopover
        );
      }

      /*
        等待浮岛宿主注入
      */
      function waitForFudao () {

        /*
          检测宿主 API 是否存在
        */
        if (
          window.fudao &&
          typeof window.fudao.invoke === 'function'
        ) {

          /*
            标记宿主已连接
          */
          fudaoReady = true;

          setStatus(
            '浮岛已连接,正在读取偏好...',
            'loading'
          );

          /*
            加载设置
            然后刷新文章
          */
          loadSettings()

            .then(requestArticles)

            .catch(function () {

              /*
                设置读取失败
                依然刷新文章
              */
              requestArticles();
            });

          return;
        }

        /*
          增加等待计数
        */
        fudaoWaitCount += 1;

        /*
          最多等待 80 次
        */
        if (fudaoWaitCount < 80) {

          /*
            120ms 后继续轮询
          */
          setTimeout(
            waitForFudao,
            120
          );

          return;
        }

        /*
          超时后进入浏览器兜底模式
        */
        setStatus(
          '浮岛宿主未连接,使用浏览器兜底刷新',
          'loading'
        );

        requestArticles();
      }

      /*
        初始化流程
      */

      /*
        渲染分类
      */
      renderCategories();

      /*
        先显示兜底文章
      */
      renderArticles(fallbackArticles);

      /*
        绑定事件
      */
      bindEvents();

      /*
        延迟检测宿主
      */
      setTimeout(
        waitForFudao,
        80
      );

    })();
  </script>
</body>
</html>

我的梦想捐钱修路建学校 最后更新于 2026/6/11

回复内容
暂无回复
回复主贴