<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <follow_challenge>
  <feedId>41798923170845756</feedId>
  <userId>56237599479518208</userId>
</follow_challenge>
<channel>
        <title>imba97</title>
        <link>https://imba97.com/</link>
        <description>imba97 Blog</description>
        <lastBuildDate>Sun, 10 May 2026 16:45:24 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>imba97</title>
            <url>https://imba97.com/favicon.png</url>
            <link>https://imba97.com/</link>
        </image>
        <copyright>CC BY-NC-SA 4.0 2018 © imba97</copyright>
        <atom:link href="https://imba97.com/feed.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[从体积焦虑到按需加载]]></title>
            <link>https://imba97.com/posts/813</link>
            <guid isPermaLink="false">https://imba97.com/posts/813</guid>
            <pubDate>Sun, 10 May 2026 09:12:52 GMT</pubDate>
            <content:encoded><![CDATA[<h2>从体积焦虑到按需加载</h2>
<p>写了个中文字体子集化工具 <code>charbi-font</code></p>
<hr>
<h2>开发契机</h2>
<p>受公司大佬做的工具的启发，了解到字体子集化这条路径。说来惭愧，入行前端这么多年了，还是头一次知道</p>
<p>字体子集化确实能显著减小体积，但落地到工作中的项目时，发现还有一些绕不开的需求</p>
<hr>
<h2>发现需求</h2>
<ol>
<li>
<p>多字重支持</p>
<p>阿里巴巴普惠体在项目里用到 5 种字重，希望能自动合并管理，不想手动维护多份配置</p>
</li>
<li>
<p>文本内容变化</p>
<p>新项目迭代快，要考虑文案、字重可能会有变更，以及后续随着迭代的文案变更等，我太懒，不想每次都手动维护字符清单</p>
</li>
<li>
<p>上传分发能力</p>
<p>Base64 自带 33% 体积增加，受小程序包体限制影响，感觉早晚需要放到云存储，所以需要有上传、构造版本路径等能力</p>
</li>
</ol>
<hr>
<h2>charbi-font 做了什么</h2>
<p><code>charbi-font</code> 覆盖了从扫描、构建到分发的完整链路：</p>
<ul>
<li>递归扫描源码，提取真实使用字符</li>
<li>扫描时自动过滤注释内容</li>
<li>原字体本地缓存，避免反复下载造成的浪费</li>
<li>上传 CDN（如 COS），支持并发上传和远端存在判断</li>
<li>支持 <code>extraText</code> 手动补充动态文本</li>
<li>输出 <code>woff / woff2 / ttf</code></li>
<li>自动生成 CSS/SCSS 样式文件</li>
<li>支持多字重合并管理</li>
</ul>
<h3>实测压缩效果</h3>
<table>
<thead>
<tr>
<th>名称</th>
<th style="text-align:right">原体积（MB）</th>
<th style="text-align:right">压缩体积（KB）</th>
<th style="text-align:right">压缩率</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alibaba PuHuiTi (5 weights)</td>
<td style="text-align:right">35 MB</td>
<td style="text-align:right">985 KB</td>
<td style="text-align:right">97.22%</td>
</tr>
<tr>
<td>DingTalkJinBuTi</td>
<td style="text-align:right">2 MB</td>
<td style="text-align:right">214 KB</td>
<td style="text-align:right">89.71%</td>
</tr>
<tr>
<td>DelaGothicOne</td>
<td style="text-align:right">2 MB</td>
<td style="text-align:right">144 KB</td>
<td style="text-align:right">94.08%</td>
</tr>
<tr>
<td>Total</td>
<td style="text-align:right">39 MB</td>
<td style="text-align:right">1,343 KB</td>
<td style="text-align:right">96.63%</td>
</tr>
</tbody>
</table>
<h3>其他特性</h3>
<ul>
<li>
<h4>Vite 插件支持</h4>
<p>通过 Vite 插件，实现业务侧更多能力</p>
<details>
  <summary>
    配置 Vite 插件...查看更多
  </summary>
<pre><code class="language-ts">import CharbiFont from 'charbi-font/vite'

export default defineConfig(() =&gt; {
  plugins: [
    CharbiFont()
  ]
})
</code></pre>
<p>使用 TS 可以在 <code>tsconfig.json</code> 中添加类型支持</p>
<pre><code class="language-json">{
  &quot;compilerOptions&quot;: {
    &quot;types&quot;: [&quot;charbi-font/client&quot;]
  }
}
</code></pre>
</details>
</li>
<li>
<h4>获取加载字体所需字段</h4>
<p>因为需要基于版本号创建不同的目录，防止多版本相互影响。在生成的样式中是自动写入的，但有时也会使用 <code>uni.loadFontFace</code> 在 JS 中加载字体</p>
<details>
<p>业务侧无需再手写一份字重与文件名对照表</p>
<pre><code class="language-ts">import { BUILD_FONT_FACES, FONT_ASSET_BASE_URL, FONT_BUILD_VERSION } from 'virtual:charbi-font'

for (const face of BUILD_FONT_FACES) {
  const base = FONT_ASSET_BASE_URL
  if (!base)
    continue
  uni.loadFontFace({
    global: true,
    family: face.family,
    source: `url(&quot;${base}/${face.file}&quot;)`,
    desc: {
      style: face.style,
      weight: face.weight,
      variant: face.variant
    }
  })
}
</code></pre>
  <summary>
    业务侧无需再手写一份...查看更多
  </summary>
</details>
</li>
</ul>
<h2>使用</h2>
<h3>安装</h3>
<pre><code>pnpm add charbi-font -D
</code></pre>
<h3>配置</h3>
<p>目录下创建 <code>fonts.config.ts</code>，配置示例</p>
<pre><code class="language-ts">import { defineConfig } from 'charbi-font/config'

/**
 * 字体构建配置
 */
export default defineConfig({
  build: {
    // 可选：字体版本基线（不填则默认读取 package.json 的 version）
    // 扫描配置 - 自动从项目中提取使用的字符
    scan: {
      srcDir: ['src'],
      extensions: ['vue', 'ts', 'tsx', 'js', 'jsx', 'scss', 'css']
    },

    // 字体配置 - 从 COS 下载原始字体
    fonts: [
      // 阿里巴巴普惠体 Regular - 按钮文字
      {
        family: 'Alibaba PuHuiTi',
        name: 'Regular',
        weight: 400,
        url: 'https://example.com/AlibabaPuHuiTi-3-55-Regular.ttf'
      },
      // 阿里巴巴普惠体 Medium - 中等粗细
      {
        family: 'Alibaba PuHuiTi',
        name: 'Medium',
        weight: 500,
        url: 'https://example.com/AlibabaPuHuiTi-3-65-Medium.ttf'
      },
      // 阿里巴巴普惠体 SemiBold - 半粗体
      {
        family: 'Alibaba PuHuiTi',
        name: 'SemiBold',
        weight: 600,
        url: 'https://example.com/AlibabaPuHuiTi-3-75-SemiBold.ttf'
      },
      // 阿里巴巴普惠体 Bold - 粗体标题
      {
        family: 'Alibaba PuHuiTi',
        name: 'Bold',
        weight: 700,
        url: 'https://example.com/AlibabaPuHuiTi-3-85-Bold.ttf'
      },
      // 阿里巴巴普惠体 Black - 标题强调
      {
        family: 'Alibaba PuHuiTi',
        name: 'Black',
        weight: 900,
        url: 'https://example.com/AlibabaPuHuiTi-3-115-Black.ttf',
        extraText: [
          '歪比巴卜'
        ]
      },
      // 钉钉进步体 - 标题/艺术字
      {
        family: 'DingTalkJinBuTi',
        name: 'Regular',
        weight: 400,
        url: 'https://example.com/DingTalk-JinBuTi.ttf'
      },
      // Dela Gothic One - 价格数字
      {
        family: 'DelaGothicOne',
        name: 'Regular',
        weight: 400,
        url: 'https://example.com/DelaGothicOne-Regular.ttf',
        format: 'ttf' // 单独配置为 ttf 格式
      }
    ],

    // 输出配置
    output: {
      // CSS 输出目录（fonts.scss/css 汇总文件生成在此目录）
      cssDir: 'src/styles',
      format: 'woff2',
      // 样式文件格式: scss 或 css
      styleFormat: 'scss',
      fontDisplay: 'optional'
    }
  },

  upload: {
    provider: 'cos',
    concurrency: 5
  },

  // COS 上传配置
  cos: {
    // Bucket 名称
    bucket: 'bucket-name',
    // 所属地域
    region: 'ap-guangzhou',
    // 上传路径（包含完整路径，{version} 会被替换为实际版本号）
    basePath: '/cos/static/fonts/built/{version}',
    // CDN 基础地址（只包含域名）
    cdnUrl: 'https://cos.example.com'
  }
})
</code></pre>
<h3>命令</h3>
<pre><code class="language-bash"># 构建及上传
pnpm charbi

# 仅构建
pnpm chabi build

# 仅上传
pnpm chabi upload
</code></pre>
<h2>关于名称</h2>
<p>最开始让 AI 起名，我说可以造组合字，于是在一堆名称中我一眼看到了 <code>char + build = charbi</code>（你没有偷摸骂我吧？）</p>
<h2>开源</h2>
<p><a href="https://github.com/imba97/charbi-font">Github</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[写了个从夯到拉排行榜组件]]></title>
            <link>https://imba97.com/posts/811</link>
            <guid isPermaLink="false">https://imba97.com/posts/811</guid>
            <pubDate>Sun, 08 Feb 2026 10:28:35 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>起因是本人经常喝完瑞幸新品后跟朋友们简评一波，然后列个榜单。所以不如就直接搞个从夯到拉的网页，既然要做不如就直接封装成组件，于是就有了这个</p>
<p>顺便一说，瑞幸家五黑拿铁、西瓜拿铁、每日维C果蔬茶我是给到一个夯的</p>
<h1>h2l-ranking</h1>
<p><img src="https://imba97.com/uploads/2026/02/h2l-ranking-1.png" alt=""></p>
<p>体验页面：<a href="https://imba97.me/ranking">https://imba97.me/ranking</a></p>
<h2>安装</h2>
<pre><code class="language-bash">pnpm add h2l-ranking
</code></pre>
<h2>使用</h2>
<pre><code class="language-html">&lt;template&gt;
  &lt;H2lRanking :rankings=&quot;rankings&quot; /&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import H2lRanking from 'h2l-ranking'
import { ref } from 'vue'
import 'h2l-ranking/style.css'

const rankings = ref({
  hang: [
    {
      title: '《中二病也要谈恋爱》',
      cover: '/images/1.png',
      url: 'https://example.com/1',
      description: '本人入宅作，给到一个夯'
    },
    { title: '《玉子爱情故事》', cover: '/images/2.png' },
    { title: '《葬送的芙莉莲》', cover: '/images/3.png' },
    { title: '《我心里危险的东西》', description: '心理描写细腻，适合我们死宅体质', cover: '/images/4.png' },
    { title: '《双城之战》', cover: '/images/5.png', description: '金克斯牛逼！' }
  ],
  upper: [
    { title: '《搞笑漫画日和》', cover: '/images/6.png' },
    { title: '《胆大党》', cover: '/images/7.png' },
    { title: '《荒川爆笑团》', cover: '/images/8.png' },
    { title: '《银魂》', cover: '/images/9.png' },
    { title: '《某科学的超电磁炮》', cover: '/images/10.png' },
    { title: '《魔法禁书目录》', cover: '/images/11.png' },
    { title: '《热带雨林的爆笑生活》', cover: '/images/12.png' }
  ],
  middle: [],
  lower: [],
  la: []
})
&lt;/script&gt;
</code></pre>
<h1>开源</h1>
<p><span i-mdi-github></span> <a href="https://github.com/imba97/h2l-ranking">imba97/h2l-ranking</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[追寻自我的 2025]]></title>
            <link>https://imba97.com/posts/810</link>
            <guid isPermaLink="false">https://imba97.com/posts/810</guid>
            <pubDate>Wed, 31 Dec 2025 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<h1>一句话</h1>
<p>从 2019 年使用至今的 “间歇性踌躇满志，持续性混吃等死” 毕业了，今年的一句话我定义为</p>
<blockquote>
<p>反者道之动，弱者道之用。</p>
</blockquote>
<p>今年有了更多的感悟，感觉开始有点 “活明白” 了</p>
<h1>这一年</h1>
<p>去年的目标是去往更多城市，今年年初的工作可以远程，所以一两个月就跑出去一周，去了 <span i-solar-map-point-wave-bold-duotone text-primary></span> 大连、<span i-solar-map-point-wave-bold-duotone text-primary></span> 昆明、<span i-solar-map-point-wave-bold-duotone text-primary></span> 大理，后来回老家陪我奶奶待了一个月，详情阅读 <a href="/posts/803">《数字游民报告》</a></p>
<p>从老家回来去 <span i-solar-map-point-wave-bold-duotone text-primary></span> 上海玩了一圈，然后去 <span i-solar-map-point-wave-bold-duotone text-primary></span> 杭州找工作，结果找到了 <span i-solar-map-point-wave-bold-duotone text-primary></span> 扬州，于是 <a href="/posts/804">《我入职了老东家的母公司》</a>。年底又离开了扬州，来到了 <span i-solar-map-point-wave-bold-duotone text-primary></span> 成都</p>
<p>有时候越想干什么越是干不成，反而不刻意去完成这个目标，结果为了见家人和朋友、为了旅游、为了看演出、为了找工作，在不知不觉中去了很多地方，也享受了一路的风景</p>
<p>一个又一个的巧合组成了这一年。一路上贵人相伴，也许我做对了一件事，就是接下来要说的</p>
<h1>心理</h1>
<p>更多的目光放在了自己身上，因为想明白了一个底层逻辑：人是会死的，人类有一天也会灭绝。人生不过三万天，而我已经快三十了</p>
<p>我属于高敏感的人，容易内耗。与其看各种解决方案，不如正视它接受它并利用它，也就是<strong>接受自己</strong>。现在阶段像是某种觉醒，一瞬的转变，但这之前一定也是需要各种或积累或摧毁，提高了能力、自信、认知等，才能兜得住这种转变</p>
<p>对他人，抛弃“我何德何能”多余的感激，转为“既然让我来做，我就要做到我能力范围内的最好”，人与人之间更多的是利益交换，而不是只有感激之情。对自己，不断探索和体验、不断完善自己、不断实现自我价值。毕竟人活着就是要去码头整点薯条</p>
<p>当然也要谦虚谨慎，独立思考，持续学习，以及我爸总跟我说的厚德载物。当自身足够强大，自然会吸引到更优秀的人，机会来了也才能承载得住</p>
<h1>程序</h1>
<ul>
<li><a href="https://github.com/imba97/fs-ctx">fs-ctx</a> - 基于文件系统的上下文管理器</li>
<li><a href="https://github.com/imba97/matchinitx">matchinitx</a> - initx 抽离的匹配器</li>
<li><a href="https://github.com/imba97/uno-colors">uno-colors</a> - UnoCSS 色阶生成</li>
<li><a href="https://github.com/imba97/steam-playing-game">steam-playing-game</a> - 获取 Steam 正在玩的游戏</li>
</ul>
<h1>影音</h1>
<ul>
<li><span i-mingcute-movie-line></span> <a href="https://movie.douban.com/subject/37508707/">如果这个世界是一座舞台，后台究竟在哪里</a></li>
<li><span i-mdi-music-circle-outline></span> 光 - 艾热孜巴</li>
</ul>
<h1>文字</h1>
<blockquote>
<p>缘分本来就稀薄寡淡，相伴一程已是万分感激。<br>
没有什么东西是一成不变的，没有谁能一直陪着谁，<br>
只要同行的时候是快乐的，就是好的相遇，<br>
至于怎么走散的并不重要。</p>
</blockquote>
<h1>2026 目标</h1>
<p>爱你老己 ❤️</p>
<h1>年度照片</h1>
<p><img src="https://imba97.com/uploads/2025/05/digital-nomad-report-9.jpg" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[不要只给 AI 软的，也要给硬的]]></title>
            <link>https://imba97.com/posts/809</link>
            <guid isPermaLink="false">https://imba97.com/posts/809</guid>
            <pubDate>Tue, 18 Nov 2025 22:21:07 GMT</pubDate>
            <content:encoded><![CDATA[<h1>近期事件</h1>
<p>最近几个月的工作中，开始接触公司的更多项目，项目一个 10 年以上一个 6 年以上，有些共同特征</p>
<ul>
<li>工具老，Webpack 2，还有各种古早依赖库</li>
<li>代码乱，没有底层架构，没有设计模式，没有代码规范，只面向需求迭代</li>
</ul>
<p>结合以上问题，我们这次来谈谈 AI 在开发老项目中的问题。那么首先，我需要解释一下</p>
<h1>什么是软的？什么是硬的？</h1>
<p>如果将文档比作<strong>软的</strong>，也就是自然语言化，灵活性高、重在意图与含义</p>
<p>那么代码则是<strong>硬的</strong>，也就是形式语言化，严谨性高，重在逻辑与结构</p>
<h1>AI 的背刺</h1>
<blockquote>
<p>本文主要介绍使用 AI 开发相关的内容，所有的“文档”指的都是“AI 开发指导文档”</p>
</blockquote>
<p>使用 AI 开发这些老项目时，经常会遇到修复一个问题后，导致另外一个问题出现了，还有就是有时候发现之前写的某个功能不起作用了，检查代码后发现其他人修改了代码，把我的逻辑注释或删掉了</p>
<p>本质上这并不是 AI 的问题。原因是 AI 是目的专注的，当你给 AI 提出一个问题修复时，它的目光首先会聚焦在目的本身，然后向外扩散检查相关逻辑。在混乱的代码中，它的专注力更多的会在出问题的一条逻辑线上，所以如果逻辑互相有多处关联</p>
<p><img src="https://imba97.com/uploads/2025/11/dont-just-give-ai-soft-give-it-hard-too-1.png" alt=""></p>
<p>就会非常容易因修复一个问题产生一个或多个新的问题</p>
<h1>稳定的三角关系</h1>
<p>解决这个问题，只优化 AI 文档总是有限的，代码的优化也是必不可少的。包括但不限于，使用设计模式、提供抽象类、配置代码规范等等。最理想的状态是，让 AI、文档、代码，形成一个稳定的三角关系</p>
<p><img src="/uploads/2025/11/dont-just-give-ai-soft-give-it-hard-too-2.png" alt=""></p>
<p>近期在实现某个相对独立的模块的时候，我尝试尽可能脱离原本的代码去实现，添加了 <code>TypeScript</code> 做类型约束、实现基础抽象类等，让新增模块中的功能像做填空题一样，或者说像插件开发一样。经历了几次小范围重构和优化，<span text="#7B6FF6"><strong>代码</strong></span>提供硬的约束边界，<span text="#89E2FC"><strong>文档</strong></span>提供软的方向指导，再通过 <span text="#FFDE5E"><strong>AI</strong></span> 让两者之间不断循环迭代</p>
<p>再次感叹，好的设计模式无论什么时代都不会过时</p>
<h1>牢骚</h1>
<p>简单分析一下，一个项目怎么就会变成这样</p>
<p>主要原因，项目在基于需求迭代，安排一个功能做一个功能，零零散散且高耦合，底层封装基本没有</p>
<p>次要原因，每个月有迭代要求，优化都只能挤时间做，重构更是不可能，发版时间为重，感觉是为了发版而发版</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[组了个 QNAS MINI]]></title>
            <link>https://imba97.com/posts/808</link>
            <guid isPermaLink="false">https://imba97.com/posts/808</guid>
            <pubDate>Tue, 14 Oct 2025 22:17:38 GMT</pubDate>
            <content:encoded><![CDATA[<h1>QNAS MINI</h1>
<p>QNAS MINI 是一个 2.5 寸 6 盘位 <span i-material-symbols-hard-disk-rounded bg-gray></span> NAS，拼好后就是下图</p>
<p><img src="https://imba97.com/uploads/2025/10/qnasmini-1.png" alt=""></p>
<p>（下半部分，上面是个 UPS）</p>
<p>硬件如下</p>
<ul>
<li>3D 打印外壳（海鲜市场）</li>
<li>QNAS MINI 背板及线材螺丝（海鲜市场）</li>
<li>畅网微控 N355 主板</li>
<li>Crucial DDR5 4800MHz 32G 内存条</li>
<li>M.2 转 6 SATA ASM1166 扩展卡</li>
<li>致钛 500G + 2TB SSD</li>
</ul>
<h1>软件</h1>
<p>系统安装了飞牛，一直想试试，真好用</p>
<p>主要是相册和影音，相册支持 AI 人脸识别和以文搜图搜视频，影音相当于 Jellyfin + 信息刮削 + 字幕下载，还自带一个 <code>FN Connect</code>，可以远程访问到 NAS，免费的完全够用</p>
<p>除此之外，我又单独跑了一个 <code>Cloudflare Tunnel</code> 做内网穿透，<code>Docker</code> 跑起来，配置简单，作为一个备份。也确实遇到过一次官方远程连不上的问题</p>
<p>之前用的 <code>NPS</code>，需要自己搭建服务器，作者也不更新了，所以刚好也是顺便换掉</p>
<h1>拼装踩坑总结</h1>
<p>出了一个视频 <span i-ix-bilibili-logo bg="#fb7299"></span> <a href="http://b23.tv/BV1H34UzWEwJ">BV1H34UzWEwJ</a>，可以参考一下，以下是文字总结</p>
<ol>
<li>主板上螺丝是，有一侧螺丝不好拧，可以使用束线带绑住螺丝伸进去拧</li>
<li>主板安装后可以先接一下 SATA 线，是有正反的</li>
<li>侧面有方形孔，可以用来扎线，如果引导盘用硬盘，注意别压到内存</li>
<li>风扇供电线有正反，不要接反了</li>
</ol>
<h1>网络拓扑图</h1>
<p>更新了一下网络拓扑图</p>
<p><img src="/uploads/2025/10/qnasmini-2.png" alt=""></p>
<p>后续逐步把 All in One 的 NAS 迁移到 QNAS MINI 上，以及安排上 6 盘位</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[公司报销全凭本事]]></title>
            <link>https://imba97.com/posts/806</link>
            <guid isPermaLink="false">https://imba97.com/posts/806</guid>
            <pubDate>Fri, 29 Aug 2025 23:33:47 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前情提要</h1>
<p>两个月前<a href="/posts/804">《我入职了老东家的母公司》</a>，也连轴转到 18 号，甚至没放过周末。做了个原生 <span i-ic-baseline-wechat bg="#07c160"></span> 微信小程序转 <span i-tdesign-logo-alipay-filled bg="#0499e1"></span> 支付宝小程序的转换器，有机会单开一篇文章聊聊</p>
<p>目前终于是告一段落了</p>
<h1>工作环境</h1>
<p>现在在公司的状态感觉跟逛超市差不多，悠闲自在的心态，整个一显眼包</p>
<p>这句话从一个一天 10 ~ 14 小时工作的人嘴里说出来，可能没啥说服力，但别人怎么想不重要</p>
<p>我现在的风格就是，如果工作环境不适合自己，那就自己动手改造（包括物和人）。这边是个新办公场地，之前说要买点小家电，在群里征集意见，我想到啥点啥，制冰机、咖啡机、吧台桌一大堆</p>
<p>但其实，公司买个东西相当慢，就算是最需要的冰箱，也等了一个多月才下来，我点的那些大部分买都不可能买</p>
<p>于是...</p>
<h1>迂回战术</h1>
<p>这件事本质上不就是，一、买制冰机，二、让公司出钱。那么事情就简单了，我先买了，凭本事报销不就好了，这可难不倒我，于是一个稀奇古怪的计划就诞生了</p>
<p>制冰机到了之后，我开始了我的报销计划，在群里发了这么个东西</p>
<p><img src="https://imba97.com/uploads/2025/08/ice-1.jpg" alt=""></p>
<p>主打一个没法反驳</p>
<p>然后在制冰机旁边还贴了个这个</p>
<p><img src="/uploads/2025/08/ice-2.jpg" alt=""></p>
<h1>结果</h1>
<p>目前参与的基本都是朋友，同事还没有，争取把公司老大拉过来拍一个</p>
<p>不过之前同事的闺女倒是被我拖过来拍了一个哈哈哈</p>
<h1>参与</h1>
<p>欢迎大家一起参与，可以扫图片二维码或以下链接</p>
<p><a href="https://docs.qq.com/sheet/DZWxhb2tHeEF1VnB3">https://docs.qq.com/sheet/DZWxhb2tHeEF1VnB3</a></p>
<p>痛失网名哈哈哈</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[两年 AI 使用感悟]]></title>
            <link>https://imba97.com/posts/805</link>
            <guid isPermaLink="false">https://imba97.com/posts/805</guid>
            <pubDate>Sat, 12 Jul 2025 11:47:47 GMT</pubDate>
            <content:encoded><![CDATA[<h1>买家秀</h1>
<p>今年是 <span i-simple-icons-githubcopilot></span> Github Copilot 第二年的付费用户了，从 Ask、Edit 到现在的 Agent 模式，AI 编程是越来越好用</p>
<p>刚开始有了 Edit 模式，我还不太爱用，因为有些时候会理解错我的意图，改了一些意料之外的代码</p>
<p>Agent 模式也是一样，个人习惯是一方面，也总感觉刚出来得保守的观望一阵再说</p>
<hr>
<p>直到最近才接触到系统化的 AI 编程，感觉 Agent 真是越来越流畅，越来越符合我的心意了</p>
<p>当然其中有些技巧大家可能早就知道了，这篇文章只是以我的视角来分享一下我的感悟</p>
<h1>AI 编程的特点</h1>
<p>我主要讲两点（doge）</p>
<h2>上下文遗忘</h2>
<p>随着对话的进行，AI 会逐渐<strong>忘记</strong>前面的重要信息。特别是在长时间的编程会话中，AI 可能会忘记项目的整体架构、之前的约定条件，或者已经确定的方案等</p>
<p>之前遇到的比如说，我有两个文档，需求文档和项目文档，一开始我说的“文档”都是指的需求文档，但后面创建项目文档后，再说“文档”时 AI 就会搞混。你就得花时间撤销内容、重新描述</p>
<h2>专注力</h2>
<p>AI 的专注力主要会分配在当前的对话中，如果你在一个对话中频繁切换任务内容，那 AI 很容易分心</p>
<p>比如先让 AI 完成了一项任务，然后突然加了一个临时修改另一块代码任务，AI 有可能会在后续的任务中，修改之前已经完成的文件</p>
<h1>针对性改进</h1>
<h2>知识库</h2>
<p>对于上下文遗忘，我们可以从全局到项目分别建立知识库，其实也就是写文档，当然这个文档一般也是让 AI 去整理</p>
<p>比如每个项目我会整理项目架构、目录结构、依赖库等</p>
<h2>规则</h2>
<p>编辑器基本都有配置 AI 规则的功能，一些细枝末节的一句话解释的内容可以通过配置规则</p>
<p>比如之前 AI 生成命令总是 <code>linux</code> 的，就可以通过配置规则约束，比如使用 <code>PowerShell</code> 命令并且多个要用 <code>;</code> 分隔之类的</p>
<h2>任务流</h2>
<p>针对专注力问题，可以使用预设的任务流，比如配置好新功能、重构、修复等任务，告诉 AI 每一步做什么</p>
<p>拿新功能来说，我们可以配置几个步骤</p>
<ol>
<li>读取新功能知识库（提供目录结构、生成文档路径等约束）</li>
<li>整理需求，生成需求文档</li>
<li>生成代码和测试用例</li>
<li>测试</li>
<li>用户验收</li>
</ol>
<p>这样下来 AI 就会按照我们拆分的更细致的步骤来完成任务，再有需求文档的加持</p>
<p>就算你中间临时修改点东西，也不会让 AI 大幅度的偏离主题</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[我入职了老东家的母公司]]></title>
            <link>https://imba97.com/posts/804</link>
            <guid isPermaLink="false">https://imba97.com/posts/804</guid>
            <pubDate>Thu, 19 Jun 2025 22:05:02 GMT</pubDate>
            <content:encoded><![CDATA[<h1>老东家</h1>
<p>去年离开老东家做了一年的数字游民，准确说是 <span i-material-symbols-light-home-work-rounded bg-primary></span> “数字居家民”。之前文章也提到过，老东家是做餐饮系统开发的，我们部门主要是做像泸溪河、赛百味、皮爷咖啡这些面向员工用的点餐屏</p>
<p>我在职的这一年多的时间里，除了页面功能上开发，主要做的是打通了点餐屏客户端与各种硬件的交互，蓝牙打印机、网口打印机、电子秤、客显屏、读卡器等等</p>
<p>还封装过一些前端库，比如基于 EJS 的 <span i-logos-electron></span> Electron 打印模板库，基于纯文本的打印模板库，新加坡支付系统 NETS 的 POS 机 SDK 等</p>
<h1>母公司</h1>
<p>其实在几天前我还不知道这两家是什么关系，只听说过一耳朵，这两天问了问前同事，才知道是全资母公司</p>
<p>当时老东家的办公场地半数是母公司的人员，门口也有 logo 和名字。最近找工作，突然刷到了这家公司（分公司），看着眼熟就试着投了一下，毕竟开发内容上也会有相似</p>
<h1>面试</h1>
<p>经过了面试题、技术面、HR 面。中间其实也有点波折，因为薪资、地点等综合因素，两次想放弃了</p>
<p>本来以为就这样了，但后续交流中听说部门还是希望我过去，并且起薪也稍微提了一下。感觉能被认可，对我来说比薪资、地点更重要，毕竟这直接关系到 <strong>个人在工作中的</strong> <span i-carbon-cabin-care bg-green></span> <strong>舒适度</strong></p>
<p>非常感谢 HR 姐的积极沟通，以及部门的认可</p>
<p>最后通过了 <span i-mdi-emoticon-devil bg-red></span> BOSS 面，就这样确定了下来</p>
<h1>入职</h1>
<p>入职第一天，看了看入职培训、考了考试、开通了开通帐号、配置了配置开发环境、熟悉了熟悉代码</p>
<p>然后下午接到了个大的，要负责开发一个转换器，简单来说就是把一个项目中的各种代码，转换成另外几种项目类型格式的代码</p>
<p>给了三周时间，下周要开始两眼一睁就是干了</p>
<h1>总结</h1>
<p>牛马要“收到”了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[数字游民报告]]></title>
            <link>https://imba97.com/posts/803</link>
            <guid isPermaLink="false">https://imba97.com/posts/803</guid>
            <pubDate>Fri, 30 May 2025 23:34:18 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前情提要</h1>
<p>一年前我向大家提交了 <a href="/posts/785">《辞职报告》</a>，给自己了一年的时间，探索数字游民的生活方式</p>
<p>这篇文章来总结一下这一年我都干了什么</p>
<h1>久违的暑假</h1>
<p>2024 年 6 月 30 日，上完了最后一天班，我就开始了自己 <a href="/posts/787">《久违的暑假》</a></p>
<p>因为有文章了，这里就不赘述了</p>
<h1><a href="http://imba97.me">imba97.me</a></h1>
<p>考虑到之后要找远程工作，那么最好有个个人网站来专门展示自己的技能和项目</p>
<p>于是就有了 <a href="https://imba97.me">imba97.me</a>，2024 年 7 月至今迭代了 42 个版本 🎉，除了基本的展示，也做了不少有意思的功能</p>
<h2>同步展示在听的歌</h2>
<p><img src="https://imba97.com/uploads/2025/05/digital-nomad-report-1.jpg" alt=""></p>
<h2>同步展示在玩的游戏</h2>
<p>封装了一个简单的 Steam SDK，快速获取正在玩的游戏 <span i-mdi-github></span> <a href="https://github.com/imba97/steam-playing-game">steam-playing-game</a></p>
<p><img src="/uploads/2025/03/steam-game-2.png" alt=""></p>
<h2>AI 简历</h2>
<p><img src="/uploads/2025/05/digital-nomad-report-2.jpg" alt=""></p>
<h1><a href="http://introxd.com">introxd.com</a></h1>
<p><a href="https://introxd.com">introxd.com</a> 这是我想到的第一个项目，个人简介定制</p>
<p><img src="/uploads/2025/05/digital-nomad-report-3.jpg" alt=""></p>
<p>在做 <a href="http://imba97.me">imba97.me</a> 的时候，想到也许其他人也会需要一个快速展示自己的网站，所以就有了这个项目</p>
<p>不过没怎么宣传过，主打一个随缘买卖</p>
<h1>存款逐渐消失</h1>
<p>虽然是在家，但每个月的开销比我想的要多</p>
<p>每个月缴着灵活就业的社保医保 1300+，相当于以前交房租了</p>
<p>在家也懒得做饭还是点外卖，吃喝玩乐每个月跟以前在外面差不多</p>
<p>于是在 2024 年 11 月又开始找工作了</p>
<h1>远程办公</h1>
<p>感觉只要有一个想法，总有一天会不经意间实现</p>
<p>本来我觉得找个坐班工作上班吧，结果就找到一个可以远程的坐班工作</p>
<p>Base 在武汉，当时也去了，是个开放办公室，同事大部分都是远程</p>
<p>于是，从 2025 年初开始...</p>
<h2>大连</h2>
<p><img src="/uploads/2025/05/digital-nomad-report-4.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-5.jpg" alt=""></p>
<h2>昆明</h2>
<p><img src="/uploads/2025/05/digital-nomad-report-6.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-7.jpg" alt=""></p>
<h2>大理</h2>
<p><img src="/uploads/2025/05/digital-nomad-report-8.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-9.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-10.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-11.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-12.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-13.jpg" alt=""></p>
<h2>老家</h2>
<p><img src="/uploads/2025/05/digital-nomad-report-14.jpg" alt=""></p>
<p><img src="/uploads/2025/05/digital-nomad-report-15.jpg" alt=""></p>
<h1>获奖感言</h1>
<p>在老家呆了一个月，其他地方基本是一周，并没有旅居</p>
<p>确实比较自由，但其实一周的时间里出门也就是两三天，然后就懒得出去了</p>
<p>对于我来说收拾行李、去坐飞机高铁、找玩的或办公的地方是一些非常麻烦的事</p>
<p>但去码头整点薯条、小酒馆认识的朋友、一眼望不到头的路、在老家陪陪奶奶，又好像让心里丰富了些什么</p>
<p>可丰富了些什么呢？我现在不能思考，只能在紧张求职的每一天中度过</p>
<p>问题不大，将来的我会想起来</p>
<hr>
<p>感谢各位收看，让我们</p>
<div text="14 center">再见江湖</div>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[PicGo 插件项目重构]]></title>
            <link>https://imba97.com/posts/802</link>
            <guid isPermaLink="false">https://imba97.com/posts/802</guid>
            <pubDate>Sun, 11 May 2025 23:13:45 GMT</pubDate>
            <content:encoded><![CDATA[<h1>FTP 上传插件</h1>
<p>这是一个四年前的项目，当时发现 PicGo 没有我需要的 FTP 上传插件，于是自己写了一个</p>
<p>最开始用的官方提供的插件模板，这是重构前的代码 <span icon i-mdi-github></span> <a href="https://github.com/imba97/picgo-plugin-ftp-uploader/tree/003b4e7b0278c92885efafed851bff84f3c54d4e">imba97/picgo-plugin-ftp-uploader/003b4e</a></p>
<p>前端技术发展迅猛，积累了四年的前端知识，让我有了重构的想法</p>
<h1>Refactor!</h1>
<p>我当前开发前端库主要用以下技术</p>
<ul>
<li><span icon i-vscode-icons-file-type-typescript-official></span> TypeScript 不用多说</li>
<li><span icon i-unjs-unbuild></span> unbuild 打包工具</li>
<li><span icon i-vscode-icons-file-type-eslint></span> eslint + @antfu/eslint-config 代码规范</li>
<li><span icon i-logos-git-icon></span> simple-git-hooks + lint-staged 代码提交规范</li>
<li><span icon i-stash-version-duotone bg-primary></span> bumpp 版本管理</li>
</ul>
<h2>打包工具</h2>
<p>之前是直接用 <code>tsc</code> 编译，unbuild 作为一个现代化的打包工具，提供更灵活、自由度更高的配置、插件等，并能优化代码</p>
<h2>规范</h2>
<p>eslint 作为代码规范工具，开源魔法师托尼老师的 <a href="https://github.com/antfu/eslint-config">@antfu/eslint-config</a> 提供了一套很好的规范，简单易用</p>
<p>simple-git-hooks 创建 git hooks，在提交前检查代码</p>
<p>lint-staged 只检查暂存的文件，避免全局检查</p>
<h2>版本管理</h2>
<p>bumpp 是一个简单的版本管理工具，命令行界面选择版本号，自动生成 git tag 并提交</p>
<h2>NPM 发布文件</h2>
<p><code>package.json</code> 中指定了需要发布的文件，因为打包会生成 <code>esm</code> 和 <code>cjs</code> 两种，避免发布多余文件</p>
<pre><code class="language-json">{
  &quot;main&quot;: &quot;dist/index.cjs&quot;,
  &quot;files&quot;: [
    &quot;dist/index.cjs&quot;,
    &quot;logo.png&quot;
  ]
}
</code></pre>
<h2>代码目录优化</h2>
<p>把原来放在 <code>index.ts</code> 中的 <code>handle</code>、<code>upload</code> 拆分成单独文件</p>
<p>封装 <code>ftp</code> 客户端，让代码看起来更清晰</p>
<h1>PicGo 插件模板</h1>
<p>PicGo 支持插件模板的开发，所以我基于上面的优化做了个插件模板</p>
<p>你可以使用以下指令初始化你的插件</p>
<pre><code class="language-bash">picgo init imba97/picgo-template-plugin picgo-plugin-name
</code></pre>
<p>模板仓库 <span icon i-mdi-github></span> <a href="https://github.com/imba97/picgo-template-plugin">imba97/picgo-template-plugin</a></p>
<h1>顺手</h1>
<p>使用 <code>picgo init</code> 命令时发现一个问题，一个可选参数不写会报错，修了一下 (<a href="https://github.com/PicGo/PicGo-Core/pull/178">#178</a>)</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[让 HR 直接跟我的 AI 聊]]></title>
            <link>https://imba97.com/posts/801</link>
            <guid isPermaLink="false">https://imba97.com/posts/801</guid>
            <pubDate>Sat, 26 Apr 2025 20:40:18 GMT</pubDate>
            <content:encoded><![CDATA[<h1>做个自我介绍</h1>
<p><img src="https://imba97.com/uploads/2025/04/imba97-ai-1.gif" alt=""></p>
<p>做了个求职偷懒 AI，基于我的简历数据的聊天，目前感觉有不少场景都可以用上</p>
<ul>
<li>比如直接发给 HR</li>
<li>比如直接复制工作要求让 AI 给出匹配程度</li>
<li>可以自己跟 AI 聊聊，学习参考 AI 的回答技巧</li>
</ul>
<h1>技术细节</h1>
<p>AI 是 DeepSeek，并使用了 <a href="https://github.com/songquanpeng/one-api">one-api</a>，直接用 <span icon text="#2396ed" i-mdi-docker></span> Docker 跑起来</p>
<p>它可以整合各种 AI 接口，统一成 ChatGPT 的格式，便于之后切换 AI</p>
<p>也算是摸清了流式输出和 SSE 格式处理</p>
<h1>网站</h1>
<p>有兴趣可以体验一下 <a href="https://imba97.me/ai">imba97.me/ai</a></p>
<p>代码可参考开源项目 <span icon i-mdi-github></span> <a href="https://github.com/imba97/me">imba97/me</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[initx v0.1.0 发布]]></title>
            <link>https://imba97.com/posts/800</link>
            <guid isPermaLink="false">https://imba97.com/posts/800</guid>
            <pubDate>Tue, 08 Apr 2025 22:39:17 GMT</pubDate>
            <content:encoded><![CDATA[<h1>可圈可点</h1>
<p><code>initx</code> 是一个免安装的脚本引擎，可以使用 <code>npx</code> 执行，通过 <span icon i-mingcute-plugin-2-fill bg-primary></span> 插件实现各种功能</p>
<p>初期设计了匹配器和插件的核心逻辑</p>
<p>简单且强大的匹配器来匹配用户输入指令，后来单独抽离成了一个项目 <span icon i-mdi-github></span> <a href="https://github.com/imba97/matchinitx">matchinitx</a></p>
<h1>美中不足</h1>
<p><code>initx</code> 可以免安装运行，但你需要全局安装各种插件，这显然是<strong>不合理</strong>的</p>
<p>虽然有一个插件管理器 <code>@initx-plugin/manager</code>，但它甚至也要先全局安装才能使用，这太傻了</p>
<h1>v0.1.0</h1>
<p>在这个版本以及之前的几个版本中，陆续解决了以上的问题</p>
<p>首先插件不是全局安装了，而是被安装在用户目录下的 <code>.initx</code> 目录中</p>
<p>同时执行 <code>npx initx</code> 命令时，如果没有 <code>@initx-plugin/manager</code> 插件则会自动安装</p>
<p>现在你可以随时随地尝试</p>
<pre><code class="language-bash">npx initx plugin list
</code></pre>
<p><strong>如果觉得每次输入 <code>npx</code> 比较麻烦，也推荐全局安装 <code>initx</code></strong></p>
<pre><code class="language-bash">npm i initx -g
</code></pre>
<h1>解决需求</h1>
<p>目前做的几个插件，以解决我自己日常使用需求为主，比如以下几个简单的例子</p>
<h2>复制</h2>
<p>比如复制 SSH 公钥、GPG 公钥、当前目录路径，你可以轻松解决</p>
<pre><code class="language-bash"># 安装插件
initx plugin add cp
# 复制 SSH 公钥
initx cp ssh
# 复制 GPG 公钥
initx cp gpg
# 复制当前目录路径
initx cp cwd
</code></pre>
<h2>git 相关</h2>
<p>安装</p>
<pre><code class="language-bash">initx plugin add git
</code></pre>
<p>设置全局用户名、邮箱</p>
<pre><code class="language-bash">initx user foo foo@bar.com
</code></pre>
<p>初始化或修改远程仓库地址</p>
<pre><code class="language-bash">initx git@github.com:initx-collective/initx.git
# 带有分支
initx git@github.com:initx-collective/initx.git main
</code></pre>
<p>没错，直接这样贴上地址就好了，插件会自动判断是初始化还是修改仓库和分支</p>
<p>开启或关闭 GPG 签名</p>
<pre><code class="language-bash">initx gpg true
initx gpg false
</code></pre>
<p>插件会自动找到 <code>gpg</code> 执行程序的路径，并设置 <code>git</code> 配置</p>
<h2>项目管理</h2>
<p>安装</p>
<pre><code class="language-bash">initx plugin add pm
</code></pre>
<p>设置项目目录</p>
<pre><code class="language-bash"># 相对路径
initx pm add .
# 绝对路径
initx pm add /home/foo/projects
# 指定名称
initx pm add frontend /home/foo/projects-frontend
initx pm add backend /home/foo/projects-backend
</code></pre>
<p>不指定名称默认是 <code>default</code></p>
<p>基于 <code>github</code> 仓库创建项目到目录</p>
<pre><code class="language-bash">initx pm create user/repo
# 别名
initx pm create user/repo as-name
</code></pre>
<p>修改默认项目目录</p>
<pre><code class="language-bash">initx use frontend
# 不指定则修改为 default
initx use
</code></pre>
<p>更多文档可以在 <span icon i-mdi-github></span> <a href="https://github.com/initx-collective">initx-collective</a> 对应的插件仓库中找到</p>
<h1>插件开发</h1>
<p><code>initx</code> 插件开发的方向大致分为两个部分</p>
<ol>
<li>以功能为主，比如以上的 <code>cp</code>、<code>git</code>、<code>pm</code> 插件</li>
<li>以个人为主，因为考虑到需要记各种命令，所以自己开发自己的插件，为自己服务也是一种选择</li>
</ol>
<p>创建插件可以直接使用插件模板 <span icon i-mdi-github></span> <a href="https://github.com/initx-collective/initx-plugin-starter">initx-plugin-starter</a></p>
<p>开发时使用 <code>stub</code> 模式，这样修改完 <code>ts</code> 代码就不需要手动执行编译</p>
<pre><code class="language-bash">pnpm stub
</code></pre>
<p>安装当前目录的插件</p>
<pre><code class="language-bash">initx plugin add .
</code></pre>
<p>可以试试这个命令</p>
<pre><code class="language-bash">initx start
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[在个人简介同步正在玩的 Steam 游戏]]></title>
            <link>https://imba97.com/posts/799</link>
            <guid isPermaLink="false">https://imba97.com/posts/799</guid>
            <pubDate>Thu, 13 Mar 2025 23:24:48 GMT</pubDate>
            <content:encoded><![CDATA[<h1>上回</h1>
<p>之前写过一个同步我当前正在听的音乐，大概长这样</p>
<p><img src="https://imba97.com/uploads/2025/03/steam-game-1.png" alt=""></p>
<h1>这次</h1>
<p>尝试把当前正在玩的 Steam 游戏展示到个人简介中</p>
<p>于是去看了一下 Steam API，发现还真行，效果是这样的</p>
<p><img src="/uploads/2025/03/steam-game-2.png" alt=""></p>
<p><img src="/uploads/2025/03/steam-game-3.png" alt=""></p>
<p>开源：<a href="https://github.com/imba97/me">imba97/me</a></p>
<h1>开源库</h1>
<p>为了方便调用，简单封装了一下用到的 Steam API</p>
<p>你可以直接使用 <a href="https://github.com/imba97/steam-playing-game">steam-playing-game</a> 的 <code>getUserPlayingGame</code></p>
<pre><code class="language-ts">import { getUserPlayingGame } from 'steam-playing-game'

getUserPlayingGame('steam-api-key', 'user-id')
</code></pre>
<p>或者使用 <code>useSteamApi</code> 获取更详细的参数</p>
<pre><code class="language-ts">import { useSteamApi } from 'steam-playing-game'

const { getPlayerSummary, getGameDetail } = useSteamApi('steam-api-key')

getPlayerSummary('user-id')
getGameDetail('game-id')
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[写了个饥荒食谱速查工具]]></title>
            <link>https://imba97.com/posts/797</link>
            <guid isPermaLink="false">https://imba97.com/posts/797</guid>
            <pubDate>Thu, 06 Feb 2025 12:02:10 GMT</pubDate>
            <content:encoded><![CDATA[<h1>使用</h1>
<p>项目地址：<a href="https://dst-recipe.netlify.app">https://dst-recipe.netlify.app</a></p>
<h2>基本搜索</h2>
<p>在下方输入框中输入关键字，可以是名称、拼音、拼音首字母</p>
<p>展示食材基本信息、制作必须食材和条件</p>
<p><img src="https://imba97.com/uploads/2025/02/dst-recipe-1.png" alt=""></p>
<h2>多个结果</h2>
<p>多个结果可以滑动查看</p>
<p><img src="/uploads/2025/02/dst-recipe-2.png" alt=""></p>
<h1>代码分析</h1>
<p>逻辑上来说是分为两块，一个是食材，一个是食谱</p>
<h2>食材</h2>
<p>每种食材有不同的属性，比如肉度、怪物度、鱼度等等</p>
<p>所以就需要一个基类来表示食材</p>
<pre><code class="language-ts">export abstract class IngredientBase extends InstanceClass {
  protected abstract _name: string

  /**
   * 肉度
   */
  protected _meat?: number

  /**
   * 怪物度
   */
  protected _monster?: number

  /**
   * 鱼度
   */
  protected _fish?: number

  // ...
}
</code></pre>
<p>实现类</p>
<pre><code class="language-ts">import icon from '~/assets/images/ingredients/monster-meat.png'
import { IngredientBase } from '~/composables/ingredient/ingredientBaseClass'

export default class MonsterMeat extends IngredientBase {
  protected _name = '怪物肉'
  protected _image = icon

  protected override _meat = 1
  protected override _monster = 1
}
</code></pre>
<h2>食谱</h2>
<p>每种食谱有不同的制作条件，比如所需食材、属性度条件等等</p>
<p>实现方式也相同</p>
<pre><code class="language-ts">export abstract class FoodBase extends InstanceClass {
  private _pinyin: string = ''
  private _pinyinInitials: string = ''

  protected abstract _name: string
  protected abstract _health: number
  protected abstract _hunger: number
  protected abstract _sanity: number
  protected abstract _rot: number
  protected abstract _cooking: number
  protected abstract _priority: number
  protected abstract _image: string

  /**
   * 合并条件
   */
  protected _merge: IngredientTypeKey[][] = []

  /**
   * 所需食材
   */
  protected _ingredientsCondition: IngredientsCondition = []

  /**
   * 仅限沃利
   */
  protected _warlyOnly: boolean = false

  /**
   * 所需肉度
   */
  protected _meat?: ComparisonOperator

  /**
   * 所需怪物度
   */
  protected _monster?: ComparisonOperator

  // ...
}
</code></pre>
<p>除了所需属性度之外，还有一些其他配置，例如 <code>_merge</code>，可以表示某几个属性度任意一个满足即可</p>
<p>比如火鸡正餐</p>
<pre><code class="language-ts">import type { IngredientTypeKey } from '~/enums/ingredientType'
import type { ComparisonOperator } from '~/types/comparisonOperator'
import type { IngredientsCondition } from '~/types/ingredientsCondition'
import icon from '~/assets/images/foods/turkey-dinner.png'
import { FoodBase } from '~/composables/food/foodBaseClass'
import Drumstick from '~/ingredients/drumstick'

export default class TurkeyDinner extends FoodBase {
  _name = '火鸡正餐'
  _health = 20
  _hunger = 75
  _sanity = 5
  _rot = 6
  _cooking = 60
  _priority = 10
  _image = icon

  protected override _ingredientsCondition: IngredientsCondition = [
    {
      ingredients: [
        Drumstick
      ],
      condition: {
        ge: 2
      }
    }
  ]

  protected override _merge: IngredientTypeKey[][] = [
    ['vegetable', 'fruit']
  ]

  protected override _meat: ComparisonOperator = {
    gt: 1
  }

  protected override _vegetable: ComparisonOperator = {
    ge: 0.5
  }

  protected override _fruit: ComparisonOperator = {
    gt: 0
  }
}
</code></pre>
<p>会显示为这样</p>
<p><img src="/uploads/2025/02/dst-recipe-3.png" alt=""></p>
<p><code>_ingredientsCondition</code> 是所需食材，有时也需要表示某几种食材满足一种即可</p>
<p>比如发光浆果慕斯</p>
<pre><code class="language-ts">import type { ComparisonOperator } from '~/types/comparisonOperator'
import type { IngredientsCondition } from '~/types/ingredientsCondition'
import icon from '~/assets/images/foods/glow-berry-mousse.png'
import { FoodBase } from '~/composables/food/foodBaseClass'
import GlowBerry from '~/ingredients/glowBerry'
import LesserGlowBerry from '~/ingredients/lesserGlowBerry'

export default class GlowBerryMousse extends FoodBase {
  _name = '发光浆果慕斯'
  _health = 3
  _hunger = 37.5
  _sanity = 10
  _rot = 8
  _cooking = 20
  _priority = 30
  _image = icon

  protected override _ingredientsCondition: IngredientsCondition = [
    [
      {
        ingredients: [
          GlowBerry
        ],
        condition: {
          ge: 1
        }
      },
      {
        ingredients: [
          LesserGlowBerry
        ],
        condition: {
          ge: 2
        }
      }
    ]
  ]

  protected override _fish: ComparisonOperator = {
    ge: 1
  }

  protected override _notEdible: ComparisonOperator = {
    eq: 0
  }

  protected override _warlyOnly = true
}
</code></pre>
<p>会显示为这样</p>
<p><img src="/uploads/2025/02/dst-recipe-4.png" alt=""></p>
<h1>开源</h1>
<p><span icon i-mdi-github></span> <a href="https://github.com/imba97/dst-recipe">imba97/dst-recipe</a></p>
<h1>数据来源</h1>
<p><span icon i-ph-files-duotone></span> <a href="https://dontstarve.huijiwiki.com/wiki/%E7%83%B9%E9%A5%AA">饥荒中文维基-烹饪</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[写了个不错的匹配器 matchinitx]]></title>
            <link>https://imba97.com/posts/796</link>
            <guid isPermaLink="false">https://imba97.com/posts/796</guid>
            <pubDate>Thu, 16 Jan 2025 01:06:57 GMT</pubDate>
            <content:encoded><![CDATA[<h1>matchinitx</h1>
<p><code>matchinitx</code> 前身是 <span icon i-mdi-github></span> <a href="https://github.com/initx-collective/initx">initx-collective/initx</a> 项目中的功能，在插件中定义了匹配规则，然后 <code>initx</code> 会收集所有插件，根据用户在命令行输入的关键字，匹配、调用对应的插件</p>
<p>来看看它具体能干嘛</p>
<!-- more -->
<h1>使用</h1>
<pre><code class="language-ts">import type { MatcherRules } from 'matchinitx'
import { useInitxMatcher } from 'matchinitx'
</code></pre>
<h2>基础匹配器</h2>
<p>匹配器默认规则类型是这样的，为了演示就简化的写一下</p>
<pre><code class="language-ts">type MatcherRules&lt;T&gt; = T &amp; {
  matching: (string | RegExp)[]
}
</code></pre>
<p>这个对象最后会返回给一个处理函数，但会把 <code>matching</code> 移除</p>
<p>所以我们需要自定义一些需要的字段，并且定义好匹配规则</p>
<pre><code class="language-ts">interface CustomField {
  name: string
}

const rules: MatcherRules&lt;CustomField&gt; = [
  {
    matching: [
      'foo',
      /^bar/
    ],
    name: 'first'
  },
  {
    matching: [
      'test',
      /^t/
    ],
    name: 'second'
  }
]
</code></pre>
<p>然后需要一个处理函数，把它传给 <code>useInitxMatcher</code></p>
<pre><code class="language-ts">interface CustomResult {
  rule: CustomField
  others: string[]
}

function resultFn(rule: CustomField, ...others: string[]): CustomResult {
  return { rule, others }
}

const matcher = useInitxMatcher&lt;CustomResult, CustomField&gt;(resultFn)
</code></pre>
<p><code>useInitxMatcher</code> 传入泛型可以让 <code>match</code> 结果有类型提示</p>
<p>匹配器执行后，通过 <code>resultFn</code> 处理后返回所有命中的结果</p>
<pre><code class="language-ts">matcher.match(rules, 'foo')
matcher.match(rules, 'barfoo')
// [{ rule: { name: 'first' }, others: [] }]

matcher.match(rules, 'test')
// [{ rule: { name: 'second' }, others: [] }]

matcher.match(rules, 'top', 'extra')
// [{ rule: { name: 'second' }, others: ['extra'] }]
</code></pre>
<h2>类型匹配器</h2>
<p>如果你有多个匹配规则，并且这几个匹配规则都对应同一个类型，那么你可以使用以下写法</p>
<pre><code class="language-ts">enum CustomType {
  FOO = 'foo',
  BAR = 'bar'
}

interface CustomField {
  title: string
}

interface CustomResult {
  rule: CustomField
  type: CustomType
  others: string[]
}

function resultFn(rule: CustomField, type: CustomType, ...others: string[]): CustomResult {
  return { rule, type, others }
}

const rules: MatcherRules&lt;CustomField&gt; = {
  [CustomType.FOO]: {
    matching: [
      'f',
      'o'
    ],
    title: 'a good matcher'
  },
  [CustomType.BAR]: {
    matching: [
      'b',
      'a'
    ],
    title: 'a good title'
  }
}

const matcher = useInitxMatcher&lt;CustomResult&gt;(resultFn)

matcher.match(rules, 'f')
matcher.match(rules, 'o')
// [{ rule: { title: 'a good matcher' }, type: 'foo', others: [] }]

matcher.match(rules, 'b', 'extra')
matcher.match(rules, 'a', 'extra')
// [{ rule: { title: 'a good title' }, type: 'bar', others: ['extra'] }]
</code></pre>
<p>具体应用场景，可以参考 <a href="https://github.com/initx-collective/initx-plugin-git/blob/main/src/index.ts">initx-plugin-git</a> 中的代码</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[2024 年度总结]]></title>
            <link>https://imba97.com/posts/795</link>
            <guid isPermaLink="false">https://imba97.com/posts/795</guid>
            <pubDate>Tue, 31 Dec 2024 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<h1>生活</h1>
<p>继续我的</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<p>年初突然动力拉满，开始学各种东西</p>
<ul>
<li>吉他 学会了平凡之路</li>
<li>快板 学会了哒哒个呤哒呤叮哒</li>
<li>曲艺 学会了《同仁堂》、《挡凉》片段</li>
<li>写了俩脱口秀录了视频</li>
</ul>
<p>我又觉得我行了</p>
<p>下半年辞职后 <span icon i-ic-round-directions-bike text-primary></span> 骑车从南京出发回老家，骑了 600 公里</p>
<p><img src="https://imba97.com/uploads/2024/12/report-2024-1.jpg" alt=""></p>
<h1>工作</h1>
<p>辞职前是做点餐屏，泸溪河、赛百味之类的，有时候路过门店看到自己做的功能，还挺有成就感的</p>
<p>6 月 30 辞职，放了个久违的暑假，然后开始尝试数字游民，找远程工作。但并不好找，最后只找了兼职</p>
<p>虽然但是，不上班确实爽，没有了神经紧绷的状态，没有闹铃，每天做自己想做的事，完全是爱好驱动</p>
<p>年底又开始了全职坐班工作，不过理论上也是可以远程的</p>
<h1>心理</h1>
<p>感觉越来越容易内耗，买了本《反内耗》也没看</p>
<p>对于有时间限制的东西会让我难受，比如闹铃、项目进度</p>
<p>经常发生像提前两个小时醒了，我会一直想着一会有个闹铃，就睡不着了</p>
<h1>健康</h1>
<p>入睡时间一直挺长，半小时到两三个小时不等</p>
<p>醒了不困，即使没睡几个小时，很多年没有过”困得睁不开眼“的状态了</p>
<h1>动漫</h1>
<p>看了《双城之战》，太爱金克斯了，必须拿出来说说</p>
<h1>程序</h1>
<p>得益于离职，下半年做了些相对更精致的东西</p>
<ol>
<li>
<p><strong>iconfontx</strong></p>
<p>写的第一个免安装脚本，可以直接执行 <code>npx iconfontx</code></p>
<p>可以将 <code>iconfont</code> 的样式和字体打包压缩成一个 CSS 文件在本地使用</p>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/imba97/iconfontx">imba97/iconfontx</a></p>
<hr>
</li>
<li>
<p><strong>introxd</strong> - Intro 🤣</p>
<p>个人简介定制项目，之前自己做了个简介，所以想着可能有同样需求的人</p>
<p>不过没怎么推广，目前没客人</p>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/introxd/web">introxd/web</a></p>
<p>后来把里面一些不错的组件抽出来，做了个组件库</p>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/introxd/components">introxd/components</a></p>
<hr>
</li>
<li>
<p><strong>initx</strong></p>
<p>一个更方便的脚本引擎，一周写了个不错的匹配器和插件系统</p>
<p>写了一堆插件，主要功能是用简单的指令完成复杂步骤的操作</p>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/initx-collective/initx">initx-collective/initx</a></p>
<hr>
</li>
<li>
<p><strong>个人简介</strong></p>
<p>找远程的时候做了个个人简介，可以很方便的展示自己的技能，同时也是第一个 Nuxt 项目</p>
<p>还做了个赛博朋克页面</p>
<p><span icon i-material-symbols-light-link></span> 链接：<a href="https://imba97.me">imba97.me</a></p>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/imba97/me">imba97/me</a></p>
</li>
</ol>
<h1>2025 目标</h1>
<p>去往更多的城市 💪</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[initx 存储模块]]></title>
            <link>https://imba97.com/posts/794</link>
            <guid isPermaLink="false">https://imba97.com/posts/794</guid>
            <pubDate>Mon, 18 Nov 2024 20:00:08 GMT</pubDate>
            <content:encoded><![CDATA[<h1>简单演示</h1>
<p><code>initx</code> 的插件如果想在硬盘中存取数据，可以使用上下文中的 <code>store</code> 字段</p>
<pre><code class="language-ts">interface Store {
  foo: string
}

export default class StarterPlugin extends InitxPlugin&lt;Store&gt; {
  defaultStore = {
    foo: 'bar'
  }

  matchers = [
    {
      matching: 'start',
      description: 'Plugin starter'
    }
  ]

  async handle({ store }: InitxContext&lt;Store&gt;, ...others: string[]) {
    store.foo = 'baz'
    // ...
  }
}
</code></pre>
<p><code>initx</code> 将自动判断 <code>store</code> 字段是否被更改，如果有更改则会在执行器结束后将新数据保存到硬盘中</p>
<p>默认会先取硬盘数据，所以 <code>store.foo</code> 的值将会改为 <code>baz</code></p>
<h1>实现原理</h1>
<p>在 <code>handle</code> 被调用前，<code>store</code> 会被创建</p>
<pre><code class="language-ts">const store = createStore(context.packageInfo, this.defaultStore)
await this.handle({ ...context, store }, ...others)
writeStore(context.packageInfo.name)
</code></pre>
<p>而 <code>store</code> 其实是个 <code>Proxy</code> 对象，当 <code>store</code> 被更改时会将更改缓存下来</p>
<!-- eslint-disable no-new -->
<pre><code class="language-ts">new Proxy(target, {
  get(target, key) {
    // ...
  },
  set(target, key, value) {
    const success = Reflect.set(target, key, value)
    rewritedCache = target
    return success
  }
})
</code></pre>
<p>最后会调用 <code>writeStore</code> 判断是否有更改</p>
<pre><code class="language-ts">function writeStore(name: string) {
  if (!rewritedCache) {
    return
  }

  writeJson(resolveStore(name), rewritedCache)
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[initx 更方便的脚本引擎]]></title>
            <link>https://imba97.com/posts/793</link>
            <guid isPermaLink="false">https://imba97.com/posts/793</guid>
            <pubDate>Fri, 25 Oct 2024 16:33:01 GMT</pubDate>
            <content:encoded><![CDATA[<h1>简要</h1>
<p>最近发布的 <span icon i-material-symbols-release-alert-rounded bg-amber></span> <a href="https://github.com/initx-collective/initx/releases/tag/v0.0.12">v0.0.12</a> 版本中移除了默认业务功能，转而加入了 <span icon i-mingcute-plugin-2-fill bg-primary></span> 插件系统</p>
<p>用了一周开发到目前的状态，我觉得 <code>initx</code> 的功能好的<strong>超出</strong>了我的预期（自嗨）</p>
<p>我其实很早之前就一直在写一些零散的脚本，把一些复杂的东西简单化，一般是放在仓库，有新环境用到的话要拉代码、配置、执行，感觉还挺麻烦</p>
<p>不管有没有重复造轮子，我非常喜欢 <code>initx</code> 以及享受开发过程 🤣</p>
<h1>插件系统</h1>
<p>把业务功能的选择交给使用者，将插件的包安装到全局即可，类似</p>
<pre><code class="language-bash">npm i @initx-plugin/git -g
</code></pre>
<p>非常骄傲的宣布，从现在开始 <code>initx</code> 是一个<strong>更方便的脚本引擎</strong>，它会负责</p>
<ul>
<li>提供方便的入口</li>
<li>收集插件</li>
<li>收集匹配器命中的执行器</li>
<li>处理可能的用户交互</li>
<li>执行！🚀</li>
</ul>
<p>而你，可以专注于写脚本</p>
<h2>提供方便的入口</h2>
<pre><code class="language-bash">npx initx &lt;something&gt;
</code></pre>
<p>作为免安装脚本，你可以直接使用 <code>npx</code> 执行，虽然它也会被安装在某个角落</p>
<h2>收集插件</h2>
<p><code>initx</code> 会先从全局安装目录加载所有依赖，根据包名过滤出插件，动态引入插件代码</p>
<h2>收集匹配器命中的执行器</h2>
<p><strong>匹配器</strong>和<strong>执行器</strong>是 <code>initx</code> 的两大核心，可以使用字符串和正则表达式，也可以通过数组组合它们</p>
<p>命中后匹配器的介绍和执行器会被收集起来</p>
<h2>处理可能的用户交互</h2>
<p>如果多个执行器被命中，则会清晰的展示插件名和匹配器介绍，让用户选择执行哪一个</p>
<h2>而你</h2>
<p>可以专注于写脚本！</p>
<h1>starter</h1>
<p>插件开发在此起步 <span icon i-logos-github-icon></span> <a href="https://github.com/initx-collective/initx-plugin-starter">initx-plugin-starter</a></p>
<pre><code class="language-ts">import type { InitxCtx } from '@initx-plugin/core'
import { InitxHandler } from '@initx-plugin/core'
import { log } from '@initx-plugin/utils'

export default class StarterHandler extends InitxHandler {
  matchers = [
    {
      matching: 'start',
      description: 'Plugin starter'
    }
  ]

  async handle(ctx: InitxCtx, ...others: string[]) {
    log.info('initx-plugin-starter is running 🎊')

    log.info('ctx')
    console.log(ctx)

    log.info('others')
    console.log(others)
  }
}
</code></pre>
<p>拉取项目后安装依赖</p>
<pre><code class="language-bash">pnpm i
</code></pre>
<p>得益于 <code>unbuild</code> 使用的 <code>jiti</code>，可以直接使用 <code>stub</code> 模式开发，无需每次打包</p>
<pre><code class="language-bash">pnpm stub
</code></pre>
<p>最后把本地包全局安装</p>
<pre><code class="language-bash">npm i . -g
</code></pre>
<p>现在只需要专注插件开发，以及测试你的脚本，比如</p>
<pre><code class="language-bash">npx initx start user --name imba97
</code></pre>
<p>得益于 <span icon i-logos-github-icon></span> <a href="https://github.com/cacjs/cac">cac</a>，这个库帮我们处理好了指令，会输出</p>
<pre><code class="language-text">[ INFO ] initx-plugin-starter is running 🎊
[ INFO ] ctx
{
  key: 'start',
  cliOptions: { '--': [], name: 'imba97' },
  optionsList: []
}
[ INFO ] others
[ 'user' ]
</code></pre>
<p>祝你用餐愉快 🍴</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[initx 更方便的初始化工具]]></title>
            <link>https://imba97.com/posts/792</link>
            <guid isPermaLink="false">https://imba97.com/posts/792</guid>
            <pubDate>Mon, 21 Oct 2024 21:44:29 GMT</pubDate>
            <content:encoded><![CDATA[<h1>首先</h1>
<p><code>initx</code> 是一个免安装脚本，需要 <span icon i-logos-nodejs-icon-alt></span> Node.js 环境</p>
<h1>一直在初始化</h1>
<p>一直以来有个让我难受的点，就是一些初始化总会需要手动操作，比如换工作的新电脑、重装系统、创建新仓库，都需要进行大量重复的准备工作，有些甚至还有很多步骤</p>
<p>虽然大部分只需要配置一次，配置的时候查文档，但看完下次就忘了</p>
<h1>比如</h1>
<p>即使想完成一次简单的 <span icon i-logos-git-icon></span> Git 提交，你也需要先完成以下步骤</p>
<p>创建并到 <code>用户/xxx/.ssh</code> 目录下复制 SSH 公钥，然后添加到远程仓库用户配置中</p>
<pre><code class="language-bash">ssh-keygen -t rsa -b 4096 -C &quot;mail@example.com&quot;
</code></pre>
<p>设置 Git 用户名邮箱</p>
<pre><code class="language-bash">git config --global user.name &quot;imba97&quot;
git config --global user.email &quot;mail@example.com&quot;
</code></pre>
<p>设置 GPG 签名</p>
<pre><code class="language-bash">git config --global commit.gpgsign true
git config --global user.signingkey XXX
git config --global gpg.program D:/GnuPG/gpg.exe
</code></pre>
<p>当然你也可以直接</p>
<pre><code class="language-bash">git config --global --edit
</code></pre>
<p>把之前保存的配置直接粘贴进去，这样还稍微方便一点</p>
<p>但你依然也需要找到不知道备份到哪里的配置文件</p>
<h1>再比如</h1>
<p>你创建了一个远程仓库，你需要在本地创建目录，然后初始化仓库，添加远程仓库地址</p>
<pre><code class="language-bash">git init
git remote add origin git@github.com:user/repository.git
</code></pre>
<p>如果你想修改远程仓库地址，你需要</p>
<pre><code class="language-bash">git remote set-url origin git@github.com:user/repository.git
</code></pre>
<p>不常用的话很难记，特别是一堆参数的</p>
<h1 id="init" text-center>init 🛠</h1>
<p text-center font-bold>非常骄傲的为你介绍</p>
<p><code>initx</code> 是我为自己做的一个能快速完成这些步骤的工具，如果它也能帮到你，那就太好了</p>
<h2>中心思想</h2>
<p>首先 <code>initx</code> 的中心思想是，能用最少的命令完成最多的事情，不需要记忆大量的命令和参数</p>
<h2>比如</h2>
<p>解决上面的“比如”，只需要一下几个简单的命令</p>
<p>创建 SSH 密钥并且执行</p>
<pre><code class="language-bash">npx initx cp ssh
</code></pre>
<p>这样就免去找到文件打开复制的麻烦</p>
<p>然后设置用户名和邮箱</p>
<pre><code class="language-bash">npx initx user imba97 mail@example.com
</code></pre>
<p>设置开启 GPG 签名，会自动找到并配置 GPG 可执行文件</p>
<pre><code class="language-bash">npx initx gpg true
</code></pre>
<p>设置 GPG 签名密钥</p>
<pre><code class="language-bash">npx initx 92038B3E14C0D332542FB082B851A3E43D739400
</code></pre>
<p>这样就完成了所有的配置</p>
<h2>再比如</h2>
<p>解决上面的“再比如”只需要一个命令</p>
<pre><code class="language-bash">npx initx git@github.com:user/repository.git
</code></pre>
<p>如果在没有初始化的目录下执行，会自动初始化仓库</p>
<p>如果已经初始化过，会修改远程仓库地址</p>
<h1>开源</h1>
<p>你可以在 <span icon i-logos-github-icon></span> <a href="https://github.com/initx-collective/initx">initx-collective/initx</a> 查看具体文档和代码</p>
<p>核心机制就是一个匹配器和一个执行器，如果命中了多个匹配器，则显示 <code>description</code> 的内容，让用户选择</p>
<pre><code class="language-ts">export default class GitHandler extends InitxHandler {
  matchers = {
    [GitMatcher.Init]: {
      matching: [
        /^(https?|git):\/\/.*\.git$/,
        /^(git@.*\.git)$/,
        /^ssh:\/\/git@.*\.git$/
      ],
      description: 'Initialize a new git repository'
    },

    [GitMatcher.User]: {
      matching: 'user',
      description: 'Set user name and email for git configuration'
    },

    [GitMatcher.Gpg]: {
      matching: 'gpg',
      description: 'Enable or disable GPG signing for git commits'
    },

    [GitMatcher.GpgKey]: {
      matching: /^[A-F0-9]{40}$/,
      description: 'Set GPG key for git commits'
    }
  }

  async handle({ key }: InitxOptions, type: GitMatcher, ...others: string[]) {
    switch (type) {
      case GitMatcher.Init: {
        repositoryHandle(key, ...others)
        break
      }

      case GitMatcher.User: {
        userHandle(others)
        break
      }

      case GitMatcher.Gpg: {
        const [switchFlag] = others
        gpgHandle(switchFlag)
        break
      }

      case GitMatcher.GpgKey: {
        gpgKeyHandle(key)
        break
      }
    }
  }
}
</code></pre>
<p>主要功能稳定后可能会支持插件开发，你可以自定义自己的指令包发布到 npm 上</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Cloudflare Axios 请求问题]]></title>
            <link>https://imba97.com/posts/791</link>
            <guid isPermaLink="false">https://imba97.com/posts/791</guid>
            <pubDate>Tue, 01 Oct 2024 11:03:20 GMT</pubDate>
            <content:encoded><![CDATA[<h1>事情经过</h1>
<p>之前 Nuxt 项目中使用了 <code>axios</code> 在 Server 端发送请求，包括获取数据和图片两种</p>
<p>但部署到 Cloudflare 后，发现会报错</p>
<pre><code class="language-text">the 'credentials' field on 'requestinitializerdict' is not implemented.
</code></pre>
<p>应该是 Cloudflare Workers 不支持 <code>credentials</code> 这个字段，但 <code>axios</code> 默认会有</p>
<h1>尝试解决</h1>
<h2>Axios Adapter</h2>
<p>使用 <code>@haverstack/axios-fetch-adapter</code> 可以解决请求问题，这个库会换成 <code>fetch</code> 请求</p>
<p>但换成这个库后，图片会拿不到，所以暂时放弃了</p>
<h1>解决方法</h1>
<p>抛弃 <code>axios</code>，简单封装了个 <code>fetch</code> 请求</p>
<div flex items-center gap-1>
  <span icon i-mdi-github></span>
  <a href="https://github.com/imba97/me/commit/0e35aec4a8ea20a292e60bdfae67da5cc692c0d1">commit 0e35ae</a>
</div>
<p>后面发现，其实图片是可以拿到的，只是最后要套一个 <code>Buffer.from(responseArrayBuffer)</code></p>
<p>所以理论上 <code>@haverstack/axios-fetch-adapter</code> 应该也可以，拿到结果后转一下，虽然没试</p>
<p>但反正只是简单的请求，已经自己封装了，而且能去掉一个库也挺好</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Github Actions 部署 NuxtHub 项目使用环境变量]]></title>
            <link>https://imba97.com/posts/790</link>
            <guid isPermaLink="false">https://imba97.com/posts/790</guid>
            <pubDate>Mon, 30 Sep 2024 21:34:39 GMT</pubDate>
            <content:encoded><![CDATA[<h1>事情经过</h1>
<p>最近在研究 NuxtHub 惹出来的事，部署过 <a href="https://introxd.com">introxd.com</a> 之后爽到了，所以就想把我的简介 <a href="https://imba97.me">imba97.me</a> 也部署到上面</p>
<p>但我的页面会用到加密环境变量，我在 NuxtHub 添加了环境变量，实际 NuxtHub 是调用 <span class="icon i-devicon-cloudflare"></span> Cloudflare 接口，环境变量会互相同步</p>
<p>在本地直接跑部署命令是可以的，但用 <span icon i-logos-github-actions></span> Github Actions 部署后，环境变量就没了，打印 <code>process.env</code> 是个空对象 <code>{}</code></p>
<h1>尝试解决</h1>
<h2>命名规则</h2>
<p>使用 <code>NUXT_</code> 开头的命名规则，但无效</p>
<h2>官方方法</h2>
<p>使用 <a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-nuxt-site/#access-bindings-in-your-nuxt-application">Cloudflare 文档</a> 中写的方法，可以拿到</p>
<p>但这种写法只能在 <code>defineEventHandler</code> 中使用</p>
<pre><code class="language-ts">export default defineEventHandler(({ context }) =&gt; {
  const MY_KV = context.cloudflare.env.MY_KV
})
</code></pre>
<p>但我是写了个配置，类似</p>
<pre><code class="language-ts">const commonConfig = {
  MY_KV: process.env.MY_KV
}
</code></pre>
<p>改成函数式，用的时候调用函数传值虽然可以，但<strong>不优雅</strong></p>
<h2>runtimeConfig</h2>
<p>网上有说需要在 <code>nuxt.config.ts</code> 的 <code>runtimeConfig</code> 里面添加</p>
<pre><code class="language-ts">export default defineNuxtConfig({
  runtimeConfig: {
    MY_KV: process.env.MY_KV
  }
})
</code></pre>
<p>但我试了，最终使用 <code>useRuntimeConfig</code> 会取到空字符串，因为 <code>process.env</code> 还是空对象</p>
<p>也试了把 <code>useRuntimeConfig</code> 从顶层放在函数中，也不行</p>
<h1>解决方法</h1>
<p>本地的打的包部署上去，<code>process.env</code> 是有值的，<code>runtimeConfig</code> 也能正常取到，所以考虑是 Github Actions 环境没有环境变量</p>
<p>测了一下，在打包的命令下加了，确实能拿到 <code>TEST</code></p>
<pre><code class="language-yml">env:
  TEST: test
</code></pre>
<p>所以就取了个巧，直接在 Github Actions 里面设置加密字段，直接把本地 <code>.env.local</code> 的内容复制进去</p>
<p>再在 Workflow 加个步骤，在打包之前写进 <code>.env</code> 文件里</p>
<pre><code class="language-yml">- name: Write NUXT_PROJECT_ENV to .env
  run: echo &quot;${{ secrets.NUXT_PROJECT_ENV }}&quot; &gt;&gt; .env
</code></pre>
<p>然后在配置 <code>runtimeConfig</code></p>
<pre><code class="language-ts">export default defineNuxtConfig({
  runtimeConfig: {
    env: {
      NAVIDROME_API_URL: process.env.NAVIDROME_API_URL,
      NAVIDROME_USERNAME: process.env.NAVIDROME_USERNAME,
      NAVIDROME_PASSWORD: process.env.NAVIDROME_PASSWORD
    } as Record&lt;string, string&gt;
  }
})
</code></pre>
<p>最后在用的地方使用</p>
<pre><code class="language-ts">const runtimeConfig = useRuntimeConfig()

export const axiosInstance = axios.create({
  baseURL: runtimeConfig.env.NAVIDROME_API_URL
})
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[NuxtHub (Cloudflare) 部署 Nuxt 项目卡住的问题]]></title>
            <link>https://imba97.com/posts/789</link>
            <guid isPermaLink="false">https://imba97.com/posts/789</guid>
            <pubDate>Fri, 20 Sep 2024 14:50:30 GMT</pubDate>
            <content:encoded><![CDATA[<h1>事情经过</h1>
<p>很早之前看到过 NuxtHub，但注册了半截，到绑定 <span class="icon i-devicon-cloudflare"></span> Cloudflare 那就暂时弃坑了</p>
<p>最近 <span class="icon i-vscode-icons-file-type-nuxt"></span> Nuxt 用的非常上头，同时也在用 Cloudflare，昨天逛 Github 发现个项目，看到官网下面写着 <code>Deployed on NuxtHub</code></p>
<p>一时间来了兴趣，想看看 NuxtHub 是个啥</p>
<h1>NuxtHub</h1>
<p>NuxtHub 是一个对接 Cloudflare Workers、Pages 的后台，同时提供 Nuxt 模块，比如数据库、KV、AI 等</p>
<p>可以非常方便的部署包含前后端的 Nuxt 项目</p>
<h1>问题</h1>
<p>把最近在做的 Nuxt 项目部署上去的时候发现，本地打包后用命令部署是没问题的</p>
<p>但用 Cloudflare 部署就会卡在下面日志的地方</p>
<pre><code>...
15:53:04.900 [info] vite v5.4.6 building for production...
15:53:04.930 [info] transforming...
15:53:06.277 Deprecation [legacy-js-api]: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.
15:53:06.324 Deprecation [legacy-js-api]: The legacy JS API is deprecated and will be removed in Dart Sass 2.0.0.
16:29:21.641 Failed: build exceeded the time limit and was terminated. Refer to https://developers.cloudflare.com/pages/platform/limits/#builds for build limits
</code></pre>
<p>最后会构建超时，部署失败</p>
<h1>排查</h1>
<p>因为本地打包也会报这个 <span class="icon i-vscode-icons-file-type-sass"></span> Sass 的警告，所以本来感觉不是这个的问题</p>
<p>后面尝试用了官方的 Demo 部署，发现正常，经过对比发现是 Sass</p>
<p>代码中使用 <code>&lt;style lang=&quot;scss&quot;&gt;</code> 就会有这个警告，但又不能不用</p>
<h1>解决</h1>
<p>搜了一下发现是要在 <code>nuxt.config.js</code> 中加上</p>
<pre><code class="language-ts">{
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          api: 'modern-compiler'
        }
      }
    }
  }
}
</code></pre>
<p>这样确实不报警告了，<code>transforming...</code> 后面也会正常走，但新问题是都完成后进程不会退出，还是一直卡在那</p>
<p>于是我想了个折中的方案，我自己写个脚本跑构建命令，通过日志输出判断是否构建成功，再手动退出进程</p>
<pre><code class="language-ts">import { spawn } from 'node:child_process'
import process from 'node:process'

const child = spawn('npx', ['nuxi', 'build'])

child.stdout.on('data', (data) =&gt; {
  process.stdout.write(data)

  if (!~data.indexOf('npx nuxthub deploy')) {
    return
  }

  console.log('Build Success')

  setTimeout(() =&gt; {
    process.exit(0)
  }, 3000)
})
</code></pre>
<p>搞定 ✔</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[阿里图标库工具 iconfontx]]></title>
            <link>https://imba97.com/posts/788</link>
            <guid isPermaLink="false">https://imba97.com/posts/788</guid>
            <pubDate>Mon, 16 Sep 2024 14:24:40 GMT</pubDate>
            <content:encoded><![CDATA[<h1>iconfontx</h1>
<p>iconfontx 是一个免安装脚本，能将 <code>iconfont</code> 的样式和字体打包压缩成一个 <code>CSS</code> 文件在本地使用</p>
<h1>场景</h1>
<p>使用 <code>iconfont</code> 图标时，可以直接引入 <code>iconfont</code> 提供的 <code>CSS</code> 文件，但如果线上 <code>CDN</code> 出了问题或用户网络波动，会导致图标加载不出来的问题</p>
<p>所以按我之前公司的做法是，把样式和字体放在本地，并且会手动把字体文件转为 <code>base64</code>，并且每次添加图标后需要重复操作更新本地样式</p>
<h1>使用</h1>
<h2>help</h2>
<pre><code class="language-bash">npx iconfontx -h
</code></pre>
<p>展示帮助信息</p>
<pre><code>Usage:
  $ iconfontx &lt;url&gt;

Commands:
  &lt;url&gt;  iconfont css url

For more info, run any command with the `--help` flag:
  $ iconfontx --help

Options:
  -o, --output &lt;filename&gt;  Output file path (default: D:\Projects\example\iconfont.css)
  -v, --version            Display version number
  -h, --help               Display this message
</code></pre>
<h2>生成</h2>
<p>首先复制 <code>iconfont</code> 提供的 <code>CSS</code> 文件链接</p>
<p><img src="https://imba97.com/uploads/2024/09/iconfontx-1.png" alt=""></p>
<p>执行命令</p>
<pre><code class="language-bash">npx iconfontx //at.alicdn.com/t/xxx.css
</code></pre>
<p>默认会在当前目录生成 <code>iconfont.css</code> 文件，可以通过 <code>-o</code> 参数指定输出文件路径</p>
<p>引入样式后可以直接使用图标</p>
<p><img src="/uploads/2024/09/iconfontx-2.png" alt=""></p>
<p>并且会压缩样式代码</p>
<p><img src="/uploads/2024/09/iconfontx-3.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[久违的暑假]]></title>
            <link>https://imba97.com/posts/787</link>
            <guid isPermaLink="false">https://imba97.com/posts/787</guid>
            <pubDate>Fri, 16 Aug 2024 13:41:53 GMT</pubDate>
            <content:encoded><![CDATA[<h1>暑假</h1>
<p>大概有七年没放过暑假了，毕竟打工人没有暑假</p>
<p>辞职后的打算是先放个暑假，转眼也过了一个半月了，盘点一下我的暑假做了什么</p>
<h1>Vue Conf</h1>
<p>看了一场 Vue Conf，就是 Vue 核心团队的大佬们分享一些技术</p>
<p>见到了业内大佬 <a href="https://github.com/antfu">Anthony Fu</a>（托尼老师）！</p>
<p><img src="https://imba97.com/uploads/2024/08/summer-day-1.jpg" alt=""></p>
<p>还有著名 Rapper（狗头） <a href="https://github.com/yyx990803">Evan You</a>（尤大）！</p>
<p><img src="/uploads/2024/08/summer-day-2.jpg" alt=""></p>
<h1>骑行</h1>
<p>从南京 <span icon i-ph-person-simple-bike-duotone bg-primary></span> 骑回了家，大概 560 多公里</p>
<p>原因是之前买了个自行车想骑着上下班，但最后也没怎么骑几次，卖了有点可惜，索性就骑回去了</p>
<p>慢悠悠骑的，一共用了 11 天，骑了 9 天，休息了 2 天，感觉把这辈子的自行车都骑完了</p>
<p><img src="/uploads/2024/08/summer-day-3.jpg" alt=""></p>
<h1>找工作</h1>
<p>骑行中途在宾馆休息的时候，构思了一个个人简介网站 <a href="https://me.imba97.com">me.imba97.com</a></p>
<p>时不时的刷刷各种社区、招聘软件。八月初找到个兼职，目前差不多半天做自己的东西半天兼职</p>
<p>布置了一下办公环境，买了人体工学椅和升降桌，可以罚站了</p>
<p><img src="/uploads/2024/08/summer-day-4.jpg" alt=""></p>
<h1>开源</h1>
<p>做兼职的时候发现一些需求，写了几个小工具</p>
<p>重构了之前的工具，现在叫 <span icon i-mdi-github></span> <a href="https://github.com/imba97/iconfontx">iconfontx</a>，可以拉取 <code>iconfont</code> 样式、转换字体、压缩代码的工具</p>
<p>还有 <span icon i-mdi-github></span> <a href="https://github.com/imba97/lodash-imports">lodash-imports</a>，配 <code>lodash</code> 自动引入的时候感觉太麻烦了，就写了个工具</p>
<p>还有 <span icon i-mdi-github></span> <a href="https://github.com/imba97/eslint-config">@imba97/eslint-config</a>，代码规范规则，引用了大佬的配置，然后根据自己的需要改了点</p>
<p>给 <span icon i-mdi-github></span> <a href="https://github.com/unjs/unimport">unimport</a> 提了个 <a href="https://github.com/unjs/unimport/pull/360">PR</a>，修复了一些小问题</p>
<p>给 <span icon i-mdi-github></span> <a href="https://github.com/cipami/nuxt-lodash">nuxt-lodash</a> 提了个 <a href="https://github.com/cipami/nuxt-lodash/pull/64">PR</a>，修复了服务端不生效的问题</p>
<h1>精神状态</h1>
<p>果然辞职才能治百病，我一直是入睡困难，至少是半小时到一小时起步</p>
<p>也经常会失眠，特别是周日晚上，就感觉第二天要上班，老想着有个闹铃会响、有个时间点必须起床，心里就膈应，然后就睡不着了</p>
<p>而且晚睡心脏还会不舒服，有种肌肉劳累的酸痛感，快睡着的时候如果醒了，感觉心脏跳的很快，但实际摸着并没有</p>
<p>现在几乎啥症状都没了，就八月失眠一次</p>
<p><img src="/uploads/2024/08/summer-day-5.jpg" alt=""></p>
<h1>暑假后</h1>
<p>首要任务就是找个远程全职工作了，有稳定收入就能出去旅居办公了，朝目标一点点靠近吧</p>
<p>就这样，告辞</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Nuxt Docker 自动化]]></title>
            <link>https://imba97.com/posts/786</link>
            <guid isPermaLink="false">https://imba97.com/posts/786</guid>
            <pubDate>Tue, 30 Jul 2024 11:11:23 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Nuxt</h1>
<p><span icon i-logos-nuxt-icon></span> Nuxt 是一个集成了前后端的框架，对于一些小型网站，不需要前后端分离的，是个好的选择</p>
<p>比如最近就做了一个 <a href="https://me.imba97.com">me.imba97.com</a>，准备放一些关于我的一些东西</p>
<!--more-->
<h1>Docker</h1>
<p>因为涉及到后端，需要 Node 环境，借助 <code>GPT</code> 简单写了个 <span icon i-logos-docker-icon></span> Dockerfile</p>
<pre><code class="language-dockerfile">FROM node:18

# 创建并设置工作目录
WORKDIR /usr/src/app

# 复制所有源代码到工作目录
COPY . .

# 安装 pnpm
RUN npm install -g pnpm

# 安装项目依赖
RUN pnpm install

# 构建 Nuxt
RUN pnpm run build

# 暴露端口（默认端口为 3000，可以通过环境变量 NUXT_PORT 配置）
ENV NUXT_PORT=3000
EXPOSE ${NUXT_PORT}

# 启动 Nuxt.js 应用程序
CMD [ &quot;pnpm&quot;, &quot;start-docker&quot; ]
</code></pre>
<p><code>start-docker</code> 是以下命令</p>
<pre><code class="language-sh">nuxt start --hostname 0.0.0.0 --port $NUXT_PORT
</code></pre>
<h1>Github Actions</h1>
<p>配置了 <span icon i-logos-github-actions></span> Github Actions 自动打包镜像并发布</p>
<pre><code class="language-yaml">name: Build and Push Docker Image

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Build the project
        run: npm run build

      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and tag Docker image
        run: |
          TAG_NAME=$(echo $GITHUB_REF | sed 's/refs\/tags\///')
          docker build . -t ${{ secrets.DOCKER_USERNAME }}/me:latest
          docker tag ${{ secrets.DOCKER_USERNAME }}/me:latest ${{ secrets.DOCKER_USERNAME }}/me:$TAG_NAME

      - name: Push Docker image to Docker Hub
        run: |
          TAG_NAME=$(echo $GITHUB_REF | sed 's/refs\/tags\///')
          docker push ${{ secrets.DOCKER_USERNAME }}/me:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/me:$TAG_NAME
</code></pre>
<h1>部署脚本</h1>
<p>我是部署在家里路由器上的，为了方便又简单写了个脚本</p>
<pre><code class="language-sh">#!/bin/sh

docker stop imba97-me
docker rm imba97-me

docker pull imba97/me:latest

docker run -d --name imba97-me -p 3000:3000 --restart always imba97/me:latest
</code></pre>
<h1>其他</h1>
<p>虽然之前看到过有自动更新容器的方法，不过目前先这样了</p>
<p>Nuxt Docker 镜像倒是也有不少，但还是自己搞一个单独的了</p>
<h1>开源</h1>
<p>有兴趣可以看一下 <span icon i-mdi-github></span> <a href="https://github.com/imba97/me">imba97/me</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[辞职报告]]></title>
            <link>https://imba97.com/posts/785</link>
            <guid isPermaLink="false">https://imba97.com/posts/785</guid>
            <pubDate>Wed, 03 Jul 2024 19:05:51 GMT</pubDate>
            <content:encoded><![CDATA[<h1>为什么辞职</h1>
<p>在这个经常能听到<strong>大环境不行</strong>的时间点，辞职了</p>
<p>去年的年度总结中立了个目标希望今年实现 —— 成为数字游民</p>
<p>试着朝这个方向走走，万一实现了自己想要的生活了呢</p>
<!-- more -->
<h1>数字游民</h1>
<p>定义来看就是相对坐班跟公司的合同制，变成用自己的技术、用网络办公、不受地域限制的自由职业者</p>
<p>还有一种就是远程办公，或在家定点上下班，或结果导向的时间内完成任务</p>
<p>有的人会把这两种严格隔离开，但其实我感觉没必要区分的那么严格，都是数字游民</p>
<h1>做的准备</h1>
<p>没做什么太多准备，也很难有准备好的时候</p>
<p>社保先自己缴灵活就业，后面看是找全职远程工作还是自己干</p>
<p>钱方面攒了五万左右，起步资金三万，剩下两万备用金，给自己一年时间，如果没法稳定收支再去找工作</p>
<h1>未来打算</h1>
<p>对于我来说，前期优先考虑找远程办公的公司，作为一个过渡还是不错的</p>
<p>但是！先给自放个暑假！两个月里到处玩玩。时不时写写简历，时不时看看工作</p>
<p>也好好权衡一下，到底是全职远程还是自己干，自己干的话要做什么</p>
<h2>全职远程工作</h2>
<p>全职远程的话主要优势就是以下几点，其他基本跟上班一样，没啥好说的</p>
<ul>
<li>在家也可以有个良好的作息</li>
<li>节省通勤时间和支出</li>
<li>有机会拿一线城市的收入在老家花实现地理套利</li>
</ul>
<p>下面主要说说自己干的话会做什么</p>
<h2>主线 - 做一个软件</h2>
<p>目前构思了一个工具类型的软件，多设备通信引擎。主要是自己用得到，让我自己爽</p>
<p>这个软件的核心思想是，<strong>能点一下绝对不点两下，能自动绝对不手动</strong></p>
<p>会向商业化方向走，但可能没法很快带来金钱上的收益</p>
<h2>支线 - 开源和自媒体</h2>
<p>你可能下意识的觉得这两个是完全没前景的东西，不做到顶流很难有收益，也确实如此</p>
<p>但对我来说，开源和自媒体都是<strong>很好的辅助</strong>，两种都是<strong>成长型选手</strong>。同时存在多种能力：学习交流、扩大影响力、创收等。我不需要过分投入，只是围绕主线附带做一些事情，与主线相辅相成</p>
<hr>
<h3>开源</h3>
<p><span icon i-ic-outline-local-fire-department></span> 扩大影响力：能最直观的通过代码展示自己能力的方式</p>
<p><span icon i-ic-round-attach-money></span> 创收能力：前期无，后期主要来自持续维护项目的赞助</p>
<p>写代码肯定需要把一些通用的东西封装起来，可以把这部分做开源。反正写进项目代码里也是写，单独拎出来封装成库也是写</p>
<p>开源也能吸引同样需要这部分功能的人一起使用、开发，完善一些我没想到的功能，也是一种学习，一种正循环</p>
<h3>自媒体</h3>
<p><span icon i-ic-outline-local-fire-department></span> 扩大影响力：相对于开源，能更清晰、全面的展示自己的所思所想</p>
<p><span icon i-ic-round-attach-money></span> 创收能力：前期较少，后期有粉丝后有多种创收方式</p>
<p>自媒体是一种总结，记录在做开发中遇到了什么问题、如何解决、怎么优化等。总结记录围绕一个目标的各种问题，形成系统的技术文章、教程视频，进一步整理甚至可以是出书、网课</p>
<p>记录本身也是个不错的习惯，加深记忆。有时候我自己遇到之前记录过的问题，直接来搜一下就能快速解决。我博客的搜索大概只有我在用</p>
<h1>获奖感言</h1>
<p>我觉得做一件事是自己感兴趣的，需要花时间积累的，还能混口饭吃的，那就是再好不过了</p>
<p>每次我发一些自己做的破烂小玩意儿，各位的反馈也是推动我进步的重要因素</p>
<p>感谢各位支持，那我们</p>
<div text="14 center">江湖再见</div>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[写了个页面分享此时此刻我在听的歌]]></title>
            <link>https://imba97.com/posts/784</link>
            <guid isPermaLink="false">https://imba97.com/posts/784</guid>
            <pubDate>Tue, 18 Jun 2024 20:59:26 GMT</pubDate>
            <content:encoded><![CDATA[<h1>演示</h1>
<p>因为是个网页，所以可以通过 <code>iframe</code> 分享到各处</p>
<iframe src="https://imba97.me/playing" b-none w-full h="lt-md:160 md:180" rounded-8></iframe>
<p>页面是 <a href="https://imba97.me/playing">https://imba97.me/playing</a></p>
<h1>原理</h1>
<p>最近发了篇博客文章：<a href="/posts/782">《自建音乐服务 Navidrome》</a>，里面提到可以使用 <code>last.fm</code> 来保存听歌历史</p>
<p>通过这个网站的接口可以拿到播放列表，根据听歌的先后顺序，也有当前是否正在播放的字段</p>
<p><img src="https://imba97.com/uploads/2024/06/playing-1.png" alt=""></p>
<h2>后端</h2>
<p>让 <code>Chat GPT</code> 用 <code>PHP</code> 写了个程序，功能就是简单的请求接口、10 秒数据缓存</p>
<p>因为 <code>last.fm</code> 接口给的图片国内没法访问，所以图片优先获取 <code>itunes</code> 上的，如果都没图，保底还会有个 <code>icon</code></p>
<h2>前端</h2>
<p>用 <span icon i-devicon-vuejs></span> 简单写了个前端，10 秒轮询调接口获取最新数据</p>
<p>可以展示加载中、当前未听歌、当前正在听歌状态</p>
<p>用了 UnoCSS，永远的神！论 UnoCSS 实现一个旋转效果能有多简单</p>
<div mb-6 flex items-center justify-center>
  <div rounded-full b="3 primary solid" text-primary>
    <div i-ph-music-note-simple-duotone h-20 w-20 animate-spin animate-duration-30000></div>
  </div>
</div>
<pre><code class="language-html">&lt;div i-ph-music-note-simple-duotone h-20 w-20 animate-spin animate-duration-30000&gt;&lt;/div&gt;
</code></pre>
<h1>开源</h1>
<p><span icon i-mdi-github></span> Github: <a href="https://github.com/imba97/playing-music">imba97/playing-music</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[吃灰树莓派再就业]]></title>
            <link>https://imba97.com/posts/783</link>
            <guid isPermaLink="false">https://imba97.com/posts/783</guid>
            <pubDate>Mon, 10 Jun 2024 21:11:58 GMT</pubDate>
            <content:encoded><![CDATA[<h1>简报</h1>
<p>今天一看这个树莓派是 2019 年买的，折腾过内网穿透、远程开机、PHP 环境、Cloudreve 网盘，甚至还有一个 魔兽争霸3 服务器 <span icon i-solar-gamepad-bold-duotone bg-primary></span> PvPGN</p>
<p>以上本博客大部分都有过记录，有兴趣可以自行搜索一下</p>
<p>后面玩了 N1 盒子、All in One 小主机之后，树莓派就吃灰了</p>
<h1>不间断电源</h1>
<p>前几天无意间发现了树莓派 <span icon i-ri-battery-2-charge-fill bg-green></span> 不间断电源</p>
<p>虽然当时还没想到怎么玩，但我感觉一定能做点有意思的东西</p>
<p>于是把树莓派去壳，接上电源，让我极度舒适的是它可以用引脚供电，没有一根线</p>
<p><img src="https://imba97.com/uploads/2024/06/raspberrypi-1.jpg" alt=""></p>
<p>因为之前的散热风扇在壳上，去壳后没法固定，所以又入手了个风扇，还带灯</p>
<p>还挺好看的，风扇和灯有单独开关控制，所以也不用担心没接电的时候更费电</p>
<h1>便携屏</h1>
<p>这个应该是稚晖君开源的那个，以前直接买的现成的，是个屏同时也是个充电宝</p>
<p>可以在出问题，SSH 连不上的时候做一些简单的调试</p>
<p><img src="/uploads/2024/06/raspberrypi-2.jpg" alt=""></p>
<h1>瞎玩</h1>
<p>装起来之后先搞了个 <code>1Panel</code> 试试水，很早就开始关注了，安装过一次，那时候还不太好用就弃了</p>
<p>期间经常看他们项目在维护，感觉差不多了，所以再试试，还不错</p>
<p><img src="/uploads/2024/06/raspberrypi-3.jpg" alt=""></p>
<p>目前安装了一些服务，什么青龙面板、Web 服务之类的简单玩一玩</p>
<p>到目前还没体现出电源的优势，最多只是不用的时候放那边充电，想玩了随时拔了玩</p>
<p>说实话也挺爽，我的爽点在一根线也没有</p>
<h1>设想</h1>
<p>目前正在尝试一些想法 <span icon i-mage-light-bulb bg-yellow></span></p>
<p>比如利用 <code>openvpn</code> 实现一个异地组网，出门在外只要树莓派连上 WIFI 就能访问家里的局域网</p>
<p>再配置一下端口转发，让树莓派同网段内的设备，不用通过任何 APP 也可以使用家里或其他地方的局域网</p>
<h1>更远的设想</h1>
<p>之后打算找远程工作，如果旅居办公的话，这就会是一个随身携带的算力</p>
<p>毕竟我能便携的工作电脑是个配置不高的 <span icon i-fluent-tablet-48-regular bg-primary></span> ARM 平板，虽然跑个前端项目绰绰有余，但能有个东西分担一下也不错</p>
<p>在上面装个 code-server，平板就可以直接打开 VSCode 远程</p>
<hr>
<p>到时候真就可以出门只带手机、电脑、树莓派加一根 <span icon i-emojione-electric-plug></span> 电源线，就够了</p>
<p>最多带个上图的便携显示器，还能当个应急电源</p>
<h1>访问一下</h1>
<p>树莓派上随便搭了个 Web 服务，做了个内网穿透，如果我的派开着，你应该能访问到它</p>
<p><a href="https://pi.npc.imba97.com">https://pi.npc.imba97.com</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[自建音乐服务 Navidrome]]></title>
            <link>https://imba97.com/posts/782</link>
            <guid isPermaLink="false">https://imba97.com/posts/782</guid>
            <pubDate>Sat, 01 Jun 2024 11:41:36 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Navidrome</h1>
<p>Navidrome 是一个支持自建的 <span icon i-ph-music-notes-duotone bg-primary></span> 音乐服务</p>
<p>除了自带的页面，还兼容很多支持连接自建服务的音乐软件</p>
<!--more-->
<h1>软件截图</h1>
<p>我目前用的是这两个，点击查看大图</p>
<MusicClientList />
<h1>音乐刮削器</h1>
<p>用的是 Music Tag Web，也是可通过 <code>Docker</code> 部署的应用，可以自动或手动搜索、写入音乐信息</p>
<p>可以点击标题右侧的 <span icon i-material-symbols-line-end-arrow-rounded></span> 右箭头搜索音乐信息，在右侧列表找到合适的后点击 <span icon i-material-symbols-line-start-arrow-rounded></span> 左箭头同步信息保存</p>
<p>这样就有音乐的相关信息，封面、歌手、专辑、年份等信息基本都有了</p>
<p><img src="https://imba97.com/uploads/2024/06/navidrome-3.png" alt=""></p>
<h1>搭建</h1>
<h2>Navidrome</h2>
<p>Navidrome 可以通过 <code>Docker</code> 部署，可以连接 <a href="https://www.last.fm/">last.fm</a> 和 <a href="https://listenbrainz.org/">ListenBrainz</a> 记录播放历史、生成音乐报告等</p>
<pre><code class="language-yaml">version: '3'
services:
  navidrome:
    image: deluan/navidrome:latest
    privileged: true
    ports:
      - '4533:4533'
    restart: unless-stopped
    environment:
      ND_SCANSCHEDULE: 1h
      ND_LOGLEVEL: info
      ND_SESSIONTIMEOUT: 24h
      ND_BASEURL: ''
      ND_CONFIGFILE: /data/navidrome.toml
      # 开启分享
      ND_ENABLESHARING: true
      # last.fm 配置
      ND_LASTFM_APIKEY: xxx
      ND_LASTFM_SECRET: xxx
      ND_LASTFM_LANGUAGE: zh
    volumes:
      - '/navidrome/data:/data'
      - '/NAS/music:/music:ro'
</code></pre>
<h2><a href="http://last.fm">last.fm</a></h2>
<p>在 <a href="https://www.last.fm/api/account/create">https://www.last.fm/api/account/create</a> 申请 API Key 和 Secret</p>
<p>Callback URL 好像可以随便写，因为最后在设置里登录的时候，会在 URL 后面带一个</p>
<p>申请后在 <a href="https://www.last.fm/api/accounts">https://www.last.fm/api/accounts</a> 查看（这个地址还真不好找）</p>
<h2>Music Tag Web</h2>
<pre><code class="language-yaml">version: '3'
services:
  music-tag:
    image: xhongc/music_tag_web:latest
    privileged: true
    container_name: music-tag-web
    ports:
      - '8001:8001'
    volumes:
      - /NAS/music:/app/media:rw
      - /music_tag_web/data:/app/data
    command: /start
    restart: always
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[解决 Github Organizations 不显示]]></title>
            <link>https://imba97.com/posts/781</link>
            <guid isPermaLink="false">https://imba97.com/posts/781</guid>
            <pubDate>Thu, 30 May 2024 22:26:24 GMT</pubDate>
            <content:encoded><![CDATA[<h1>描述</h1>
<p>水一篇文章</p>
<p>最近发现在 <span icon i-mdi-github></span> Github Profile 本来显示的组织，突然多了个 <span icon i-clarity-eye-hide-line></span> 不可见的标志</p>
<p><img src="https://imba97.com/uploads/2024/05/github-organizations-1.png" alt=""></p>
<p>开了个无痕看了看，也确实被隐藏了</p>
<h1>解决</h1>
<p>找了半天，原来可以在 Organization People 里设置</p>
<p>把 Organization visibility 设置为 Public 即可</p>
<p><img src="/uploads/2024/05/github-organizations-2.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Windows 安装 ZSH 踩坑记录]]></title>
            <link>https://imba97.com/posts/780</link>
            <guid isPermaLink="false">https://imba97.com/posts/780</guid>
            <pubDate>Sat, 04 May 2024 23:09:39 GMT</pubDate>
            <content:encoded><![CDATA[<h1>WSL</h1>
<p>WSL 是适用于 Linux 的 Windows 子系统，可以在 Windows 中跑一个 Linux，常用于开发</p>
<p>我的使用场景只有一个，就是安装 <i icon i-simple-icons-zsh></i> <code>zsh</code> 作为 VS Code 的终端</p>
<p>但里面有太多的坑，目前我个人已经换回 <code>PowerShell</code> 了，以下是踩过的坑</p>
<h1>安装</h1>
<p>打开启用或关闭 Windows 功能</p>
<p>勾选适用于 Linux 的 Windows 子系统</p>
<p>重启后打开 <i icon i-fluent-store-microsoft-24-filled bg="#125697"></i> 应用商店，安装 <code>Ubuntu</code></p>
<h1>升级到 WSL 2</h1>
<p>执行 <code>wsl -l -v</code> 查看当前安装了什么版本，我这边是 <i icon i-logos-ubuntu></i> <code>Ubuntu 24.04 LTS</code></p>
<p>切换版本</p>
<pre><code class="language-bash">wsl --set-version Ubuntu-24.04 2
</code></pre>
<p><img src="https://imba97.com/uploads/2024/05/wsl-1.png" alt=""></p>
<p>设置默认安装版本</p>
<pre><code class="language-bash">wsl --set-default-version 2
</code></pre>
<p><a href="https://learn.microsoft.com/zh-cn/windows/wsl/install">官方文档</a></p>
<h1>安装 ZSH</h1>
<p>管他有没有先更新再说</p>
<pre><code class="language-bash">apt update &amp;&amp; apt upgrade
</code></pre>
<p>安装 ZSH</p>
<pre><code class="language-bash">apt install zsh
</code></pre>
<p>安装 <code>oh-my-zsh</code></p>
<pre><code class="language-bash">sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
</code></pre>
<p>下面参考托 <a href="https://github.com/antfu/dotfiles/blob/main/.zshrc">尼老师的配置</a>，安装主题、插件</p>
<p>主题是 <code>spaceship</code></p>
<pre><code class="language-bash">git clone https://github.com/denysdovhan/spaceship-prompt.git &quot;$ZSH_CUSTOM/themes/spaceship-prompt&quot; --depth=1
</code></pre>
<pre><code class="language-bash">ln -s &quot;$ZSH_CUSTOM/themes/spaceship-prompt/spaceship.zsh-theme&quot; &quot;$ZSH_CUSTOM/themes/spaceship.zsh-theme&quot;
</code></pre>
<p>常用插件安装，<code>zsh-autosuggestions</code>、<code>zsh-syntax-highlighting</code>、<code>zsh-z</code></p>
<pre><code class="language-bash">git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
</code></pre>
<pre><code class="language-bash">git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
</code></pre>
<pre><code class="language-bash">git clone https://github.com/agkozak/zsh-z $ZSH_CUSTOM/plugins/zsh-z
</code></pre>
<h1>挂载 Windows 盘符</h1>
<p>默认是 <code>/mnt/c</code> 之类的，改成直接挂在到根目录 <code>/c</code></p>
<p>后面发现其实改不改影响不是很大，姑且先记录一下</p>
<p>编辑</p>
<pre><code class="language-bash">code /etc/wsl.conf
</code></pre>
<p>添加</p>
<pre><code class="language-ini">[automount]
enabled = true
root = /
options = &quot;metadata,umask=22,fmask=11&quot;
mountFsTab = false
</code></pre>
<h1>同步配置</h1>
<p>大部分文件、文件夹的配置，例如 <span icon i-logos-git-icon></span> <code>.gitconfig</code>、<span icon i-streamline-interface-security-shield-profileshield-secure-security-profile-person></span> <code>.ssh</code>，都可以通过链接的方式同步</p>
<pre><code class="language-bash">ln -s /c/Users/imba97/.gitconfig ~/.gitconfig
ln -s /c/Users/imba97/.ssh ~/
</code></pre>
<p>出现权限问题</p>
<pre><code>Load key &quot;/root/.ssh/id_rsa&quot;: bad permissions
</code></pre>
<p>设置权限解决</p>
<pre><code class="language-bash">chmod -R 600 ~/.ssh
</code></pre>
<h1>设置 VS Code</h1>
<p>你可以分别设置 Windows 和 Linux 下的终端</p>
<pre><code>&quot;terminal.integrated.defaultProfile.windows&quot;: &quot;Ubuntu-24.04 (WSL)&quot;,
&quot;terminal.integrated.defaultProfile.linux&quot;: &quot;zsh&quot;,
</code></pre>
<p>根据自己实际安装的版本，不知道的话可以先打个引号看看</p>
<p><img src="/uploads/2024/05/wsl-2.png" alt=""></p>
<h1>VS Code 插件</h1>
<p>安装 <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl">WSL</a> 插件，可以快速切换项目等</p>
<h1>WSL 打开项目</h1>
<p>先打开原项目，然后 <code>Ctrl + Shift + P</code> 打开命令中心，选择在 WSL 中重新打开文件夹</p>
<p><img src="/uploads/2024/05/wsl-3.png" alt=""></p>
<p>也可以选在 WSL 中打开文件夹，然后输入 Windows 中目录地址</p>
<h1>第三方包找不到</h1>
<p>使用 WSL 打开项目后文件路径都会走 Linux 系统的，所以需要删掉原来的 <code>node_modules</code> 和 <code>lock</code> 文件，重新跑一下安装</p>
<p><img src="/uploads/2024/05/wsl-4.png" alt=""></p>
<h1>Git CRLF</h1>
<p>因为 Windows 和 Linux 换行符的不同，在 Linux 中 Git 会匹配到换行符不同，提示文件差异</p>
<p><img src="/uploads/2024/05/wsl-5.png" alt=""></p>
<p>只需要设置一下 Git 配置即可</p>
<pre><code class="language-bash">git config --global core.autocrlf true
</code></pre>
<p><img src="/uploads/2024/05/wsl-6.png" alt=""></p>
<h1>Path 问题</h1>
<p>默认会自动包含 Windows 的所有 <i icon i-vscode-icons-file-type-dotenv></i> 环境变量，但 <code>node</code> 比较离谱</p>
<p>可以使用 <code>node.exe</code> 但没法用 <code>node</code>，但很多程序比如 <code>npm</code> 默认执行的是 <code>node</code>，所以就会报 <code>node</code> 不存在</p>
<p>目前大概只能再在 Linux 中安装一下 <code>node</code>，也有 <a href="https://learn.microsoft.com/zh-cn/windows/dev-environment/javascript/nodejs-on-wsl#install-nvm-nodejs-and-npm">官方文档</a>，是通过 <code>nvm</code> 安装</p>
<h1>结束</h1>
<p>大概先分享这么多</p>
<p>我是后面搞到 <code>GPG</code> 的时候，发现还需要导出公钥私钥，在从 Linux 导入 <i icon i-material-symbols-key-vertical-outline-rounded bg-orange></i> 公钥私钥，需要维护两份，就感觉很麻烦</p>
<p>然后感觉只为了一个 ZSH 没必要，后面感觉还有不少坑</p>
<p>比如我能想到的就是 <i icon i-skill-icons-electron></i> <code>electron</code> 打包的时候这种依赖环境的东西</p>
<p>所以就暂时弃坑了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[狠狠地美化我的 VS Code]]></title>
            <link>https://imba97.com/posts/779</link>
            <guid isPermaLink="false">https://imba97.com/posts/779</guid>
            <pubDate>Thu, 02 May 2024 12:01:12 GMT</pubDate>
            <content:encoded><![CDATA[<h1>上图</h1>
<p><img src="https://imba97.com/uploads/2024/05/vscode-1.png" alt=""></p>
<p>首先，大多数配置来自托尼老师的方案，详情请看 <span icon i-mdi-github></span> <a href="https://github.com/antfu/use">antfu/use</a></p>
<p>不愧是托尼老师！</p>
<!--more-->
<h1>契机</h1>
<p>最开始看托尼老师的视频，发现虽然都是用的 <span icon i-vscode-icons-file-type-vscode></span> VS Code，但感觉完全是两个软件</p>
<p>配色、动画效果、错误提示、快速切换项目、闭合指示等等，每一个都让我狠狠地羡慕了</p>
<p>后来通过一些教程找到了错误提示插件 <a href="https://marketplace.visualstudio.com/items?itemName=usernamehw.errorlens">Error Lens</a>，快速切换项目的快捷键 <code>Ctrl + R</code>，但其他的一直没发现</p>
<p>再到前几天，发现了托尼老师专门开了个项目分享自己的配置。太贴心了，这必须狠狠抄袭一波！</p>
<h1>基本配置</h1>
<p>来自 <span icon i-mdi-github></span> <a href="https://github.com/antfu/use">antfu/use</a></p>
<p><span icon i-icon-park-twotone-theme text="#c36858"></span> 主题：<a href="https://marketplace.visualstudio.com/items?itemName=antfu.theme-vitesse">Vitesse Theme</a> - Vitesse Dark Soft</p>
<p><span icon i-ic-baseline-insert-emoticon text="#c19999"></span> 图标：<a href="https://marketplace.visualstudio.com/items?itemName=antfu.icons-carbon">Carbon Product Icons</a></p>
<p><span icon i-material-symbols-font-download-rounded text="#00a000"></span> 字体：<a href="https://input.djr.com/download/">Input Mono</a></p>
<p><span icon i-material-symbols-light-settings-rounded text-yellow></span> 配置：<a href="https://github.com/antfu/vscode-settings/blob/main/.vscode/settings.json">antfu VS Code settings</a> - 根据自己需要微调</p>
<h1>code-server</h1>
<p><code>code-server</code> 中使用 <code>Vitesse Theme</code>，目前插件市场中只有老版本，只能使用 <code>Vitesse Light</code> 和 <code>Vitesse Dark</code></p>
<p><img src="/uploads/2024/05/vscode-2.png" alt=""></p>
<p>但可以通过下载 <code>vsix</code> 文件手动拖到里面来安装最新版本</p>
<h1>终端</h1>
<p><span icon i-streamline-warning-triangle-solid text-yellow></span> 目前使用 <code>WSL ZSH</code> 有个关键步骤没解决，就是 Git 提交时 <code>GPG</code> 加密，慎重将终端切换为 <code>WSL ZSH</code></p>
<p>因为我是 Windows 系统，没法直接用 <code>ZSH</code>，只能通过 <code>WSL</code> 安装</p>
<p>其实设置完以上主题、配置后，终端就已经很好看了，其实完全可以用默认的，我之前是 <code>PowerShell</code></p>
<p>之后会发一下 <code>WSL ZSH</code> 折腾的记录</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[我奇怪的精神状态]]></title>
            <link>https://imba97.com/posts/778</link>
            <guid isPermaLink="false">https://imba97.com/posts/778</guid>
            <pubDate>Mon, 01 Apr 2024 21:16:31 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>突发奇想写一篇描述我最近精神状态的文章，正如文章一样最近我也有很多突发奇想</p>
<p>仿佛不止最近，到目前的人生都是一个个突发奇想</p>
<p>自从偶然看到 <code>间歇性踌躇满志，持续性混吃等死</code>，就感觉我被严丝合缝的形容出来了</p>
<p>执行能力满级但三分钟热度</p>
<h1>最近</h1>
<p>从一个颓废的状态突然变成充满动力</p>
<p>近期从一月初开始健身，说是健身其实只是跟着视频，每天也就十几分钟，二月逐渐减少，三月基本放弃</p>
<p>买了个自行车想骑车上下班，目前骑车次数大概只有三四次</p>
<p>开始练吉他，勉强会了《平凡之路》，手指也出了茧子，应该还会坚持一阵</p>
<p>写了两个脱口秀稿子，拍了拍视频，本来还想去小剧场说开放麦，目前还没实现</p>
<p>学会了<a href="https://www.bilibili.com/video/BV1n1421f7ra">《挡凉》</a>、<a href="https://www.bilibili.com/video/BV18D421j7u7">《同仁堂》</a>，还发了视频，最近买了快板打算打板唱《同仁堂》，不知道能不能学会</p>
<p>走走停停，走走停停停停停停</p>
<p>从一个充满动力的状态突然变得颓废</p>
<h1>还好</h1>
<p>细数我学过的东西，好像大部分都是只有开头没有结尾</p>
<p>倒也不是一无是处</p>
<p>专升本考的日语，一门的分数就过了合格线</p>
<p>入门级的急救帮一位电动车摔车的大爷做了个消毒、包扎</p>
<p>所以</p>
<p><strong>管他的，先学再说吧 ¯\_(ツ)_/¯</strong></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[PVE 硬件直通导致不能启动的问题]]></title>
            <link>https://imba97.com/posts/777</link>
            <guid isPermaLink="false">https://imba97.com/posts/777</guid>
            <pubDate>Thu, 08 Feb 2024 13:30:11 GMT</pubDate>
            <content:encoded><![CDATA[<h1>回放</h1>
<p>之前给公司电脑装了个 <code>PVE</code>，想把 USB 直通给虚拟机，摸索这设置了之后需要重启，再启动发现管理界面打不开、<code>SSH</code> 也连不上了</p>
<p>目测是配置导致的某些服务没法启动，网上资料很分散，零零碎碎试了一些，最后试出来了，赶紧存一下</p>
<h1>解决</h1>
<p>首先启动时，在这个页面选第二个</p>
<p><img src="https://imba97.com/uploads/2024/02/pve-1.jpg" alt=""></p>
<p>下一个页面，也选第二个</p>
<p><img src="/uploads/2024/02/pve-2.jpg" alt=""></p>
<p>然后会提示输入 <code>root</code> 密码</p>
<p><img src="/uploads/2024/02/pve-3.jpg" alt=""></p>
<p>登录进来后，先检查 <code>pve-cluster</code> 服务有没有启动，没启动的话启动一下</p>
<pre><code class="language-bash">service pve-cluster start
</code></pre>
<p><img src="/uploads/2024/02/pve-4.jpg" alt=""></p>
<p>启动后就可以找到虚拟机配置文件，需要记得虚拟机 ID，比如我这里是 100</p>
<p>编辑配置文件</p>
<pre><code class="language-bash">vim /etc/pve/qemu-server/100.conf
</code></pre>
<p><img src="/uploads/2024/02/pve-5.jpg" alt=""></p>
<p>可以看到我这里，第 5 行是我添加的硬件直通，删掉这行保存后重启</p>
<p><img src="/uploads/2024/02/pve-6.jpg" alt=""></p>
<p>重启后又看到熟悉的界面，正常启动了</p>
<p><img src="/uploads/2024/02/pve-7.jpg" alt=""></p>
<h1>结尾</h1>
<p>这个方法按理说可以解决大部分因为配置文件导致的不能启动的问题</p>
<p>只要命令行还能启动，就没啥大问题</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[自建服务 Google Gemini]]></title>
            <link>https://imba97.com/posts/776</link>
            <guid isPermaLink="false">https://imba97.com/posts/776</guid>
            <pubDate>Sun, 07 Jan 2024 22:33:02 GMT</pubDate>
            <content:encoded><![CDATA[<p>以下介绍由 <code>Google Gemini</code> 生成</p>
<h1>Google Gemini</h1>
<blockquote>
<p>简单介绍一下 Google Gemini</p>
</blockquote>
<p>Google Gemini 是谷歌公司推出的一款对话式人工智能模型，可以进行更为复杂的对话。Gemini 具有以下特点：</p>
<ul>
<li><strong>知识广博</strong>：Gemini 拥有海量的知识，可以回答各种各样的问题。</li>
<li><strong>对话式交互</strong>：Gemini 可以进行对话式的交互，就像你在和人聊天一样。</li>
<li><strong>个性化体验</strong>：Gemini 可以根据你的喜好和兴趣来调整自己的回复，为你提供个性化的体验。</li>
<li><strong>创造性思维</strong>：Gemini 可以进行创造性的思维，可以生成故事、诗歌、笑话等内容。</li>
<li><strong>强大的推理能力</strong>：Gemini 具有强大的推理能力，可以对复杂的问题进行分析和推理，并给出合理的答案。</li>
</ul>
<h1>能做什么</h1>
<blockquote>
<p>你能做什么</p>
</blockquote>
<p>作为人工智能语言模型，我可以做很多事情，包括：</p>
<ul>
<li><strong>回答各种问题</strong>：我可以回答关于世界各地的历史、科学、文化、地理和其他主题的问题。</li>
<li><strong>生成文本</strong>：我可以生成各种形式的文本，包括故事、诗歌、文章、演讲和代码。</li>
<li><strong>翻译语言</strong>：我可以将文本从一种语言翻译成另一种语言。</li>
<li><strong>总结文本</strong>：我可以提取文本中的关键信息并提供摘要。</li>
<li><strong>回答创意写作提示</strong>：我可以为你提供创意写作提示，并根据你的想法生成文本。</li>
<li><strong>回答数学问题</strong>：我可以回答各种数学问题，包括代数、几何和微积分等。</li>
<li><strong>生成图像</strong>：我可以生成不同的图像，包括照片、插图和艺术品等。</li>
<li><strong>音乐创作</strong>：我可以创作音乐，包括旋律、和声和节奏等。</li>
<li><strong>生成食谱</strong>：我可以生成不同菜肴的食谱，包括食材、烹饪方法和烹饪时间等。</li>
<li><strong>生成代码</strong>：我可以生成不同编程语言的代码，包括 Python、Java、C++ 等。</li>
</ul>
<hr>
<p>这里开始就是我开始表演了</p>
<p>其实跟 <code>Chat GPT</code> 一样，就是个大语言模型。API 提供免费版，1分钟限制 60 次请求，对于个人用户再带几个朋友一起用也完全够用</p>
<h1>API KEY 获取</h1>
<p>首先需要科学上网到 <a href="https://ai.google.dev/available_regions?hl=zh-cn">支持的地区</a>，不然打不开申请页面。选择好地区后打开 <a href="https://ai.google.dev/">申请页面</a>，点击创建即可</p>
<p><img src="https://imba97.com/uploads/2024/01/gemini-1.png" alt=""></p>
<h1>部署</h1>
<p>部署网页程序：<a href="https://github.com/babaohuang/GeminiProChat">babaohuang/GeminiProChat</a></p>
<p>支持很多平台的一键部署，但我是部署在软路由上。所以这里我用的 <code>Docker</code>，首先写个 <code>docker-compose.yml</code></p>
<pre><code class="language-yml">version: '3'
services:
  geminiprochat:
    image: babaohuang/geminiprochat:latest
    ports:
      # 宿主机端口:容器端口
      - '3000:3000'
    restart: always
    environment:
      GEMINI_API_KEY: API_KEY
</code></pre>
<p>填好你的 <code>GEMINI_API_KEY</code>，如果需要其他参数参考上面的文档</p>
<p>跑起来后你就能看到</p>
<img src="/uploads/2024/01/gemini-2.png" style="zoom:50%;" />
<h1>试一下</h1>
<p>刚好公司要写年终述职报告，基本上再简单改改就能用</p>
<img src="/uploads/2024/01/gemini-3.png" style="zoom:50%;" />
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[在博客中使用 UnoCSS！]]></title>
            <link>https://imba97.com/posts/775</link>
            <guid isPermaLink="false">https://imba97.com/posts/775</guid>
            <pubDate>Mon, 01 Jan 2024 01:04:39 GMT</pubDate>
            <content:encoded><![CDATA[<h1>演示</h1>
<pre><code class="language-html">&lt;div text=&quot;center 10 primary&quot;&gt;你可以设置字体样式&lt;/div&gt;
</code></pre>
<div py-5>
  <div text="center 10 primary">你可以设置字体样式</div>
</div>
<hr>
<pre><code class="language-html">&lt;div text-10 animate-bounce&gt;轻松设置动画&lt;/div&gt;
</code></pre>
<div py-5>
  <div text-10 animate-bounce>轻松设置动画</div>
</div>
<hr>
<pre><code class="language-html">&lt;div flex items-center justify-center gap-3&gt;
  &lt;div&gt;设置一个图标&lt;/div&gt;
  &lt;div i-mdi-emoticon-cool-outline h-10 w-10&gt;&lt;/div&gt;

  &lt;div flex=&quot;~ col&quot; items-center&gt;
    &lt;div i-mdi-emoticon-angry-outline animate=&quot;wobble count-infinite&quot; h-15 w-15 bg-red&gt;&lt;/div&gt;
    &lt;div&gt;加点颜色？再设置个动画？&lt;/div&gt;
  &lt;/div&gt;

  &lt;div i-line-md-moon-twotone-loop h-10 w-10 bg-primary&gt;&lt;/div&gt;
  &lt;div&gt;使用动画图标&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<div my-5 flex items-center justify-center gap-3>
  <div>设置一个图标</div>
  <div i-mdi-emoticon-cool-outline h-10 w-10></div>
  <div flex="~ col" items-center>
    <div i-mdi-emoticon-angry-outline animate-wobble animate-count-infinite h-15 w-15 bg-red></div>
    <div>加点颜色？再设置个动画？</div>
  </div>
  <div i-line-md-moon-twotone-loop h-10 w-10 bg-primary></div>
  <div>使用动画图标</div>
</div>
<hr>
<pre><code class="language-html">&lt;div py-5 rounded-xl flex items-center justify-center bg-gradient-to-b from-blue to-primary&gt;
  &lt;div text=&quot;10 white&quot; tracking&gt;来个渐变色&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<div my-5>
  <div py-5 rounded-xl flex items-center justify-center bg-gradient-to-b from-blue to-primary>
    <div text="10 white" tracking>来个渐变色</div>
  </div>
</div>
<hr>
<div my-5>
  <div text="5 center">实现一个图片九宫格</div>
</div>
<pre><code class="language-html">&lt;div mx-a mt-5 p-1 w-90 bg-white b=&quot;3 solid primary&quot;&gt;
  &lt;div grid=&quot;~ cols-3&quot; gap-1 class=&quot;[&amp;&gt;img]:(m-0! w-30 h-30 object-cover)&quot;&gt;
    &lt;img src=&quot;/uploads/2023/04/code-server-1.png&quot;&gt;
    &lt;img src=&quot;/uploads/2023/12/report-2023-3.png&quot;&gt;
    &lt;img src=&quot;/uploads/2023/12/report-2023-4.png&quot;&gt;
    &lt;img src=&quot;/uploads/2023/07/serialport-1-1.jpg&quot;&gt;
    &lt;img src=&quot;/uploads/2023/07/serialport-1-2.jpg&quot;&gt;
    &lt;img src=&quot;/uploads/2023/07/serialport-1-3.jpg&quot;&gt;
    &lt;img src=&quot;/uploads/2023/03/bitwarden-login-error-1.png&quot;&gt;
    &lt;img src=&quot;/uploads/2022/10/simple-zip-upload-1.png&quot;&gt;
    &lt;img src=&quot;/uploads/2018/08/switch_4.gif&quot;&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<div my-5>
  <div mx-a p-1 w-90 bg-white b="3 solid primary">
    <div grid="~ cols-3" gap-1 class="[&>img]:(m-0! w-30 h-30 object-cover)">
      <img src="https://imba97.com/uploads/2023/04/code-server-1.png">
      <img src="/uploads/2023/12/report-2023-3.png">
      <img src="/uploads/2023/12/report-2023-4.png">
      <img src="/uploads/2023/07/serialport-1-1.jpg">
      <img src="/uploads/2023/07/serialport-1-2.jpg">
      <img src="/uploads/2023/07/serialport-1-3.jpg">
      <img src="/uploads/2023/03/bitwarden-login-error-1.png">
      <img src="/uploads/2022/10/simple-zip-upload-1.png">
      <img src="/uploads/2018/08/switch_4.gif">
    </div>
  </div>
</div>
<hr>
<div text="5">做个好看的 Todo List</div>
<pre><code class="language-html">&lt;div text=&quot;10 primary&quot; font-bold&gt;我的计划&lt;/div&gt;

&lt;div my-5&gt;
  &lt;div flex items-center gap-1&gt;
    &lt;div i-ic-round-check-box h-8 w-8 bg-green&gt;&lt;/div&gt;
    &lt;div text=&quot;5 gray-3&quot; decoration=&quot;line-through&quot;&gt;hexo-unocss&lt;/div&gt;
  &lt;/div&gt;
  &lt;div flex items-center gap-1&gt;
    &lt;div i-ic-round-check-box-outline-blank h-8 w-8&gt;&lt;/div&gt;
    &lt;div text=&quot;5&quot;&gt;浏览器标签墙&lt;/div&gt;
  &lt;/div&gt;
  &lt;div flex items-center gap-1&gt;
    &lt;div i-ic-round-check-box-outline-blank h-8 w-8&gt;&lt;/div&gt;
    &lt;div text=&quot;5&quot;&gt;数字游民&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<div text="10 primary" font-bold>我的计划</div>
<div my-5>
  <div flex items-center gap-1>
    <div i-ic-round-check-box h-8 w-8 bg-green></div>
    <div text="5 gray-3" decoration="line-through">hexo-unocss</div>
  </div>
  <div flex items-center gap-1>
    <div i-ic-round-check-box-outline-blank h-8 w-8></div>
    <div text="5">浏览器标签墙</div>
  </div>
  <div flex items-center gap-1>
    <div i-ic-round-check-box-outline-blank h-8 w-8></div>
    <div text="5">数字游民</div>
  </div>
</div>
<h1>使用</h1>
<p>安装插件和相关依赖（必选）</p>
<pre><code class="language-bash">npm install hexo-unocss unocss
</code></pre>
<h2>使用图标</h2>
<p>安装 icon（可选）</p>
<pre><code class="language-bash">npm install @iconify/json
</code></pre>
<p>图标站：<a href="https://icones.js.org/">Icônes</a></p>
<h1>配置</h1>
<h2>插件配置</h2>
<pre><code class="language-yml">unocss:
  # 是否启用
  enable: true

  # 生成的样式文件
  # 以 source 为根目录
  file: css/uno.css

  # 扫描文件
  patterns:
    - 'source/**/*.md'
</code></pre>
<h2>UnoCSS 配置</h2>
<p>配置完全沿用 UnoCSS 的配置，详情请参考 <a href="https://unocss.dev/config/">https://unocss.dev/config/</a></p>
<p>在根目录下新建 <code>uno.config.ts</code>，推荐以下配置。包含基础写法、属性写法、icon 的预设配置</p>
<pre><code class="language-typescript">import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  presets: [presetUno(), presetAttributify(), presetIcons()],
  content: {
    pipeline: {
      include: [/\.(md)($|\?)/]
    }
  },
  theme: {
    colors: {
      primary: '#50528a'
    }
  }
})
</code></pre>
<p>自定义颜色 <code>primary</code> 后，即可使用 <code>text-primary</code>、<code>bg-primary</code> 等</p>
<p>推荐 VSCode 插件</p>
<ul>
<li><a href="https://marketplace.visualstudio.com/items?itemName=antfu.unocss">antfu.unocss</a> 可以显示具体生成的样式和颜色</li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=antfu.iconify">antfu.iconify</a> 可以直接显示图标</li>
</ul>
<p>不愧是托尼老师！</p>
<p><img src="/uploads/2024/01/hexo-unocss-1.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[2023 年度总结]]></title>
            <link>https://imba97.com/posts/774</link>
            <guid isPermaLink="false">https://imba97.com/posts/774</guid>
            <pubDate>Sun, 31 Dec 2023 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>今年是感觉到了累的一年，更多的是精神压力。依然</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<h1>工作</h1>
<p>工作上更得心应手了，感觉现代程序猿到了后期就是手握一堆开源库，要什么功能找哪个库</p>
<p>换了工作，当前技术栈以 <code>capacitor</code> 为主，一个跨平台框架，用前端技术开发 <code>Android</code>、<code>iOS</code> 应用，也可以通过 <code>electron</code> 打包桌面应用</p>
<p>除了前端也搞一搞 <code>nodejs</code> 之类的，简单写写 <code>iOS</code> 相关的，于是变成了这样</p>
<img src="https://imba97.com/uploads/2023/12/report-2023-1.png" style="zoom:50%;" />
<p>加班略多，也学到不少新鲜东西</p>
<h1>生活</h1>
<p>出现了严重拖延，感觉有种死循环。明知道不能睡太晚，但还是不太想睡，放下手机睡觉吧睡不着，至少得半小时一个小时才能睡着，于是就一两点了</p>
<p>但即使是这样感觉自己的时间还是很少，很多想做的、得做的一直排着队</p>
<p>以前工作之余还做做自己的项目，现在回家直接摆烂，边看 “一口气看完 XXX” 边打游戏</p>
<h1>程序</h1>
<p>开源项目比较少，大部分都是围绕工作的项目</p>
<ol>
<li>
<p><strong>games</strong></p>
<p>做了两个前端小游戏，最开始是看朋友在抖音发了一个图片记忆游戏，但这个游戏每次的图都是一样的，于是打算自己做一个，这个项目就这么开始了</p>
<p>之后又做了一个玩梗的：“陶吉吉模拟器”。后面如果有什么有意思的想法还会继续更新</p>
<img src="/uploads/2023/12/report-2023-2.png" style="zoom:50%;" />
<p>项目不大，里有图片音频预加载、国际化，整个一踩坑练手项目，有兴趣可以看一下</p>
<p>游戏地址：<a href="https://games.imba97.com">https://games.imba97.com</a></p>
<p>Github: <a href="https://github.com/imba97/games">imba97/games</a></p>
<hr>
</li>
<li>
<p><strong>code-server</strong></p>
<p>一个在线的包含服务端的 <code>vscode</code>， 让你能随时随地在任何设备（浏览器版本不能太低）敲代码，我一般用作一些简单的代码验证</p>
<p>并参考大佬的方案做了自动化发布 <code>docker</code> 镜像</p>
<p><img src="/uploads/2023/04/code-server-1.png" alt=""></p>
<p>博客文章: <a href="/posts/762">《搭建自己的在线 vscode》</a></p>
<p>Github: <a href="https://github.com/imba97/code-server">code-server</a></p>
<hr>
</li>
<li>
<p><strong>各种公司项目</strong></p>
<ul>
<li>
<p><strong>自动登录公司网络</strong></p>
<p>入职第二天发现公司网络需要每天登录才能用，当天加班写了自动连接程序</p>
<p>后面根据需要又加了自动获取 <code>IP</code>，自动设置 <code>iKuai</code> 路由器等</p>
<p>GitLab: <a href="https://repo.rydeen.com.cn/imba97/auto-login-network">auto-login-network</a></p>
<hr>
</li>
<li>
<p><strong>app store 应用版本管理器</strong></p>
<p>工作第一个项目，三个 <code>iOS</code> 应用。领导要求每个版本要按一定的格式写备注，这是件非常麻烦的事情</p>
<p>登录开发者平台（1~2个步骤）、点进具体版本详情查版本号（4个步骤）、整理备注内容（2个步骤）</p>
<p>太麻烦！于是写了个脚本</p>
<p><img src="/uploads/2023/12/report-2023-3.png" alt=""></p>
<hr>
</li>
<li>
<p><strong>打印模板上传器</strong></p>
<p>正常上传模板需要登录后台、选择相应的配置、选择模板等一系列操作，非常繁琐</p>
<p>于是就有了这个一键上传脚本，还配置了自动化，只需要提交一下 <code>git</code>，剩下的交给 <code>Gitlab Runner</code> 自动化处理</p>
<p><img src="/uploads/2023/12/report-2023-4.png" alt=""></p>
<hr>
</li>
<li>
<p><strong>git-remote-url-replacer</strong></p>
<p><code>git</code> 仓库地址批量替换器</p>
<p>公司项目批量添加了分组，写了个批量替换器，可以很方便查询一个文件夹下所有项目仓库，并通过字符串或正则匹配进行替换</p>
<p><img src="/uploads/2023/12/report-2023-5.png" alt=""></p>
<hr>
</li>
<li>
<p><strong>desktop-mover</strong></p>
<p>桌面迁移器，应对公司神奇的安全审核，要求电脑桌面除了应用软件快捷键，其他文档类不能有，于是写了个脚本一键搬运</p>
<p>其实也可以直接隐藏桌面图标。唯一的用处可能就是让我知道了获取用户目录的方法和 <code>node</code> 命令行等待输入的方法</p>
<p><img src="/uploads/2023/12/report-2023-6.png" alt=""></p>
</li>
</ul>
</li>
</ol>
<h1>小玩意儿</h1>
<p>经过了两次迭代，已经是个小有所成的 <code>HomeLaber</code>，现在家庭网络成了这样</p>
<p>有了两台小主机，一台是之前的 <code>All in One</code>，一台单独跑了 <code>Jellyfin</code> 做硬解</p>
<p><img src="/uploads/2023/12/report-2023-7.png" alt=""></p>
<h1>感悟</h1>
<p>近两年开始被叫做“大佬”的声音越来越多，我其实也没做啥，只是刚巧做了自己热爱的事情。时不时也在朝着我心里的“大佬”们前进，主打的就是一个随意</p>
<p>开始其实不太好意思，没啥值得夸的。后来感觉应该接受夸奖，不骄不躁就好</p>
<p>遗憾的是目前还没啥人来骂我</p>
<blockquote>
<p>要想成功需要的是朋友，要想巨大的成功，需要的是敌人 —— 郭德纲</p>
</blockquote>
<h1>2024 目标</h1>
<p>我是个不太适合设立目标的人，设了也完不成，但今年突然有了个清晰的目标：<strong>数字游民</strong></p>
<p>希望能实现吧</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Safari 浏览器踩坑记录]]></title>
            <link>https://imba97.com/posts/773</link>
            <guid isPermaLink="false">https://imba97.com/posts/773</guid>
            <pubDate>Tue, 19 Dec 2023 00:30:32 GMT</pubDate>
            <content:encoded><![CDATA[<h1>音频加载</h1>
<p>近期做了个小游戏，需要用到 <code>mp3</code> 文件，为了不让用户等待做了预加载，实现方式就是</p>
<pre><code class="language-javascript">const audio = new Audio()
audio.src = '音频地址'
audio.load()
</code></pre>
<p>这样再监听 <code>canplay</code> 事件，即可拿到是否加载完成，加载多个并做了进度条</p>
<p>但后来朋友反馈发现，<code>iPhone</code> 手机进度一直是 0%，原因有两个</p>
<ol>
<li>
<p>iOS 加载音频非 WIFI 下不能预加载，需要用户同意</p>
<p>也就是 <code>audio.load()</code> 需要在一个用户操作事件中，比如 <code>onClick</code></p>
</li>
</ol>
<p>本想着这就解决了，在同一个点击事件里先加载，做了个异步，等待加载完成后继续执行播放，结果又不对了</p>
<ol start="2">
<li>
<p>播放前不能有 <code>await</code></p>
<p>也就是调用 <code>audio.play()</code> 之前不能有 <code>await</code>，需要先执行加载，再进行播放</p>
<p>于是最后做了设备类型判断，如果是 <code>iOS</code> 设备，就需要先点击加载音频，加载完后才会出现开始按钮</p>
</li>
</ol>
<h1>滚动回弹</h1>
<p>近期工作中遇到了这个问题，<code>Safari</code> 浏览器如果滚动到最下面先停一下，然后还能继续往上拉，松开就有个很回弹的回弹</p>
<p>关掉这个效果很简单，在滚动元素上加个样式即可</p>
<pre><code class="language-css">-webkit-overflow-scrolling: auto;
</code></pre>
<h1>100vh 的问题</h1>
<p>这也是工作中遇到的，最先是 <code>Safari </code> 遇到的，但按理说是所有移动端浏览器都可能有的</p>
<p>正常写个 <code>100vh</code> 就是窗口高度，配合 <code>calc</code> 可以很方便的设置想要的高度，但移动端会有个问题</p>
<p>就是地址栏或者 <code>tab bar</code> 也会计算在 <code>100vh</code> 里，所以实际获取到的高度就是 <strong>可视区域</strong> + <strong>tab bar</strong></p>
<p><img src="https://imba97.com/uploads/2023/12/safari-1.png" alt=""></p>
<blockquote>
<p>图片来自 <a href="https://web.dev/blog/viewport-units?hl=zh-cn">https://web.dev/blog/viewport-units?hl=zh-cn</a></p>
</blockquote>
<p>所以需要用 <code>svh</code>，这个单位可以减掉 <code>tab bar</code></p>
<p>但实际有个问题就是，有些浏览器还没支持 <code>svh</code>，这就需要自己计算了，好在已经有人实现了类似的功能，使用这个库</p>
<pre><code class="language-javascript">import 'large-small-dynamic-viewport-units-polyfill'
</code></pre>
<p>其实它实现的功能很简单，就是获取可视区域高度，并声明一个叫 <code>--1svh</code> 的 CSS 变量</p>
<pre><code class="language-javascript">const svh = document.documentElement.clientHeight * 0.01
document.documentElement.style.setProperty('--1svh', (`${svh}px`))
</code></pre>
<p>使用时就使用 <code>calc</code> 自行计算 <code>calc(var(--1svh, 1vh) * 100);</code>，这样就相当于是 <code>100svh</code></p>
<p>此时只需要写一个 <code>@supports</code> 根据浏览器支持自动选择用哪个即可</p>
<pre><code class="language-css">@supports (height: 100svh) {
    height: 100svh;
}

@supports not (height: 100svh) {
    height: calc(var(--1svh, 1vh) * 100);
}
</code></pre>
<h1>最后</h1>
<p>上面说的小游戏在这里：<a href="https://games.imba97.com">https://games.imba97.com</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Fedora Docker 自动关闭的问题]]></title>
            <link>https://imba97.com/posts/772</link>
            <guid isPermaLink="false">https://imba97.com/posts/772</guid>
            <pubDate>Fri, 15 Dec 2023 23:38:57 GMT</pubDate>
            <content:encoded><![CDATA[<h1>事件回放</h1>
<p>之前在做 Homelab 的时候用到了 <code>Fedora</code> 这个系统，做显卡直通后，在系统中单独跑一个 <code>Jellyfin</code> 可以开启硬件解码</p>
<p>但后面发现 <code>Jellyfin</code> 时不时就打不开了，最开始没发现什么问题，也找到了一个简易解决方案，就是执行以下 <code>docker ps</code> 就启动了</p>
<p>之后感觉是系统休眠问题，于是就搜了一下 <code>Fedora</code> 怎么关闭自动休眠，但没搜到命令</p>
<h1>关闭自动休眠</h1>
<p>昨天仔细找了找，发现了关闭自动休眠的命令，<code>Fedora</code> 也可以用</p>
<pre><code class="language-bash">sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
</code></pre>
<p>记录一下</p>
<h1>添加自动重启</h1>
<p>之前加了个音乐服务，使用频率大幅增加</p>
<p>然后发现即使是不进入休眠，过几天也会莫名其妙挂，甚至 <code>SSH</code> 也连不上，所以设置了自动重启</p>
<p>打开计划任务 <code>crontab -e</code>，根据自己需要添加启动时间，我这里设置每天 5 点重启</p>
<pre><code>0 5 * * * reboot
</code></pre>
<h1>添加 NAS 挂载重试</h1>
<p>2024-04-20 更新</p>
<p>最近发现 <code>NAS</code> 有一次没挂上，用的是 <code>nfs</code></p>
<pre><code class="language-bash">vim /etc/fstab
</code></pre>
<p>在网页管理后台添加 <code>nfs</code> 后，默认会有一段，然后只需要在 <code>defaults</code> 后面添加重试次数即可</p>
<pre><code>192.168.1.200:/NAS /NAS nfs defaults,x-systemd.mount-retries=5 0 0
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Nodejs 对接 TSCLIB]]></title>
            <link>https://imba97.com/posts/771</link>
            <guid isPermaLink="false">https://imba97.com/posts/771</guid>
            <pubDate>Mon, 27 Nov 2023 20:33:46 GMT</pubDate>
            <content:encoded><![CDATA[<h1>开始</h1>
<p>最近工作中有这么个课题。曾几何时我基于 <code>electron</code> 做了个打印功能，按照官网文档的描述，原理其实是 <code>electron</code> 启动一个渲染进程，加载一个网页，并且调用 <code>electron</code> 给的打印函数实现打印</p>
<p>这种方式其实就是打印网页，但后面发现推到 Windows 打印队列中的数据比较大，有 <code>300</code> 到 <code>500</code> KB，相比友商足足大了 <code>100</code> 倍！</p>
<p>于是又被派来研究一下友商的解决方案，友商的解决方案也很容易猜到，其实就是推送了打印指令，而不是整个网页的渲染物</p>
<p>虽说容易猜到，但实际调研过程还是困难重重，但这篇文章就不展开说了，直奔主题</p>
<h1>TSCLIB</h1>
<p>TSCLIB 是个打印机函数库，它提供了一些基础的打印封装，并提供了推送二进制数据的方法</p>
<p>根据 <a href="https://fs.chinatsc.cn/system/files/tsc_dll_instruction_c.pdf">官方文档</a> 描述，你可以调用 <code>TSCLIB.dll</code> 中封装的函数来实现打印</p>
<p>这就引出第二个问题，<code>nodejs</code> 如何调用 <code>dll</code></p>
<h1>调用 DLL</h1>
<p>使用 <code>ffi-napi</code> 和 <code>ref-napi</code> 这两个开源库，能做到 <code>nodejs</code> 调用 <code>dll</code> 中的方法</p>
<p>首先将你用到的函数，函数名、返回值类型、参数类型一一声明</p>
<pre><code class="language-javascript">import ffi from 'ffi-napi'
import ref from 'ref-napi'

const tsclib = ffi.Library('TSCLIB.dll', {
  openport: [ref.types.int, [ref.types.CString]],
  clearbuffer: [ref.types.void, []],
  printlabel: [ref.types.void, [ref.types.CString, ref.types.CString]],
  sendBinaryData: [ref.types.void, [ref.types.CString, ref.types.long]],
  closeport: [ref.types.void, []]
})
</code></pre>
<p>然后是使用，参考文档第 8 页的调用案例</p>
<p>开启打印端口，<code>printer name</code> 就是 Windows 中打印机驱动的名称</p>
<pre><code class="language-javascript">tsclib.openport('printer name')
</code></pre>
<p>清除缓冲区数据</p>
<pre><code class="language-javascript">tsclib.clearbuffer()
</code></pre>
<p>构造打印数据，直接丢进去一个 <code>byte</code> 数组</p>
<pre><code class="language-javascript">const data = Buffer.from(byteArray)
tsclib.sendBinaryData(data, data.length)
</code></pre>
<p>设置打印版数、份数</p>
<pre><code class="language-javascript">tsclib.printlabel('1', '1')
</code></pre>
<p>关闭端口</p>
<pre><code class="language-javascript">tsclib.closeport()
</code></pre>
<h1>通关</h1>
<p>最后你就会看到，一个小到 3KB 的打印任务出现在 Windows 打印队列里</p>
<p><img src="https://imba97.com/uploads/2023/11/nodejs-tsclib-1.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Hexo 主题开发 hexo-theme-imba97 —— 第零篇]]></title>
            <link>https://imba97.com/posts/770</link>
            <guid isPermaLink="false">https://imba97.com/posts/770</guid>
            <pubDate>Sat, 14 Oct 2023 16:18:28 GMT</pubDate>
            <content:encoded><![CDATA[<h1>使命</h1>
<p>近一年中我了解到<strong>数字游民</strong>这个概念，就深受吸引，想逐步达成这一目标</p>
<p>不管以后找远程工作还是全职开源开发转商业化，都需要一个完善的开源项目</p>
<p>加上一直想自己动手做一个自己博客的主题，于是就有了这个项目</p>
<!--more-->
<h1>地基</h1>
<p>模板的开发是在 <code>Hexo</code> 默认模板的基础上，最开始没有直接写页面，而是丰富开发工具，会让开发变得更简单</p>
<h2>TypeScript</h2>
<p>第一个就是 <code>TypeScript</code>，这让项目有更好的规范性，代码写起来也不容易出问题</p>
<h2>Rollup</h2>
<p>主题开发在代码层面分为 <code>Hexo</code> 运行时执行的代码和实际博客页面中的代码</p>
<p>在开发或打包时，先使用 <code>tsc</code> 将 <code>ts</code> 编译为 <code>js</code>，再通过 <code>rollup</code> 根据不同目录转成相应格式的代码</p>
<p>比如 <code>Hexo</code> 运行时识别的 <code>cjs</code>，浏览器执行的 <code>iife</code></p>
<h2>UnoCSS</h2>
<p><code>UnoCSS</code> 是个原子化 <code>CSS</code> 引擎，可以根据关键词生成具体的样式</p>
<p>这样就不用为 <code>class</code> 起名发愁了，并且按需生成，不会有大量重复样式</p>
<h1>功能</h1>
<p>除了自带的图片预览，目前还加了 <code>Pjax</code>、<code>nprogress</code>、<code>smooth-scrollbar</code></p>
<p>开发层面有自动引入 <code>js</code> 库并支持权重，比如要在其他库之前引入 <code>jquery</code>，可以配置 <code>jquery</code> 的权重更高</p>
<p>并且可以方便的设置哪个库是用户可以在配置里控制开关的</p>
<h1>开源</h1>
<p>Github：<a href="https://github.com/imba97/hexo-theme-imba97">imba97/hexo-theme-imba97</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[参与一下 unocss 开发]]></title>
            <link>https://imba97.com/posts/769</link>
            <guid isPermaLink="false">https://imba97.com/posts/769</guid>
            <pubDate>Tue, 29 Aug 2023 20:26:09 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Unocss</h1>
<p><code>unocss</code> 是个强大的原子化 CSS 引擎，可以让你无需考虑样式的命名，按需生成样式</p>
<p>比如</p>
<pre><code class="language-html">&lt;div mt-5&gt;&lt;/div&gt;
</code></pre>
<p>就会自动生成样式</p>
<pre><code class="language-css">[mt-5=&quot;&quot;]{margin-top:1.25rem;}
</code></pre>
<h1>发现需求</h1>
<p>最近在做一个 <code>Hexo</code> 的主题，用到了 <code>unocss</code></p>
<pre><code class="language-typescript">import { defineConfig, presetAttributify, presetIcons, presetUno } from 'unocss'

export default defineConfig({
  presets: [presetUno(), presetAttributify(), presetIcons()],
  cli: {
    entry: {
      patterns: ['layout/**/*.ejs'],
      outFile: 'source/css/style.css'
    }
  }
})
</code></pre>
<p>可以看到我配置了 <code>cli</code>，执行 <code>unocss</code> 它会获取 <code>layout</code> 下的所有 <code>.ejs</code> 文件</p>
<p>生成的样式输出到 <code>source/css/style.css</code></p>
<p>但我如果想分成多个文件生成，比如根据 <code>layout</code> 下的文件夹、文件名等信息生成不同的样式文件</p>
<p>扒拉了一下生成文件的源码</p>
<pre><code class="language-typescript">const outFile = resolve(options.cwd || process.cwd(), options.outFile ?? 'uno.css')
</code></pre>
<p>可以看到，<code>outFile</code> 只是拼接了程序执行目录和你设置的 <code>outFile</code>，如果没设置就默认生成 <code>uno.css</code></p>
<p>所以我想着优化一下，让 <code>outFile</code> 可以支持例如 <code>source/css/[dirname]/[name].css</code> 的写法，或者直接是个函数</p>
<p>类似这样</p>
<pre><code class="language-javascript">{
  outFile: info =&gt; `source/${info.dirname}/${info.name}.css`
}
</code></pre>
<h1>Uno 启动！</h1>
<ul>
<li>
<p>第一步，fork 项目、下载源码 <a href="https://github.com/unocss/unocss">unocss</a>，参考其他人的 PR，fork 后还需要创建一个分支，格式是 <code>git 提交类型/简单的修改描述</code></p>
<p>例如我创建的分支叫 <code>feat/out-file</code></p>
</li>
<li>
<p>下一步，安装，<code>pnpm install</code>，安装后执行 <code>pnpm dev</code>，因为是 <code>monorepo</code>，所以会在 <code>packages</code> 下的每个子项目中安装、执行 <code>dev</code></p>
<p>执行后每个子项目中会生成 <code>dist</code>，开发模式下会使用 <code>jiti</code> 来在执行时编译 <code>ts</code> 文件并执行</p>
</li>
<li>
<p>接下来，你只需要对 <code>src/*.ts</code> 进行修改即可</p>
</li>
</ul>
<h1>最后</h1>
<p>头回知道 <code>jiti</code>，这种开发方式真的太爽了，不需要频繁执行 <code>tsc</code>，学到</p>
<p>我先去改了，争取 PR 成功合并</p>
<h1>汇报</h1>
<p>写完了发现 <code>unocss</code> 本身就支持多个输出文件功能，再加上几个单元测试没过，后面就不想搞了</p>
<p>最后在托尼老师的开源探店投了一稿：<a href="https://github.com/antfu/discussions/discussions/1#discussioncomment-7254789">开源探店直播 项目搜集</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[这个全平台远程桌面软件我必须安利！]]></title>
            <link>https://imba97.com/posts/768</link>
            <guid isPermaLink="false">https://imba97.com/posts/768</guid>
            <pubDate>Tue, 08 Aug 2023 22:58:30 GMT</pubDate>
            <content:encoded><![CDATA[<h1>我太激动了</h1>
<p>今天发现一个 <code>全平台</code>、<code>远程桌面</code>、<code>免费开源</code>、<code>支持自建服务器</code> 的软件！可以说是 <code>Buff</code> 都让它叠满了！</p>
<p>我这么激动的原因就是，没有对比就没有伤害，平时深受某几个远程桌面软件的迫害</p>
<hr>
<p>工作中经常需要连接同网络下的 <code>mac mini</code> 进行打包等操作，时不时也会连接客户的电脑排查异常</p>
<p>那某几个软件不但画面糊！有时候能把 <code>1</code> 显示成 <code>2</code> 我就觉得离谱</p>
<p>而且我是连接本地设备，它还要去那破破烂烂的服务器绕一圈我就不是很理解，撒我像素是吧？</p>
<h1>软件本体</h1>
<p>请记住，它叫：<a href="https://rustdesk.com/">rustdesk</a></p>
<img src="https://imba97.com/uploads/2023/08/rustdesk-1.png" style="zoom:50%;" />
<p>它有多良心，如果说全平台和提供免费中继服务器，是一个远程桌面软件最基本的条件，那么开源和支持自建服务器就实属是<strong>格局大开</strong>了</p>
<p>下载仅 <code>19M</code> 大小的可执行文件，打开后直接就可以远程，你甚至都不用安装。使用完整功能才需要安装</p>
<p><strong>它真的，我哭死！</strong></p>
<h1>服务端</h1>
<p>官方有开源的服务端项目：<a href="https://github.com/rustdesk/rustdesk-server">rustdesk-server</a></p>
<p>还有 <code>Docker</code> 镜像：<a href="https://hub.docker.com/r/rustdesk/rustdesk-server">rustdesk/rustdesk-server</a></p>
<p>这次浅浅尝试了一下，在软路由 <code>Docker</code> 跑起来一个服务端，使用 <code>NPS</code> 穿透到公网，穿透服务器位置是北京，但速度依然飞快，手机远程控制非常跟手</p>
<img src="/uploads/2023/08/rustdesk-2.png" style="zoom:50%;" />
<h1>其他要说的</h1>
<p>我从两个月前就开始找平替，但一直没找到满意的</p>
<p>不是画质拉胯就是功能拉胯，用来用去莫得办法只能一直忍着那某几个</p>
<p>终于今天还是让我找到这个良心软件，这软件真的是在用爱发电了</p>
<p>赞美开源</p>
<p>告辞</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[nestjs 联表查询]]></title>
            <link>https://imba97.com/posts/767</link>
            <guid isPermaLink="false">https://imba97.com/posts/767</guid>
            <pubDate>Sun, 30 Jul 2023 23:04:38 GMT</pubDate>
            <content:encoded><![CDATA[<h1>联表查询</h1>
<p>浅浅记录一下 <code>nestjs</code> 开发的联表查询</p>
<p>用户表和上传表，需求是获取用户列表时联表查询上传表的用户头像</p>
<!--more-->
<pre><code class="language-typescript">async findAll(page: number, limit: number) {
  const list = await this.userRepository
    .createQueryBuilder('user')
    .select([
      'user_id',
      'name'
    ])
    .leftJoin(Upload, 'upload', 'upload.upload_id = user.avatar')
    .addSelect(['upload.filename as avatar'])
    .offset(limit * (page - 1))
    .limit(limit)
    .getRawMany()

  return {
    list
  }
}
</code></pre>
<p>没有用 <code>@OneToOne</code> 之类的表关系，因为好像数据库要多生成一个 ID 字段 ( ´_ゝ｀)</p>
<p>这边我直接用了 <code>leftJoin</code>，指定了 <code>Update</code> 这个实体 <code>(entity)</code>，拿到 <code>upload.filename</code></p>
<p>需要注意的是 <code>getMany</code> 好像会返回空，需要用 <code>getRawMany</code></p>
<p>而且分页的话需要用 <code>offset</code> 和 <code>limit</code>，<code>take</code> 和 <code>skip</code> 没效果</p>
<h1>后言</h1>
<p>请教一下各位大佬，如果用 <code>@OneToOne</code> 的话要不额外生成 ID 要怎么写</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[GitLab Runner 初体验]]></title>
            <link>https://imba97.com/posts/766</link>
            <guid isPermaLink="false">https://imba97.com/posts/766</guid>
            <pubDate>Thu, 27 Jul 2023 22:10:31 GMT</pubDate>
            <content:encoded><![CDATA[<h1>概念</h1>
<p><code>GitLab</code> 在跑流水线时需要配置 <code>runner</code>，<code>runner</code> 就是一个计算服务，说白了就是个一台电脑</p>
<p>根据流水线的步骤执行编译、打包、部署等工作</p>
<h1>创建 Runner</h1>
<p>首先在 <code>GitLab</code> 项目设置中创建 <code>runner</code>，拿到 <code>token</code></p>
<p><code>runner</code> 服务这里我用的 <code>Docker</code> 在软路由上安装的，<code>dockerhub</code> 上下载最高的是 <code>bitnami/gitlab-runner</code></p>
<p>但我实际使用中在输入 <code>runner</code> 名称时会出现权限问题导致没法使用，所以我用了 <code>gitlab/gitlab-runner</code></p>
<hr>
<p>创建 <code>docker-compose.yml</code></p>
<pre><code class="language-yaml">version: '3.1'
services:
  runner:
    image: gitlab/gitlab-runner:latest
    container_name: gitlab-runner
    restart: always
    privileged: true
    volumes:
      - /home/runner/config:/etc/gitlab-runner
      - /var/run/docker.sock:/var/run/docker.sock
</code></pre>
<p>注册 <code>runner</code></p>
<pre><code class="language-bash">docker exec -it gitlab-runner gitlab-runner register
</code></pre>
<p>按照提示补全信息</p>
<pre><code class="language-bash"># 输入 gitlab 地址
Enter the GitLab instance URL (for example, https://gitlab.com/):

# runner token
Enter the registration token:

# runner 名称
Enter a name for the runner. This is stored only in the local config.toml file:

# 选择执行器
Enter an executor: custom, shell, ssh, instance, docker-autoscaler, docker+machine, kubernetes, docker, docker-windows, parallels, virtualbox:
</code></pre>
<h1>报错</h1>
<p><code>runner</code> 配置成功后跑的时候报了个错</p>
<pre><code class="language-bash">$ npm install
bash: line 125: npm: command not found
ERROR: Job failed: exit status 1
</code></pre>
<p>是执行 <code>npm install</code> 的时候没找到 <code>npm</code></p>
<h1>解决</h1>
<p>后面发现是选择执行器，我最开始选的 <code>shell</code></p>
<p>只要在这一步选择合适的执行器，比如根据 <code>gitlab-ci.yml</code> 的需要</p>
<p><img src="https://imba97.com/uploads/2023/07/gitlab-runner-1.png" alt=""></p>
<p>我要用到 <code>node</code> 环境，所以这里可以选 <code>docker</code></p>
<pre><code class="language-bash">Enter an executor: custom, shell, ssh, instance, docker-autoscaler, docker+machine, kubernetes, docker, docker-windows, parallels, virtualbox:
docker

# 输入 Docker 镜像
Enter the default Docker image (for example, ruby:2.7):
node:18.15.0
</code></pre>
<p>即可解决</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Nodejs 发送短信程序避坑指南]]></title>
            <link>https://imba97.com/posts/765</link>
            <guid isPermaLink="false">https://imba97.com/posts/765</guid>
            <pubDate>Fri, 21 Jul 2023 22:40:39 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前面的废话</h1>
<p>前阵子接了个有意思的活，需求根据模板批量发送短信，在强烈兴趣爱好的驱使下决定研究研究，顺便把钱赚了</p>
<p>找教程的时候发现非常少，视频资源都比较老，中英文短信混合发送没有一个教程提及，调通硬件又发现，<code>nodejs</code> 相关的资源就更少了</p>
<!--more-->
<p>折腾了两周多，最开始买的板子只支持 <code>2G</code>，想着办 <code>2G</code> 卡结果营业厅说没法办，一通找最后又换成现在这个板子，还有发短信各种失败...</p>
<p>总之最后折腾出来这篇，分享一下踩的坑</p>
<h1>硬件准备</h1>
<ul>
<li>
<p>封装好的 4G 模块 <strong>(A7680C)</strong></p>
<img src="https://imba97.com/uploads/2023/07/serialport-1-1.jpg" style="zoom:50%;" />
</li>
<li>
<p>USB TO TTL <strong>(CH340G)</strong> 一般会附赠线</p>
<img src="/uploads/2023/07/serialport-1-2.jpg" style="zoom:50%;" />
</li>
</ul>
<h1>接线参考</h1>
<img src="/uploads/2023/07/serialport-1-3.jpg" style="zoom:50%;" />
<h1>测试</h1>
<p>用串口工具，或者使用 <code>serialport</code> 包写个简单的程序连接并发送指令</p>
<p>但注意甄别，我遇到有些工具不会按你的指令，会给你发一些乱七八糟的短信</p>
<h2>发送英文短信</h2>
<ul>
<li>
<p><code>AT</code> 测试命令</p>
</li>
<li>
<p><code>AT+CMGF=1</code> 设置短信格式</p>
</li>
<li>
<p><code>AT+CMGS=13000000000</code> 接收手机</p>
<p>以上都需要发送新行，也就是回车</p>
</li>
<li>
<p><code>&gt;</code> 出现箭头后写上短信内容，去掉发送新行并发送</p>
</li>
<li>
<p>切换到 <code>16进制</code> 新行发送 <code>1A</code></p>
</li>
</ul>
<p>有的教程说的最后发送 <code>Ctrl + Z</code> 其实说的就是 <code>1A</code></p>
<p><img src="/uploads/2023/07/serialport-1-4.jpg" alt=""></p>
<p>接收成功的短信</p>
<img src="/uploads/2023/07/serialport-1-5.jpg" style="zoom: 33%;" />
<h2>发送中文短信</h2>
<p>首先要将手机号、短信内容经过一系列处理</p>
<ul>
<li><code>801986</code> + 发送人手机号 + <code>F</code>，进行奇偶反转，如 <code>80198613012345678F</code> 变为 <code>0891683110325476F8</code></li>
<li><code>86</code> + 收件人手机号 + <code>F</code>，进行奇偶反转</li>
<li>短信内容转为 16 进制</li>
<li><code>11000D91</code> + 收件人反转的手机号 + <code>0008AA</code> + 16 进制短信内容</li>
<li>计算上一步字符串长度除以二</li>
<li>发件人反转手机号 + 第四步的字符串</li>
<li>依次执行 <code>AT+CMGF=0</code>、<code>AT+CSCS=&quot;GSM</code>、<code>AT+CMGS=字符串长度</code>、上一步的字符串、<code>1A</code></li>
</ul>
<h1>避坑</h1>
<ol>
<li>
<p>中文短信转 16 进制要 4 位，不足补零，比如 <code>ABC</code> 是不行的，必须是 <code>0ABC</code>，否则会乱码</p>
</li>
<li>
<p>在 <code>nodejs</code> 里 <code>1A</code> 的发送方式是</p>
<pre><code class="language-javascript">serialPort.write(Buffer.from([0x1A]))
serialPort.write('\r')
</code></pre>
</li>
</ol>
<h1>参考</h1>
<ul>
<li><a href="https://github.com/imba97/simple-serialport-gsm/blob/master/packages/adapters/src/MainlandChinaAdapter.ts">中文短信编码流程</a></li>
<li><a href="https://github.com/imba97/simple-serialport-gsm/blob/master/packages/utils/src/unicode.ts">短信内容转 16 进制</a></li>
</ul>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Capacitor js 使用体验]]></title>
            <link>https://imba97.com/posts/764</link>
            <guid isPermaLink="false">https://imba97.com/posts/764</guid>
            <pubDate>Thu, 01 Jun 2023 20:40:23 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前端现状</h1>
<p>众所周知，当下前端现状就是“打包 Android、打包 iOS、打包 Electron”。前端已死，大家都往端上靠</p>
<p>今天谈谈最近工作中用到的，入坑两个月的前端框架 <code>Capacitor js</code></p>
<h1>Capacitor js</h1>
<p><a href="https://capacitorjs.com/">Capacitor js</a> 是一个跨平台框架，可以打包基于前端的 <code>Android</code>、<code>iOS</code>、<code>Electron</code> 应用</p>
<p>这个框架的逻辑很简单，它可以通过 <code>npx cap add android</code> 等命令生成其他平台的项目模板，通过 <code>npx cap sync</code> 命令把打包好的前端代码移动到其他平台项目下。最后再用对应平台的 <code>IDE</code> 打包，比如 <code>Android Studio</code>、<code>Xcode</code></p>
<h1>插件</h1>
<p>插件的概念就是通讯工具。因为前端无法操作设备的很多功能，比如你想打开摄像头，就需要通过前端插件发送一个指令到平台端的插件，收到指令后执行对应的逻辑</p>
<p>官方提供的插件基本覆盖了设备常用功能，同时还有社区插件，再不济可以自己开发插件，官方也提供了很好的例子</p>
<p>起初插件是放在本地的，项目中引入走的 <code>file:</code> 协议，这导致新电脑拉取项目后必须先在每个插件目录下 <code>install</code>、<code>build</code> 一遍才能在项目 <code>install</code></p>
<p>后来公司搭建了 <code>npm</code> 私服解决了这个问题</p>
<h1>npm 私服带来的问题</h1>
<p>使用私服后，执行 <code>electron</code> 插件的 <code>sync</code> 命令时出现了问题，会提示找不到依赖包</p>
<p>翻看了插件源码之后发现，<code>sync</code> 命令会执行两步，一个是 <code>copy</code> 一个是 <code>update</code>。在 <code>update</code> 执行过程中会拉取你安装的所有社区插件，并在 <code>electron</code> 文件夹下再次安装，安装使用的命令是 <code>npm i</code>，所以会导致私服上的依赖拉不到</p>
<p>解决方法是给他的命令后加一个 <code>--registry</code> 指定以下源地址就可以了</p>
<h1>总体感受</h1>
<p>目前使用中还没遇到过什么大的问题，框架的工作流程简单易懂，基本上只需要写前端代码，然后无脑打包就好了</p>
<p>插件开发也贼简单，新插件不需要在代码中注册等操作，引入即用，非常的 nice</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[网络游戏防沉迷实名认证系统 1007 错误记录]]></title>
            <link>https://imba97.com/posts/763</link>
            <guid isPermaLink="false">https://imba97.com/posts/763</guid>
            <pubDate>Wed, 12 Apr 2023 23:49:41 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>最近在接 <a href="https://wlc.nppa.gov.cn/fcm_company/index.html">网络游戏防沉迷实名认证系统</a>，它得要你跑通所有测试接口才给你用正式接口</p>
<p>所以我直接投降，用了个开源包，<a href="https://github.com/chinawilon/fcm_game">chinawilon/fcm_game</a></p>
<p>奈何是 <code>composer</code> 的，现学现卖终于跑起来了，但这才是刚开始</p>
<h1>直奔主题</h1>
<p>遇到了个报错怎么调也调不通，接口返回报错内容</p>
<pre><code class="language-json">{&quot;errcode&quot;:1007,&quot;errmsg&quot;:&quot;SYS REQ EXPIRE ERROR&quot;}
</code></pre>
<p>根据文档说明是：接口请求过期</p>
<p>最开始没仔细想，以为是参数问题，就把各种参数打印了个遍，发现没啥问题</p>
<p>后面仔细一想，<strong>过期</strong> 那不就是时间的问题吗</p>
<h1>调试</h1>
<p>使用命令打印服务器日期</p>
<pre><code class="language-bash">date &quot;+%Y-%m-%d %H:%M:%S&quot;
</code></pre>
<p>发现果然罪魁祸首就是服务器日期慢了几分钟！</p>
<h1>解决</h1>
<p>使用 <code>ntp</code>、<code>ntpdate</code> 让服务器自己去时间同步服务器同步时间</p>
<p>首先安装这两个软件</p>
<pre><code class="language-bash">yum -y install ntp ntpdate
</code></pre>
<p>设置同步服务器</p>
<pre><code class="language-bash">ntpdate time.windows.com
</code></pre>
<p>它输出的内容会告诉你慢了或者快了多少</p>
<pre><code>[root@localhost auth]# ntpdate time.windows.com
12 Apr 23:31:48 ntpdate[45077]: adjust time server 52.231.114.183 offset 195.562017 sec
</code></pre>
<p>愉快解决</p>
<p>再去解决下一个问题了，告辞</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[搭建自己的在线 vscode]]></title>
            <link>https://imba97.com/posts/762</link>
            <guid isPermaLink="false">https://imba97.com/posts/762</guid>
            <pubDate>Wed, 05 Apr 2023 12:06:15 GMT</pubDate>
            <content:encoded><![CDATA[<h1>一张图</h1>
<p><img src="https://imba97.com/uploads/2023/04/code-server-1.png" alt=""></p>
<h1>code-server</h1>
<p><a href="https://coder.com/docs/code-server/latest">code-server</a> 是一个在线版的 <code>vscode</code>，跟其他在线编辑器不同的是，它包含服务端，通过 <code>WebSockets</code> 与主机通讯，这使得你可以搭建后端环境来编译、运行你的程序</p>
<p>跟本地 <code>vscode</code> 基本一致，可以非常方便的进行项目演示、合作开发等</p>
<h1>启动</h1>
<p>这里使用的是 <code>Docker</code> 部署</p>
<p>只需要下载 <a href="https://github.com/imba97/code-server/blob/master/docker-compose.yaml">docker-compose.yaml</a>，配置相应的环境变量，然后执行 <code>docker-compose up -d</code> 即可</p>
<h1>持续集成</h1>
<p>参考 <a href="https://github.com/monlor/docker-code-server">monlor/docker-code-server</a> 做了持续集成</p>
<p>使用 <code>Github Actions</code> 根据 <code>Dockerfile</code> 进行构建、发布镜像，根据自己需要进行了以下配置</p>
<ul>
<li>
<p>给 <code>zsh</code> 装了个可可爱爱的主题</p>
<p><img src="/uploads/2023/04/code-server-2.png" alt=""></p>
</li>
<li>
<p>安装了 <code>nvm</code>、<code>nodejs</code>、<code>yarn</code>、<code>pnpm</code>、<code>vue-cli</code> 等前端环境</p>
</li>
<li>
<p>安装了一些常用插件</p>
</li>
<li>
<p>自定义 <code>vscode</code> 用户配置</p>
</li>
<li>
<p>参考的项目里还带 <code>NPS</code>，正好需要就也保留了</p>
</li>
</ul>
<p>有持续集成就方便多了，配置环境、安装软件、更改配置等只需要改一下相关的文件或者写写脚本、命令提交即可</p>
<h1>后言</h1>
<p>奈何现在不太会写 <code>shell</code> 脚本，之后慢慢会把这个镜像做大做强，这个东西可玩性真是太大了</p>
<p>Github: <a href="https://github.com/imba97/code-server">imba97/code-server</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Bitwarden 登录失败问题记录]]></title>
            <link>https://imba97.com/posts/761</link>
            <guid isPermaLink="false">https://imba97.com/posts/761</guid>
            <pubDate>Sun, 12 Mar 2023 21:50:51 GMT</pubDate>
            <content:encoded><![CDATA[<h1>错误</h1>
<p>Bitwarden 是一个密码管理工具，服务端支持自己部署，所以用 <code>Docker</code> 搭了一个，稳定运行了一年多</p>
<p>直到几个月前，突然出现了问题，所有客户端（浏览器插件、手机 app）都不能登录了</p>
<img src="https://imba97.com/uploads/2023/03/bitwarden-login-error-1.png" style="zoom:50%;" />
<p>不过服务端自带的 Web 界面还能用，再加上懒就一直没折腾，但确实也挺困扰的，总不能一直登 Web 界面获取密码</p>
<p>于是今天想着解决解决</p>
<h1>原因</h1>
<p>像这种所有客户端同时报错的情况，理所应当就会想到肯定是服务端更新了什么</p>
<p>搜了一下问题，发现 <a href="https://tieba.baidu.com/p/8195973485">贴吧</a> 有讨论，果然官方早就发布过 <a href="https://bitwarden.com/help/november-deprecation-notice/">《11月弃用通知》</a></p>
<h1>解决</h1>
<p>像我这种怕麻烦的人，换浏览器插件版本这种解决方案我是直接放弃的，因为多台设备换一遍也是挺麻烦的</p>
<p>也是同篇帖子提到把镜像换成 <code>vaultwarden/server</code>，于是我选择按这个方案进行</p>
<p>首先肯定要把原来的密码<strong>全部导出</strong>，然后再</p>
<p>因为我是用了宝塔，所以直接打开 <code>Docker</code> 管理器，拉取镜像，创建容器</p>
<img src="/uploads/2023/03/bitwarden-login-error-2.png" style="zoom:50%;" />
<p>按以上配置，其中 <code>WEBSOCKET_ENABLED=true</code> 是 <a href="https://github.com/dani-garcia/vaultwarden/blob/a13a5bd1d8c3fea3fce80eba6e8c3aa8880855dd/.env.template#L68">WebSocket 通知端口</a>，不需要的可以不加，并且也不需要映射 <code>3012</code> 端口</p>
<p>目录映射一般写网站根目录，毕竟反向代理后闲着也是闲着</p>
<p><code>80</code> 端口如果跟之前一样，反向代理就不用改，直接保存即可</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Vue 自定义指令生成 placeholder]]></title>
            <link>https://imba97.com/posts/760</link>
            <guid isPermaLink="false">https://imba97.com/posts/760</guid>
            <pubDate>Thu, 16 Feb 2023 22:57:41 GMT</pubDate>
            <content:encoded><![CDATA[<h1>需求产生</h1>
<p>在使用表单时，表单的输入框、选择器会设置 <code>placeholder</code> 属性来进行提示</p>
<p>不设置感觉空荡荡的，但如果自己一条条的写又感觉非常麻烦</p>
<p><img src="https://imba97.com/uploads/2023/02/auto-placeholder-1.jpg" alt=""></p>
<p>那我们为何不封装个指令，让它自己加呢</p>
<h1>思路</h1>
<p>因为我用的是 <code>element-plus</code>，组件都有固定的样式、模板</p>
<p>比如</p>
<pre><code class="language-vue">&lt;el-form-item label=&quot;姓名&quot;&gt;
  &lt;el-input&gt;&lt;/el-input&gt;
&lt;/el-form-item&gt;
</code></pre>
<p>最后的代码就是</p>
<pre><code class="language-vue">&lt;div class=&quot;el-form-item&quot;&gt;
  &lt;label for=&quot;title&quot; class=&quot;el-form-item__label&quot;&gt;姓名&lt;/label&gt;
  &lt;div class=&quot;el-form-item__content&quot;&gt;
      &lt;div class=&quot;el-input&quot;&gt;
      &lt;input type=&quot;text&quot; class=&quot;el-input__inner&quot;&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>那我们只需要获取到具体的输入框，然后给它设置属性就可以</p>
<p><code>Vue</code> 指令可以在组件挂载后拿到添加指令的 <code>DOM</code> 元素，再用 <code>DOM</code> 的 <code>querySelector</code> 获取到所有 <code>el-form-item</code> 子元素，遍历后再根据 <code>label</code> 的内容给 <code>input</code> 设置 <code>placeholder</code></p>
<h1>代码参考</h1>
<pre><code class="language-js">import _ from 'lodash'

export default { 
  beforeMount(el) {
    if (!(_.isFunction(_.get(el.classList, 'contains')) &amp;&amp; el.classList.contains('el-form'))) {
      console.warn('v-auto-placeholder 指令需要放在 el-form 上使用')
      return
    }

    // 取出 form 下所有 el-form-item
    const formItems = el.querySelectorAll('.el-form-item')

    _.forEach(formItems, item =&gt; {
      // label element
      const labelEl = _.get(item.children, '0', '')

      // label 文本，如果 label 有子级，则取出子级第一个的 文本
      const label = _.get(labelEl.children, 'length', 0) &gt; 0 ? _.get(labelEl.children, '0.children.0.innerText', '') : _.get(labelEl, 'innerText', '')

      // content element
      const contentEl = _.get(item.children, '1')

      // input 框
      const input = contentEl &amp;&amp; contentEl.querySelector ? contentEl.querySelector('input,textarea') : null

      // 原 placeholder
      const placeholder = _.get(input, 'placeholder')

      // 如果有值则不替换
      if (placeholder &amp;&amp; placeholder !== '' &amp;&amp; placeholder !== '请选择') {
        return
      }

      // 取出内容第一个元素的 classList
      const classList = _.get(contentEl, 'children.0.classList')

      // 根据不同组件显示不同文本
      if (_.some(classList, (className) =&gt; ~[
        'el-select',
        'el-cascader'
      ].indexOf(className))) {
        if (input) input.setAttribute('placeholder', `请选择${label}`)
      } else {
        if (input) input.setAttribute('placeholder', `请输入${label}`)
      }
    })
  }
}
</code></pre>
<h1>最终效果</h1>
<p>在 <code>el-form</code> 加上 <code>v-auto-placeholder</code></p>
<pre><code class="language-vue">&lt;el-form v-auto-placeholder&gt;
  &lt;el-form-item label=&quot;姓名&quot;&gt;
    &lt;el-input&gt;&lt;/el-input&gt;
  &lt;/el-form-item&gt;

  &lt;!-- ... --&gt;
&lt;/el-form&gt;
</code></pre>
<p>就可以自动生成 <code>placeholder</code></p>
<p><img src="/uploads/2023/02/auto-placeholder-2.jpg" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[2022 年度总结]]></title>
            <link>https://imba97.com/posts/759</link>
            <guid isPermaLink="false">https://imba97.com/posts/759</guid>
            <pubDate>Sat, 31 Dec 2022 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>今年的状态依然是</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<!--more-->
<h1>去年的 Flag</h1>
<ol>
<li>日语 JLPT N1，报了也成功抢到位置了，但最后几天被通知考点取消了</li>
<li>系统学习数据结构和设计模式，完全没看 ¯\_(ツ)_/¯</li>
</ol>
<p>我就不适合立 flag</p>
<h1>工作</h1>
<p>今年一整年在做前端，从小白到入土 <code>Vue2</code>、<code>Vue3</code> 全家桶，<code>WebPack</code>、<code>Vite</code> 以及 <code>PC</code> 端、移动端的 <code>UI</code> 框架、常见的几个后台管理框架也都摸了个遍</p>
<p>下半年新项目是完全从零开始做了一套管理后台，封装了一些不错的组件，至少自己用起来很爽</p>
<h1>生活</h1>
<p>依旧 “间歇性踌躇满志，持续性混吃等死”</p>
<p><a href="/posts/756">考了红十字救护员</a></p>
<h1>程序</h1>
<ol>
<li>
<p><strong>蓝天救援队项目</strong></p>
<p>给队里做的公益性质的项目，目前可以管理任务、签到签退，简化繁琐操作</p>
<p>整个项目分三块，服务端、用户端、管理端，边摸索边做的</p>
<p><img src="https://imba97.com/uploads/2022/12/report-2022-2.png" alt=""></p>
<p><img src="/uploads/2022/12/report-2022-3.png" alt=""></p>
<p>Github：<a href="https://github.com/bsr-project">bsr-project</a></p>
</li>
</ol>
<hr>
<ol start="2">
<li>
<p><strong>简单的打包上传工具</strong></p>
<p>工作中有时需要前端打包再文件发给别人上线，一向嫌麻烦的我做了个程序</p>
<p>可以打包后一键压缩、管理版本、推送钉钉群</p>
<p><img src="/uploads/2022/10/simple-zip-upload-1.png" alt=""></p>
<p>Github：<a href="https://github.com/imba97/simple-zip-upload">simple-zip-upload</a></p>
</li>
</ol>
<hr>
<ol start="3">
<li>
<p><strong>自动签到与反自动签到</strong></p>
<p><a href="https://xyears.cn/thread-163.htm">《十年之约论坛自动签到》</a>、<a href="https://xyears.cn/thread-258.htm">《十年之约论坛反自动签到》</a>，话都让我一个人说了</p>
<p>Github：<a href="https://github.com/imba97/imba97_project/tree/master/xyears-auto-sign-in">xyears-auto-sign-in</a></p>
</li>
</ol>
<hr>
<p>今年没怎么做什么，基本都在维护之前做的东西</p>
<h1>小玩意儿</h1>
<p>在今年最后几天决定更新一下网络环境设备，准备把零散的设备 All in One</p>
<p><img src="/uploads/2022/12/report-2022-4.jpg" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Github Actions Hexo 自动部署]]></title>
            <link>https://imba97.com/posts/758</link>
            <guid isPermaLink="false">https://imba97.com/posts/758</guid>
            <pubDate>Tue, 22 Nov 2022 21:39:34 GMT</pubDate>
            <content:encoded><![CDATA[<h1>来龙去脉</h1>
<p>最近重装了电脑，近几年内每次重装也好，装备新电脑也好，越来越觉得这个博客是令我比较头疼的事</p>
<p>因为前几年从 <code>WordPress</code> 迁移到了 <code>Hexo</code>，虽然变成了纯静态，访问速度上提高了。但本地文件的维护、文章的发布就变得很繁琐</p>
<p>比如</p>
<ul>
<li>重装必须备份</li>
<li>多台电脑必须同步最新的</li>
<li>发布文章需要在本地跑三条命令</li>
</ul>
<!--more-->
<p>多台电脑同步的问题，我在家里树莓派上建了个 <code>git</code> 仓库，专门同步文章。不过大部分时间都会忘记，并且配置文件、主题的配置文件都不在文章目录，所以没法同步</p>
<h1>觉醒</h1>
<p>最近一次重装我依然是手动备份，重装后看着这个备份，觉得这样下去不行，得赶紧拿出个更完美（懒）的方案</p>
<p>想起之前在一个浏览器插件的仓库看到了自动发布脚本，我有个浏览器插件每次发布都很繁琐，因此早就想研究一下自动化</p>
<p>所以这次第一个念头也是自动化：<code>Github Actions</code></p>
<h1>旧流程</h1>
<p>按照之前手动挡的流程是这样的</p>
<ol>
<li>清除缓存 <code>hexo clean</code></li>
<li>生成静态文件 <code>hexo g</code></li>
<li>部署 <code>hexo d</code>（执行一次 <code>git</code> 推送）</li>
<li>远程仓库 <code>hooks</code> 被触发，自动去网站根目录，<code>git pull</code></li>
</ol>
<p>最后网站根目录拉取到文章，部署成功</p>
<p>其中 <code>3</code> 之前你需要在本地手动执行的，而且重装或者换电脑意味着需要在远程服务器配置新的公钥</p>
<h1>新流程</h1>
<p>新的半自动挡解决了 <code>1-3</code>，你只需要进行一次 git 提交</p>
<p>其他的事情交给 <code>Github Actions</code></p>
<p><img src="https://imba97.com/uploads/2022/11/github-actions-hexo-1.png" alt=""></p>
<p>整个流程是</p>
<ul>
<li>创建密钥，用于连接博客服务器</li>
<li>安装 <code>hexo</code></li>
<li>安装 <code>hexo</code> 插件</li>
<li>拉取主题</li>
<li>安装主题依赖包</li>
<li>检查主题目录</li>
<li>配置 <code>git</code></li>
<li>生成静态文件</li>
<li>部署！</li>
</ul>
<h1>工作流配置</h1>
<pre><code class="language-yaml">name: publish

on:
  push:
    branches:
      - master

jobs:
  publish-blog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1
        with:
          node-version: 14
          registry-url: https://registry.npmjs.org/

      - name: create keys
        run: |
          mkdir -p ~/.ssh
          echo '${{secrets.PRIVATE_KEY}}' &gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -t rsa 1.2.3.4 &gt;&gt; ~/.ssh/known_hosts
          ls -l -a ~/.ssh
      - name: install hexo-cli
        if: steps.cache.outputs.cache-hit != 'true'
        run: npm i -g hexo-cli

      - name: install hexo dependencies
        run: npm i
        working-directory: hexo

      - name: pull themes
        run: |
          mkdir -p hexo/themes
          git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git hexo/themes/butterfly
          echo 'themes config'
          cp hexo/themes_butterfly_config.yml hexo/themes/butterfly/_config.yml
          echo 'background image'
          cp hexo/images/bg.png hexo/themes/butterfly/source/img/bg.png
      - name: install themes package
        run: npm i
        working-directory: hexo/themes/butterfly

      - name: check themes package
        run: ls -l
        working-directory: hexo/themes/butterfly

      - name: git config
        run: |
          git config --global user.name imba97
          git config --global user.email mail@imba97.cn
          git config --global init.defaultBranch master
        working-directory: hexo

      - name: hexo generate
        run: |
          hexo clean
          hexo g
        working-directory: hexo

      - name: deploy!
        run: |
          git init
          git add .
          git commit -m &quot;deploy!&quot;
          git remote add origin root@1.2.3.4:/git/blog.git
          git push --force origin master
        working-directory: hexo/public
</code></pre>
<p>经过了大概 <code>40</code> 次左右的测试，最后终于是成功部署</p>
<p><img src="/uploads/2022/11/github-actions-hexo-2.png" alt=""></p>
<h2>主要问题</h2>
<p>期间主要解决 <code>git push</code> 不成功，私钥问题</p>
<p>问题的关键是，你不但需要在 <code>~/.ssh/</code> 下创建私钥，还需要创建一个 <code>known_hosts</code>，记录已知的服务器 <code>IP</code></p>
<p>私钥创建流程，我是在我本地创建了一对密钥，把私钥复制出来放到仓库配置中</p>
<p><img src="/uploads/2022/11/github-actions-hexo-3.png" alt=""></p>
<p>工作流使用的时候就把它写入到文件</p>
<pre><code class="language-bash">echo '${{secrets.PRIVATE_KEY}}' &gt; ~/.ssh/id_rsa
</code></pre>
<p>记录已知的服务器 <code>IP</code> 也就是工作流的这一步</p>
<pre><code class="language-bash">ssh-keyscan -t rsa 1.2.3.4 &gt;&gt; ~/.ssh/known_hosts
</code></pre>
<h2>其他小问题</h2>
<p>主要是<strong>主题</strong>，主题自定义的配置文件和图片，解决方案是直接放在仓库，复制过去</p>
<p>所以如果你打算用以上工作流，还得根据自己的主题去改改</p>
<h1>收工</h1>
<p>至此，我本地将不在管理博客的任何东西，甚至写博客都可以随时打开 <code>Github</code> 来写，比如现在</p>
<p><img src="/uploads/2022/11/github-actions-hexo-4.png" alt=""></p>
<p>而且依然也可以在本地写，如果换主题肯定还是要本地跑一下测一下的</p>
<p>总之，爽到了</p>
<h1>其他补充</h1>
<p>为啥说是半自动挡呢，因为我感觉服务器部分也需要继续优化，不能通过 <code>git hooks</code> 的方式部署，这样的话迁移到另一台服务器还得配置一遍，也是很麻烦的</p>
<p>之后考虑在工作流中用 <code>sftp</code> 直接上传的方式部署，需要解决的问题是生成静态文件数量非常多，目前考虑可以先压缩，再上传，再给博客服务器发送解压命令</p>
<p>等不懒了搞一搞，再水一篇文章 ( ´_ゝ｀)</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[前端自动打包上传推送钉钉工具]]></title>
            <link>https://imba97.com/posts/757</link>
            <guid isPermaLink="false">https://imba97.com/posts/757</guid>
            <pubDate>Thu, 27 Oct 2022 23:52:06 GMT</pubDate>
            <content:encoded><![CDATA[<h1>介绍</h1>
<p>在一部分公司中，上线时前端需要打包后发给后端或运维同学，他们再放到服务器上</p>
<p>但每次打包、压缩、发给别人这个过程太麻烦了，于是就有了这个小工具</p>
<p>我们就可以简单配置一下，在打包结束后自动执行这个程序</p>
<p><img src="https://imba97.com/uploads/2022/10/simple-zip-upload-1.png" alt=""></p>
<p>并且可以直接下载压缩包</p>
<p><img src="/uploads/2022/10/simple-zip-upload-2.png" alt=""></p>
<p><strong>好了，现在只要把后端骗进群，压力一下子就来到了后端这边</strong></p>
<p>除了自动展示版本、压缩包大小、打包日期之外</p>
<p>它还可以自动维护本地及线上的压缩包，每次执行时会检查非今天的文件，自动删除</p>
<h1>使用方法</h1>
<h2>安装</h2>
<pre><code>pnpm i -D simple-zip-upload
</code></pre>
<h2>导入</h2>
<pre><code class="language-javascript">import ZipUpload from 'simple-zip-upload'
</code></pre>
<h2>配置</h2>
<pre><code class="language-javascript">const fs = require('node:fs')
const path = require('node:path')
const ZipUpload = require('simple-zip-upload')

const ZipUploadConfig = {
  sftpOptions: {
    // SFTP 连接信息
    connect: {
      host: '1.2.3.4',
      port: 22,
      username: 'root',
      privateKey: fs.readFileSync('C:\\Users\\imba97\\.ssh\\id_rsa')
    },

    // 必填，上传的远程目录
    remoteDir: '/www/download/upload'
  },

  // 应用名，唯一名称，会拼接在压缩文件名
  app: 'simple-zip-upload',

  // 版本号填充“0”的个数，可选，默认 2
  fill: 3,

  // 压缩目标文件夹
  zipTargetDir: 'dist',

  // 本地压缩文件存放目录
  zipFileDir: 'build',

  // 压缩文件下载连接，最后会与文件名拼接
  host: 'https://download.imba97.com/upload/',

  // 钉钉推送卡片信息
  cardInfo: {
    title: '测试发布',
    subTitle: '没啥事就是测试一下',

    // 可选 默认是：
    // 版本 20221027005
    // 大小 1.00M
    // 打包日期 2022-10-27 23:57:31
    body(cardInfo) {
      return `
自定义内容
开头顶格写，不然会有空格
版本 ${cardInfo.version}
大小 ${cardInfo.size}
打包日期 ${cardInfo.date}`
    }

    // 可以是字符串
    // body: '自定义内容'
  },

  // 钉钉连接 token、secret
  dingTalk: {
    accessToken: '',
    secret: ''
  }
}
</code></pre>
<h2>使用</h2>
<h3>普通</h3>
<pre><code class="language-javascript">// 普通
new ZipUpload(ZipUploadConfig).start()
</code></pre>
<h3>Webpack</h3>
<pre><code class="language-javascript">// ...
{
  plugins: [new ZipUpload(ZipUploadConfig)]
}
</code></pre>
<h3>vue.config.js</h3>
<pre><code class="language-javascript">// ...
export default {
  // ...
  chainWebpack(config) {
    // 添加插件
    config
      .plugin('ZipUploadPlugin')
      .use(ZipUpload)
      .tap(() =&gt; [ZipUploadConfig])
  }
  // ...
}
</code></pre>
<h1>更多详情</h1>
<p><a href="https://github.com/imba97/simple-zip-upload">Github</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[红十字救护员考证见闻]]></title>
            <link>https://imba97.com/posts/756</link>
            <guid isPermaLink="false">https://imba97.com/posts/756</guid>
            <pubDate>Mon, 22 Aug 2022 21:07:32 GMT</pubDate>
            <content:encoded><![CDATA[<h1>先整个图显摆显摆</h1>
<p><img src="https://imba97.com/uploads/2022/08/redcross-1.jpg" alt=""></p>
<p>实际上初级没啥可显摆的</p>
<h1>为啥考</h1>
<p>因为去年参加了蓝天救援队，红十字救护员是蓝天的必备证，队里会统一组织培训和考试</p>
<p>虽然并不是必须的，但学习一下简单的急救方法也是对自己的提升嘛</p>
<h1>考试流程</h1>
<ol>
<li>看视频：在考前的一段时间看，需要达到一定的学习时长，视频中还会有选择题让你做</li>
<li>理论考试：我们是先到考场进行笔试，在手机上做选择题</li>
<li>实操培训：现场有两位红会工作人员给我们做实操培训，学员互相练习</li>
<li>实操考试：包扎部分是工作人员出题，学员互相做。心肺复苏是用假人</li>
</ol>
<h1>笔记</h1>
<p>记个笔记，有空自己也要翻一翻。因为考完之后有红会工作人员会电话回访，而且还会出题，上次回访有几道题答的不是很好</p>
<h2>1. 心肺复苏（CPR）</h2>
<ul>
<li>
<p><strong>评估现场环境安全</strong>，任何救援先要保证自己不会受伤</p>
</li>
<li>
<p><strong>判断意识</strong>，跪姿，双腿分开。双手拍肩，同时大声在耳边重复喊“喂！醒醒！你怎么了”，无反应</p>
</li>
<li>
<p><strong>检查呼吸</strong>，耳朵贴近患者鼻部，听呼吸声；眼睛看向患者胸部，看是否有起伏。观察 <strong>5-10秒</strong>，观察时急救读秒，1001、1002、1003 ...，无呼吸</p>
</li>
<li>
<p><strong>呼救</strong>，“快来人啊！这里有人晕倒了！我是红十字救护员，请您拨打120急救电话，通与不通都请告知我。请您去拿AED（有没有人知道AED，快去拿AED）”，根据现场实际情况，没人就别咋呼了，自己拨120吧</p>
</li>
<li>
<p><strong>松解衣领裤带，放置硬板床或平地</strong></p>
</li>
<li>
<p><strong>胸外心脏按压</strong>，按压频率 <strong>100-120次/分</strong>，急救读秒 01、02、03 ...，约 <strong>0.5秒</strong> 按压一次</p>
<p>部位及方式</p>
<ul>
<li>成人为男性两乳头连线中点（胸骨中下三分之一处）</li>
<li>儿童可以采用单手按压</li>
<li>婴儿部位为两乳头连线中点下，可采用双手指按压、双手环抱按压（两人施救时）</li>
</ul>
<p>深度</p>
<ul>
<li>成人 <strong>5-6CM</strong></li>
<li>儿童 约<strong>5CM</strong></li>
<li>婴儿 约<strong>4CM</strong></li>
</ul>
</li>
<li>
<p><strong>人工呼吸</strong>，打开气道，一手置前额，将头部向后倾斜，颈部仰伸，另一只手将下颌向上向前抬起。按压与人工呼吸比例为 <strong>30:2</strong>，儿童、婴儿在两名施救者的情况下可以为 <strong>15:2</strong>，实施时眼睛看胸部起伏。成人和儿童可实施口对口人工呼吸，婴儿可实施口对口鼻人工呼吸</p>
</li>
<li>
<p><strong>AED</strong>，尽早使用AED，AED与按压和人工呼吸同时进行，可根据AED的提示进行操作。电极片一个贴于右胸（胸骨右缘，锁骨之下）；另一个贴于左腋下肋骨位置（左腋前线之后第五肋间处）；电极片不能相互接触</p>
</li>
</ul>
<h2>2. 包扎</h2>
<ul>
<li>
<p><strong>环形包扎法</strong>，用于粗细较均的肢体包扎。伤口用敷料覆盖，缠绕绷带，每圈覆盖上一圈，<strong>4-5</strong> 圈后将多余的绷带固定在肢体外侧**（上肢在大拇指侧，下肢在小脚趾侧）**</p>
<p><img src="/uploads/2022/08/redcross-2.png" alt="图片来自网络"></p>
</li>
<li>
<p><strong>螺旋包扎法</strong>，用于粗细较均的肢体、躯干部位的包扎。伤口用敷料覆盖，先缠绕两圈，从第三圈开始缠绕是压住前一圈的 <strong>1/2</strong> 或 <strong>1/3</strong>，最后缠绕两圈，将多余的绷带固定在肢体外侧。检查血液循环</p>
<p><img src="/uploads/2022/08/redcross-3.png" alt="图片来自网络"></p>
</li>
<li>
<p><strong>八字包扎法</strong>，用于关节弯曲部位、手背等外伤伤口的处理。伤口使用敷料覆盖，先在敷料旁缠两圈固定绷带，再斜着覆盖敷料再另一端缠一圈，随后进行八字形缠绕。关节处则为上一圈下一圈，交叉在关节内侧。最后再缠绕两圈，将多余的绷带固定在肢体外侧。检查血液循环</p>
<p><img src="/uploads/2022/08/redcross-4.png" alt="图片来自网络"></p>
</li>
<li>
<p><strong>回返式包扎法</strong>，用于断肢末端伤口处理。伤口使用敷料覆盖，先在腕部缠两圈，随后拇指捏住绷带进行断面包扎，断面中间一次回返，两边各一次回返。然后再从远心端到近心端螺旋缠绕直至腕部，最后在腕部缠绕两圈，将多余的绷带固定在肢体外侧</p>
<p><img src="/uploads/2022/08/redcross-5.png" alt="图片来自网络"></p>
</li>
<li>
<p><strong>三角巾上臂悬吊式包扎法</strong>，用于前臂损伤和骨折。视情况对伤口进行包扎、夹板固定。将三角巾顶角置于患肢肘关节处并超过关节，露出手指，手部略高于肘部。上端角从患肢对侧绕到肩膀后，下端角从患肢侧与上端角相系，顶角收紧，整理剩余布料。有条件的话再用另一块三角巾折叠成条状或其他条状物绕过患肢在对侧腋下相系，固定患肢。最后检查血液循环情况</p>
<p><img src="/uploads/2022/08/redcross-6.png" alt="图片来自网络"></p>
</li>
</ul>
<h1>最后</h1>
<p>如果有错误或不完整的地方，欢迎纠错和补充</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Nest JS 入门笔记]]></title>
            <link>https://imba97.com/posts/755</link>
            <guid isPermaLink="false">https://imba97.com/posts/755</guid>
            <pubDate>Sun, 10 Jul 2022 15:48:29 GMT</pubDate>
            <content:encoded><![CDATA[<!-- eslint-disable -->
<h1>使用契机</h1>
<p>前段时间打算做一个系统，涉及前后端</p>
<p>前端分为用户端和后台管理，后台发布任务，用户端签到、签退</p>
<p>选后端框架时挑挑拣拣，最后感觉 <code>Nest JS</code> 的装饰器写法令我非常满意，于是就决定下来了</p>
<h1>学到的东西</h1>
<h2>Typeorm</h2>
<p>使用 <code>typeorm</code> 维护数据库字段</p>
<pre><code class="language-typescript">/**
 * 用户ID 主键
 */
@PrimaryGeneratedColumn({ type: 'int', comment: '用户ID' }) id: number

/**
 * 用户名
 */
@Column({ type: 'varchar', length: 20, comment: '用户名' }) username: string

/**
 * 密码
 */
@Column({ type: 'varchar', length: 50, comment: '密码' }) password: string
</code></pre>
<p>自动维护时间</p>
<pre><code class="language-typescript">/**
 * 创建时间
 */
@CreateDateColumn({ type: 'datetime', comment: '创建时间' }) created_at: Date

/**
 * 最后更新时间
 */
@UpdateDateColumn({ type: 'datetime', comment: '最后更新时间' }) updated_at: Date

/**
 * 删除时间
 */
@DeleteDateColumn({ type: 'datetime', comment: '删除时间' }) deleted_at: Date
</code></pre>
<p>删除时间可以用于软删除</p>
<p>数据关系，比如一个用户有多辆车</p>
<pre><code class="language-typescript">// user.entity
@OneToMany(() =&gt; Car, (car) =&gt; car.user)
car_list: Car[]

// car.entity
@ManyToOne(() =&gt; User, (user) =&gt; user.car_list)
@JoinColumn()
user: number
</code></pre>
<h2>JWT 认证</h2>
<p>因为<a href="https://docs.nestjs.cn/9/security?id=%e8%ae%a4%e8%af%81%ef%bc%88authentication%ef%bc%89">官网文档</a>太详细了，就简单说一下</p>
<p>分为 <code>local</code> 策略和 <code>JWT</code> 策略</p>
<p><code>local</code> 策略用于登录接口，验证用户名密码是否正确，登录成功后把 <code>Token</code> 返回，前端保存在本地</p>
<p><code>JWT</code> 就是其他接口的验证了，请求接口时携带保存的 <code>Token</code>，后端能从 <code>Token</code> 中取到保存的户信息</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[隔离不完全攻略]]></title>
            <link>https://imba97.com/posts/754</link>
            <guid isPermaLink="false">https://imba97.com/posts/754</guid>
            <pubDate>Tue, 19 Apr 2022 21:45:22 GMT</pubDate>
            <content:encoded><![CDATA[<h1>隔离原因</h1>
<p>公司领导从外地回来，第二天通知说他是密接，当天我们一整个公司就去隔离了</p>
<p>于是分享一下隔离的整个流程</p>
<!--more-->
<h1>基本问答</h1>
<ol>
<li>要钱吗？<strong>不要</strong></li>
<li>几天？<strong>密接14天，次密接7天（特殊情况按社区通知为准）</strong></li>
<li>隔离宾馆。<strong>一般是社区安排就近的</strong></li>
<li>饮食。<strong>免费提供三餐，部分宾馆可以点外卖但送餐时间固定</strong></li>
</ol>
<h1>隔离攻略</h1>
<h2>回家等通知</h2>
<p>密接的领导是下午3点左右接到通知，上报了所有人的姓名手机号，过了会儿得到反馈要求所有人回家收拾好等待隔离安排</p>
<p>此时还是绿码，可以出入小区</p>
<h2>社区来电</h2>
<p>我到家后收拾好东西等通知，期间社区人员会联系，询问家里住着多少人，是否是合租，合租的是否有独立卫生间</p>
<p>如果你之前没有跟任何人报备过姓名电话，可能需要你自己联系社区</p>
<p>此时健康码会变红（次密接，其他情况不知道）</p>
<img src="https://imba97.com/uploads/2022/04/quarantine-1.jpg" style="zoom: 33%;" />
<h2>建议准备的东西</h2>
<p>除了带住宾馆需要的物品之外，一定要带<strong>身份证</strong>，建议带口罩若干、笔</p>
<h2>提前写好活动情况</h2>
<p>理论上是14天内的，但像我这种是一天前才跟密接有接触的，所以就只写从开始有接触到隔离时间的就行</p>
<p>可以提前记录到手机上，进入宾馆后会发表格填（带笔就是填这个）</p>
<h2>等待隔离转运车</h2>
<p>一般会有电话通知，叫你出来到一般小区门口都会有的临时隔离区（临时帐篷）</p>
<p>会发给你防护服，穿上后在临时隔离区等车</p>
<p>我当时被叫过去后，穿好隔离服就一直站在那，那车就是不来，至少得等了三个小时，估计是当天隔离的人比较多</p>
<h2>上车前采集核酸</h2>
<p>上车前会做一次核酸，鼻拭子，需要出示健康码和身份证</p>
<p>是我有史以来做的最酸爽的一次鼻拭子，捅进去各种转，还两个鼻孔都要捅，但我居然没打喷嚏！</p>
<h2>前往隔离点</h2>
<p>下车后会有工作人员引导，一直到房间，不用多说</p>
<h2>隔离日常</h2>
<p>一般每天早上4点到6点会有一次核酸，鼻咽拭子都做</p>
<p>三餐，有些可以点外卖，最后会在统一配餐时间放在门口桌子上</p>
<p>垃圾放在宾馆提供的垃圾袋内，扔的时候要<strong>封口</strong>，放在门口</p>
<p>会发体温计，基本上每天量一次</p>
<h2>隔离结束</h2>
<p>一般结束当天社区会把健康码调整为绿色，如果一直是红色，你可以点击健康码旁边的<strong>修改</strong>，填一下表单提交就会变绿</p>
<p>如果不行就联系社区或者隔离前打电话的工作人员</p>
<p>次密接隔离完后不需要再居家隔离，所以就可以正常坐地铁、打车</p>
<p>以上</p>
<h1>隔离感受</h1>
<p>u1s1，我好喜欢隔离。一日三餐免费还有人送、垃圾放门口有人收、居家办公，走两步就到工位、看不见领导！</p>
<p>这七天真的太爽了，不过我倒不想一直隔离下去</p>
<p>蹭了国家七天的吃住，继续当好我的志愿者回馈社会吧</p>
<p>就是这样，告辞</p>
<h1>部分照片</h1>
<p><img src="/uploads/2022/04/quarantine-2.jpg" alt=""></p>
<p><img src="/uploads/2022/04/quarantine-3.jpg" alt="我已出仓 感觉良好"></p>
<p><img src="/uploads/2022/04/quarantine-4.jpg" alt=""></p>
<p><img src="/uploads/2022/04/quarantine-5.jpg" alt=""></p>
<p><img src="/uploads/2022/04/quarantine-6.jpg" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Rust 报错 link.exe was not found]]></title>
            <link>https://imba97.com/posts/753</link>
            <guid isPermaLink="false">https://imba97.com/posts/753</guid>
            <pubDate>Mon, 28 Mar 2022 20:23:18 GMT</pubDate>
            <content:encoded><![CDATA[<h1>报错简介</h1>
<p>记录一个报错</p>
<p>刚开始学<code>Rust</code>，基本的一些环境安装完建了个项目后发现跑不了，报错：<code>linker link.exe not found</code></p>
<p><img src="https://imba97.com/uploads/2022/03/rust-1.png" alt=""></p>
<p>后面给的提示也很明确，<code>please ensure that VS 2013, VS 2015, VS 2017 or VS 2019 was installed with the Visual C++ option</code></p>
<p>少安装了一些环境</p>
<h1>解决</h1>
<ol>
<li>
<p>下载 <a href="https://visualstudio.microsoft.com/zh-hans/visual-cpp-build-tools/">Build Tools</a>，等它自动下载安装</p>
</li>
<li>
<p><code>Build Tools</code>相当于一个安装器，可以安装各种环境</p>
<p>安装环境时选择<strong>单个组件</strong>，够两个即可：<code>MSVC v142 - VS 2019 C++ x64/x86 生成工具(最新)</code>、<code>Windows 10 SDK (xxx)</code>，版本号可能有区别</p>
<p><img src="/uploads/2022/03/rust-2.png" alt=""></p>
</li>
</ol>
<p>等安装完成后可能要重启，不出意外应该就能跑起来了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Electron 主进程与渲染进程通讯]]></title>
            <link>https://imba97.com/posts/752</link>
            <guid isPermaLink="false">https://imba97.com/posts/752</guid>
            <pubDate>Sun, 13 Feb 2022 16:29:49 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Electron 的通讯机制</h1>
<p>Electron 在运行时分为<strong>主进程</strong>和<strong>渲染器进程</strong>(渲染进程)，<strong>主进程</strong>可以用<code>NodeJS</code>的 api，<strong>渲染进程</strong>可以用浏览器的 api</p>
<p>两者之间需要通过 Electron 提供的 api 来通讯</p>
<!--more-->
<h2>渲染进程</h2>
<p>首先需要在渲染进程设置一个监听器，用来监听主进程发来的消息</p>
<p>然后主动发送一个消息给主进程</p>
<pre><code class="language-javascript">// 监听器
ipcRenderer.on('mainSend', (event, message) =&gt; {
  console.log('主进程发来的消息', message)
})

// 在合适的地方发送给主进程，比如按钮点击事件中
button.onclick = function () {
  ipcRenderer.send('rendererSend', 'xxx')
}
</code></pre>
<h2>主进程</h2>
<p>接下来是主进程中，需要创建一个监听器，用来监听渲染进程发过来的消息</p>
<pre><code class="language-javascript">ipcMain.on('rendererSend', (event, path) =&gt; {
  // 这里是主进程的处理，比如渲染进程发来的是一个文件路径
  // 主进程可以调用 NodeJs 的 api
  fs.unlink(path)
  // 删除后在告诉渲染进程已经删掉了
  event.sender.send('mainSend', `${path} 已删除`)
})
</code></pre>
<p>至此，渲染进程的监听器<code>mainSend</code>会接收到消息，<code>xxx 已删除</code></p>
<h2>带来的问题</h2>
<p>个人感觉这种写法比较难受，因为我加一个通讯，就得写一遍上述内容</p>
<p>最少的情况也是<strong>主进程</strong>和<strong>渲染进程</strong>各要写一个，最后会有大量的<code>on</code>、<code>send</code></p>
<h1>electron-vue-event-manager</h1>
<p>于是解决方案出现了，<code>electron-vue-event-manager</code>是个基于<code>electron-vue</code>的事件管理器</p>
<p>可以让你在任意地方创建监听器，在任意地方触发它（发起广播）</p>
<p><strong>它能解决如下问题</strong></p>
<ul>
<li><strong>主进程</strong>与<strong>渲染进程</strong>通信</li>
<li><strong>渲染进程</strong>自身的通信 (同一窗口内<code>Vue</code>组件之间)</li>
<li><strong>渲染进程</strong>与<strong>其他渲染进程</strong>通信 (不同窗口之间)</li>
</ul>
<h2>初始化</h2>
<p>既然每个通信都需要一个<code>on</code>和一个<code>send</code>，那其实我只需创建一次</p>
<p>再通过不同的参数区分具体是什么通讯，不就解决了每种通讯要创建一个<code>on</code>和<code>send</code>的问题了</p>
<p><code>electron-vue-event-manager</code>的解决方案正是如此，在<strong>主进程</strong>和<strong>渲染进程</strong>中调用各自的初始化函数</p>
<pre><code class="language-typescript">// 主线程 (需要把所有创建的窗口传进去)
EventManager.Instance().mainInit([
  {
    // 创建的窗口，类型 BrowserWindow
    window: window1,
    // 类型，唯一标示
    type: 'window1'
  },
  {
    window: window2,
    type: 'window2'
  }
])

// 渲染进程
EventManager.Instance().rendererInit()
</code></pre>
<h2>监听器</h2>
<p>然后就可以在任何地方写一个监听器，比如下面这个是在<strong>渲染进程</strong>中，<code>Vue</code>的某个页面的<code>created</code>生命周期中</p>
<pre><code class="language-typescript">// Window2 添加监听事件
EventManager.Instance().addEventListener&lt;string&gt;(
  // 事件类型 (string)
  EventType.Window1SendMessage,
  // 接收到广播后的回调函数
  (window1Message) =&gt; {
    this.message = window1Message
    console.log('接收到来自 Window1 的消息：', window1Message)
  }
)
</code></pre>
<h2>广播</h2>
<p>广播指的是调用监听器，监听器可以有多个，相当于收音机，所以<strong>调用</strong>这个动作叫做广播</p>
<p>以下是在<strong>另一个渲染进程</strong>中</p>
<pre><code class="language-typescript">// Window1 进行广播
EventManager.Instance().broadcast&lt;string&gt;(
  // 事件类型 (string)
  EventType.Window1SendMessage,
  // 参数 (输入框内容)
  this.inputValue
)
</code></pre>
<h2>实际应用</h2>
<p>以上的代码实际应用如下图，你可以从<code>Window1</code>发送消息给<code>Window2</code></p>
<p>也可以在<code>Vue</code>组件之间传递</p>
<p><img src="https://imba97.com/uploads/2022/02/electron-vue-event-manager-1.png" alt=""></p>
<h2>网络请求</h2>
<p>除了跨窗口、跨组件通信之外，还有<strong>网络请求</strong>是需要封装的</p>
<p>因为渲染进程是个浏览器窗口，发起网络请求也就会有<strong>跨域问题</strong></p>
<p>所以你需要先让<strong>渲染进程</strong>把请求相关的数据发给<strong>主进程</strong>，再由<strong>主进程</strong>发起网络请求，拿到数据后发回<strong>渲染进程</strong></p>
<p>在<code>electron-vue-event-manager</code>基于<code>axios</code>封装好了一个方法，可以在任何地方调用即可发起网络请求</p>
<pre><code class="language-typescript">EventManager.Instance().sendRequest({
  url: 'Request URL',
  method: 'POST'
}).then((data) =&gt; {
  console.log(data)
})

// EventManager.Instance().sendRequest(options) === axios(options).then(response =&gt; response.data)
</code></pre>
<h1>使用</h1>
<div style="display: flex; align-items: center;">
    <p>当前<img src="https://img.shields.io/npm/v/electron-vue-event-manager?label=electron-vue-event-manager&style=flat-square" style="display: inline; margin: 0 5px;" /></p>
</div>
<p><code>1.0.0</code>之前为<strong>自用版</strong>，因为还有很多没完善</p>
<p>目前能做到像上述的功能一样，但可能存在问题，<strong>谨慎使用</strong></p>
<p>你可以在 <a href="https://github.com/imba97/electron-vue-event-manager">Github</a>、<a href="https://www.npmjs.com/package/electron-vue-event-manager">Npmjs</a> 获取到更多信息</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[钉钉日报机器人]]></title>
            <link>https://imba97.com/posts/751</link>
            <guid isPermaLink="false">https://imba97.com/posts/751</guid>
            <pubDate>Sun, 09 Jan 2022 19:46:56 GMT</pubDate>
            <content:encoded><![CDATA[<h1>介绍</h1>
<p>NodeJS 开发的一个根据 <code>git log</code> 自动生成日报的程序，并可以通过钉钉机器人发到群里</p>
<p><img src="https://imba97.com/uploads/2021/12/report-2021-5.png" alt=""></p>
<!--more-->
<h1>开发契机</h1>
<p>我们每天要写钉钉日报，最开始我是查看<code>git</code>的提交日志，然后选出有价值的提交</p>
<p>比如修复了什么问题、优化了什么功能等，像修改了配置文件、隐藏某些功能等就不会写到日报里</p>
<p>这样手动筛选挺麻烦，下班了还要现查<code>git</code>、现找有价值的提交</p>
<p>作为一个程序猿，当然不能麻烦了自己，于是就有了这个东西</p>
<h1>使用</h1>
<p>我目前是放在了公司电脑上，并设置了开机启动</p>
<p>也就是说我只要上班，电脑开着，它到时间就会自动收集今天<code>git</code>提交中带有前缀的内容，整理好并发到一个钉钉群里</p>
<h2>拉取项目</h2>
<p>Github：<a href="https://github.com/imba97/imba97_project/tree/master/dingtalk-report-robot">dingtalk-report-robot</a></p>
<pre><code class="language-shell">git clone git@github.com:imba97/imba97_project.git
</code></pre>
<p>进入<code>dingtalk-report-robot</code>目录</p>
<h2>安装依赖包</h2>
<pre><code class="language-shell">npm i
</code></pre>
<h2>配置</h2>
<p>打开<code>src/config.js</code>，进行配置</p>
<h2>安装 pm2 和 自启动包</h2>
<pre><code class="language-shell">npm install pm2 pm2-windows-startup -g
</code></pre>
<p><code>pm2</code>可以管理<code>NodeJs</code>项目，也用于开机启动</p>
<h2>自启动包安装</h2>
<pre><code class="language-shell">pm2-startup install
</code></pre>
<h2>启动项目</h2>
<pre><code class="language-shell">pm2 start 项目目录/src/ding-robot.js --name 名称
</code></pre>
<p>文件是<code>ding-robot.js</code>，注意别启动错了</p>
<h2>保存</h2>
<pre><code class="language-shell">pm2 save
</code></pre>
<h1>手动执行</h1>
<p>程序可以使用<code>node</code>直接执行，手动执行后会自动复制到剪切板</p>
<h2>获取日报</h2>
<p>会根据配置文件的天数获取日报</p>
<pre><code class="language-shell">node ./src/index.js
</code></pre>
<h2>指定天数</h2>
<p>获取今天的日报</p>
<pre><code class="language-shell">node ./src/index.js 0
</code></pre>
<p>获取 7 天内的日报</p>
<pre><code class="language-shell">node ./src/index.js 7
</code></pre>
<h2>指定日期</h2>
<p>获取指定日期的日报</p>
<pre><code class="language-shell">node ./src/index.js 2023-07-14
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[留下了遗憾的 2021]]></title>
            <link>https://imba97.com/posts/750</link>
            <guid isPermaLink="false">https://imba97.com/posts/750</guid>
            <pubDate>Fri, 31 Dec 2021 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>又到了写年度总结的时间了，2021年状态依然</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<!--more-->
<h1>生活</h1>
<p>年初做了近视手术，因为打算下半年当兵，4月从巴中回到了山东（但是既然你能看到这篇文章，说明我没去成，原因下面会说）</p>
<p>回家后不久把我的串串（猫）也托运回来了，目前丢给我妈养了</p>
<p>上半年跟着朋友健身，健身一段时间后无意间一摸肱二头肌，给我自己吓到了，粗了一圈</p>
<p>不过下半年又回归了咸鱼状态，肌肉就又没了</p>
<p>虽然说去年说了“越来越想把台式机换成笔记本了”，但还是真香了，入手了<code>HUAWEI Mate Station S</code></p>
<p>不过也入手了一台笔记本，用来玩VR游戏的，VR 设备是华为<code>VR Glass 6DoF</code></p>
<h1>遗憾</h1>
<p>本年度最大的遗憾就是当兵没去成，原因是刚刚刚刚好下半年<a href="https://xw.qq.com/amphtml/20210713A0B99Y00">征兵年龄修改</a></p>
<p>本来是通过了，武装部的短信都来了</p>
<img src="https://imba97.com/uploads/2021/12/report-2021-1.jpg" style="zoom:50%;" />
<p>6月份的某天我一看征兵网，发现年龄不合格了</p>
<p>非常的巧，今年下半年开始，征兵年龄修改为全日制18-24，非全日制18-22，我刚好是非全日制</p>
<h1>工作</h1>
<p>其实在打算当兵时我就做好了没去成的准备，没去成就继续当我的程序猿</p>
<p>7月来到杭州开始找工作，当时方向是<code>PHP</code>和<code>web前端</code>，更偏向<code>web前端</code>，但我没<code>web前端</code>的实际工作经验，只有一些个人项目</p>
<p>面试的时候我甚至连<code>Vue</code>的生命周期都背不出来，不过好在有点基础，不会的现学也挺快</p>
<p>跟朋友开发的游戏因为近视手术暂时搁置，之后朋友也工作了，就彻底停了，以后应该还会继续</p>
<h1>程序</h1>
<p>今年跟<strong>上传</strong>功能干上了，写了好多上传相关的东西</p>
<ol>
<li>
<p><strong>两个 Picgo 插件</strong></p>
<p>写博客的时候想把图片上传到自己服务器上，但发现没有我想要的插件，于是被迫自己做了</p>
<p><a href="https://github.com/imba97/picgo-plugin-ftp-uploader">picgo-plugin-ftp-uploader</a> FTP 上传</p>
<p><a href="https://github.com/imba97/picgo-plugin-sftp-uploader">picgo-plugin-sftp-uploader</a> SFTP 上传</p>
<p><img src="/uploads/2021/12/report-2021-2.jpg" alt=""></p>
<hr>
</li>
<li>
<p><strong>前端项目部署工具</strong></p>
<p>可以快速把打包后的前端项目部署到服务器的工具，也是 SFTP 上传</p>
<p>可单独使用，可作为<code>webpack</code>插件使用，也可以单纯作为上传工具用</p>
<p><img src="/uploads/2021/12/report-2021-3.jpg" alt=""></p>
<p>Github：<a href="https://github.com/imba97/simple-sftp-uploader">simple-sftp-uploader</a></p>
<p>Npm：<a href="https://npmjs.com/simple-sftp-uploader">simple-sftp-uploader</a></p>
<hr>
</li>
<li>
<p><strong>iconfont 样式生成工具</strong></p>
<p>根据 <a href="https://iconfont.cn">iconfont</a> 的项目样式链接，将字体转<code>Base64</code>、样式压缩后生成<code>css</code>文件到指定目录</p>
<p>Github：<a href="https://github.com/imba97/simple-iconfont-builder">simple-iconfont-builder</a></p>
<p>Npm：<a href="https://npmjs.com/simple-iconfont-builder">simple-iconfont-builder</a></p>
<hr>
</li>
<li>
<p><strong>重构 Btools 浏览器插件</strong></p>
<p>Btools 浏览器插件的重构版本，这个虽然2020年5月就打算动工，但一直懒迟迟没做</p>
<p>今年打算找前端工作了，想着得有个像样的项目，所以光速重构并发布了</p>
<p>还砍了原来版本的一些功能，反向更新最为致命（但周用户数居然破2w了，惊了）</p>
<p>Github：<a href="https://github.com/imba97/Btools-vue">Btools-vue</a></p>
<hr>
</li>
<li>
<p><strong>bsr-helper</strong></p>
<p>蓝天救援队助手，使用<code>Vue</code>+<code>TypeScript</code>+<code>Vant UI</code>做的面向移动端设备的网页应用</p>
<p>蓝天队员每次参加活动或者结束后回家，都需要在群里发一个“消息回传”，大致是把时间、人员、交通手段、出发地、目的地等信息汇总发过去</p>
<p>每次手打或者复制别人的再改感觉比较麻烦，所以就做了一个可以通过简单设置、点选后自动生成信息回传内容的小工具</p>
<p>Github：<a href="https://github.com/bsr-project/bsr-helper">bsr-helper</a></p>
<hr>
</li>
<li>
<p><strong>微信机器人</strong></p>
<p><a href="/posts/741">《程序猿复健计划之微信机器人》</a> 又名：《如何跟微信好友对线时轻松取胜》</p>
<p>Github：<a href="https://github.com/imba97/imba97_project/tree/master/wechat_bot">wechat_bot</a></p>
<hr>
</li>
<li>
<p><strong><s>B站机器人</s>（已废弃）</strong></p>
<p>B站改了接口，目前不能用了</p>
<p><a href="/posts/742">《程序猿复健计划之B站机器人》</a></p>
<p>Github：<a href="https://github.com/imba97/imba97_project/tree/master/bili_bot">bili_bot</a></p>
<p>Docker：<a href="https://hub.docker.com/r/imba97/bili-bot/tags">bili-bot</a></p>
<hr>
</li>
<li>
<p><strong>索尼中国自动签到</strong></p>
<p>每天 <code>00:00:05</code> 这个时间点在 <a href="https://www.sonystyle.com.cn/">索尼中国官网</a> 和 <a href="https://www.sonystyle.com.cn/mysony/bbs/web/">My Sony 社区</a> 自动签到</p>
<p><img src="/uploads/2021/12/report-2021-4.png" alt=""></p>
<p>Github：<a href="https://github.com/imba97/imba97_project/tree/master/sonystyle-auto-sign-in">sonystyle-auto-sign-in</a></p>
<hr>
</li>
<li>
<p><strong>表格生成器</strong></p>
<p>给朋友写的表格统计器，朋友的工作是负责旅游用车的调度，每月要统计汇总，需要在个有<strong>40多个工作簿</strong>的表格中，每个工作簿是一辆车的信息，记录着用车天数、里程、加油升数、加油的钱、乘客人数等等，把所有的数据加起来汇总成一张表格。并且这样的表格有两个，手动统计非常麻烦。这个工具可以把朋友的摸鱼时间直接拉满，每个表格直接拖进去不到1秒就能自动生成汇总表格</p>
<p>后来用这个，给我姨做了一个类似的，她是大学老师，需要统计自己班学生的成绩，涉及到两张表，<strong>全校学生表</strong>和<strong>班级学生表</strong>。统计方式是在全校学生表中，根据学号找到并复制学生成绩，粘贴到班级学生表对应的学生成绩一栏。使用工具只需要把两个表分别拖到指定区域，点击生成即可</p>
<p>也是想通过做项目的方式研究一下<code>electron</code>，体验一下前端语言开发桌面应用</p>
<p>Github：<a href="https://github.com/imba97/electron-excel-generator">electron-excel-generator</a></p>
<hr>
</li>
<li>
<p><strong>electron-vue 事件监听广播系统</strong></p>
<p>基于<code>electron</code>主进程和渲染进程的交互逻辑</p>
<p>主要功能是统一以下消息传递</p>
<ul>
<li>主进程到渲染进程</li>
<li>渲染进程到主进程</li>
<li>渲染进程到渲染进程</li>
<li>渲染进程中<code>Vue</code>组件之间</li>
</ul>
<p>Github：<a href="https://github.com/imba97/electron-vue-event-manager">electron-vue-event-manager</a></p>
<p>Npm：<a href="https://www.npmjs.com/package/electron-vue-event-manager">electron-vue-event-manager</a></p>
<hr>
</li>
<li>
<p><strong>钉钉日报助手</strong></p>
<p>每天提交日报多少有点麻烦，之前我是每天去看<code>git</code>的提交记录，一条条复制粘贴到日报里。但这完全能用程序解决，这个程序就是可以在<strong>指定时间</strong>，去一个或多个<strong>指定项目目录</strong>下拉取<code>git log</code>，从中取出<strong>指定用户</strong>提交的信息，没有则会过滤，并可以<strong>指定前缀</strong>，只有带这个前缀的才会被抓取，最后自动发送到钉钉群</p>
<p><img src="/uploads/2021/12/report-2021-5.png" alt=""></p>
<p>比如我设置的，每天<code>18:00</code>拉取4个项目的日志，用户是<code>imba97</code>，前缀是<code>report: </code></p>
<p>使用<code>pm2</code>让这个程序开机启动，所以只要我在公司，开了机，这个程序就会启动，到点就会整理我今天的日志</p>
</li>
</ol>
<hr>
<p>这一年对<code>node.js</code>这套东西有了更深的认识，自己也发了几个<code>npm</code>包，虽然都是些小玩意儿，但也算是从<code>import</code>迈向了<code>export</code>的第一步</p>
<p>还有是赞美开源，可以这么说，如果没有开源社区，单靠我一个人的力量，这里面任何一个项目都完不成</p>
<h1>博客</h1>
<p>加入了“<a href="https://github.com/volfclub/travellings/issues/748">开往</a>”</p>
<p>加入了“<a href="https://www.foreverblog.cn/blog/2491.html">十年之约</a>”</p>
<h1>2022 Flag</h1>
<ol>
<li>拿下日语 JLPT N1，目标分数<strong>150</strong>！</li>
<li>系统学习数据结构和设计模式</li>
</ol>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Log4j 漏洞，阿里躺枪]]></title>
            <link>https://imba97.com/posts/749</link>
            <guid isPermaLink="false">https://imba97.com/posts/749</guid>
            <pubDate>Thu, 23 Dec 2021 01:22:08 GMT</pubDate>
            <content:encoded><![CDATA[<h1>说在前头</h1>
<p>大半夜看到新闻，大概意思是“阿里发现<code>Log4j</code>的漏洞后率先告知了美国的阿帕奇软件基金会，未及时向电信主管部门报告”</p>
<p>好像很早之前就有，但话题还没发酵的这么厉害，没看到，今天看到了感觉不太对劲，就来说说个人看法</p>
<p>首先这篇文章<span style="color: red;"><strong>不是给阿里洗白</strong></span>，只针对以上论述进行探讨</p>
<!--more-->
<h1>个人看法</h1>
<p>从一个程序员的角度看，我认为这个论述是有点奇怪的，以下几点</p>
<h2>1. 程序的BUG反馈</h2>
<p>对于一个程序员来说，使用别人或别的组织的程序时发现了问题，一般情况下有两个选择</p>
<ol>
<li>向开发者或组织反馈问题，等待对方确认和修复**（Issue）**</li>
<li>如果是开源可以自己修复问题并提交到开源项目中**（Pull Request）**</li>
</ol>
<p>比如以下是我的一个开源项目中，用户给我提的BUG反馈，以及我确认修复后关闭的反馈</p>
<p><img src="https://imba97.com/uploads/2021/12/Log4j-ali-1.png" alt=""></p>
<p>再比如我发现了别人项目的问题，修复并提交到他的项目中</p>
<p><img src="/uploads/2021/12/Log4j-ali-2.png" alt=""></p>
<p><strong><span style="color: red;">个人猜测</span>阿里为什么要先联系阿帕奇软件基金会？</strong></p>
<p>因为他们是<code>Log4j</code>的开发组织，问题的确认及修复会更快</p>
<p><strong><span style="color: red;">个人猜测</span>阿里为什么会未及时向电信主管部门报告？</strong></p>
<p>可能是我见识短，我猜测是一个开源软件出了严重的BUG，需要第一时间上报给电信主管部门的先例，很少</p>
<p>或者说因为没有上报被曝出来的，很少。我是没怎么听说过</p>
<h2>2. 开源项目和组织</h2>
<p><code>Log4j</code>是个开源项目，开源协议是<code>Apache-2.0 License</code></p>
<p><img src="/uploads/2021/12/Log4j-ali-3.png" alt=""></p>
<p>开源就意味着这个软件<strong>不属于</strong>任何一个人或者国家，而是属于整个<strong>开源社区</strong>（个人理解），每个人都可以是作者之一</p>
<p>所以很多文章特意强调了**“美国”<strong>，只能说是一个噱头。虽然</strong>阿帕奇软件基金会**确实是美国的，我不否认</p>
<h2>3. 这可是提交了一个BUG啊！</h2>
<p>这可是提交了<strong>一个BUG</strong>啊！又不是提交了国家机密！</p>
<p><strong>个人认为</strong>，阿里只是之前的一些事上引起了大众的反感（违反《反垄断法》之类的）</p>
<p>但如果单看这件事上，说阿里的技术团队是**“吹哨人”**都不为过。国内外的软件应用、整个开源社区都得到了止损</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[bsr-hepler 使用教程 - 信息回传]]></title>
            <link>https://imba97.com/posts/748</link>
            <guid isPermaLink="false">https://imba97.com/posts/748</guid>
            <pubDate>Mon, 15 Nov 2021 21:35:00 GMT</pubDate>
            <content:encoded><![CDATA[<h1>简介</h1>
<p><code>bsr-hepler</code>是一些小工具，帮助蓝天队员快速完成一些日常在手机上的操作</p>
<p>目前只有一个功能：<strong>信息回传</strong></p>
<!--more-->
<h1>使用</h1>
<p>在工具主页：<a href="https://bsr.imba97.com">https://bsr.imba97.com</a></p>
<p>可以看到这唯一的一个功能</p>
<p><img src="https://imba97.com/uploads/2021/11/bsr-helper-information-return-1.png" alt=""></p>
<p>点击进入该功能，可以看到这样一个页面（默认是全部展开的，这里演示我手动收起了）</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-2.png" alt=""></p>
<p>最上面是<strong>出发</strong>、<strong>到家</strong>的选项，不同选项生成的信息回传文本会有所区别</p>
<p>右侧的<strong>复制</strong>点击可快速复制文本</p>
<h1>设置项</h1>
<p>如果什么都不设置，这段话看起来怪怪的，所以你需要设置每一项的内容</p>
<p>这些设置会在你点击复制按钮时<strong>保存</strong>，所以大部分只需要设置一次即可</p>
<h2>任务简介</h2>
<p>可以设置任务简介，比如：“马拉松安保训练”</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-3.png" alt=""></p>
<h2>时间</h2>
<p>可以设置使用当前时间、设置开始时间和结束时间</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-4.png" alt=""></p>
<p>如果你需要自选时间，请关闭使用当前时间开关</p>
<p>关闭后可自选时间</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-5.gif" alt=""></p>
<p>自定义时间时，可以选择结束时间，一般用于到家的消息回传，会显示一个时间段</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-6.png" alt=""></p>
<h2>出发地、目的地</h2>
<p>可以设置地点，点击复制按钮后，本次设置的地点会保存在<strong>历史地点</strong></p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-7.png" alt=""></p>
<h2>人员</h2>
<p>可以添加人员、设置姓名、<strong>左滑删除</strong></p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-8.png" alt=""></p>
<p>程序将根据打勾的人<strong>自动生成人数</strong>，无需手动输入</p>
<h2>交通工具</h2>
<p>可以选择交通工具、设置车牌号、设置自定义交通工具</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-9.png" alt=""></p>
<p>如果设置为“<strong>开车</strong>”，那么<strong>人员设置</strong>会与交通工具联动</p>
<p>此时<strong>人员</strong>中<strong>第一个打勾的人</strong>是驾驶员，会出现<strong>驾驶员标识</strong></p>
<p>可以拖拽排序</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-10.gif" alt=""></p>
<p>程序会根据<strong>勾选</strong>、<strong>排序</strong>自动设置消息回传中人员的显示、排序</p>
<h2>专业工具</h2>
<p>可以选择专业工具、搜索专业工具、添加自定义专业工具</p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-11.png" alt=""></p>
<p>目前专业工具只是演示性的加了这四个，如果想<strong>添加</strong>，可以在搜索中输入，搜索内容不存在时会出现<strong>添加按钮</strong></p>
<p><img src="/uploads/2021/11/bsr-helper-information-return-12.png" alt=""></p>
<p>更多专业工具会慢慢完善</p>
<h1>其他</h1>
<p>其他的功能及注意点</p>
<h2>自动切换出发、到家</h2>
<p>当你出发时复制了一次<strong>出发</strong>的消息回传后，下次再打开这个页面，会自动切换到<strong>到家</strong>，并且自动填写<strong>起始时间</strong>和<strong>结束时间</strong>，自动切换<strong>出发地</strong>、<strong>目的地</strong></p>
<h2>时间优化</h2>
<p>起始时间、结束时间做了一点优化，相同年时不显示年、同月不显示月、同日不显示日</p>
<h2>本地存储</h2>
<p>所有数据都是存在浏览器中，如果更换了浏览器或手机再打开页面，则需要重新设置</p>
<p>之后会考虑做<strong>导出设置项</strong>、<strong>导入设置项</strong></p>
<h1>开源</h1>
<p>本程序使用<code>Vue2</code>+<code>Vant</code>+<code>TypeScript</code>开发</p>
<p>欢迎蓝天的程序员大佬们一起开发各种小工具</p>
<p><a href="https://github.com/bsr-project/bsr-helper">项目地址</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[位枚举]]></title>
            <link>https://imba97.com/posts/747</link>
            <guid isPermaLink="false">https://imba97.com/posts/747</guid>
            <pubDate>Sun, 17 Oct 2021 22:43:36 GMT</pubDate>
            <content:encoded><![CDATA[<h1>位枚举是个啥</h1>
<h2>枚举</h2>
<p>首先来说一下枚举，在一些语言中我们可以用<code>enum</code>等关键字定义枚举，它的作用是让值更易读</p>
<p>比如我们设置一个人的状态：吃饭、睡觉、打豆豆，分别为0、1、2</p>
<!--more-->
<p>如果我们在一个地方判断这个人当前状态是否是吃饭会用<code>status == 0</code></p>
<p>但时间长了你可能就忘了0是指的什么，所以你还要需要去看</p>
<p>枚举就可以解决这个问题，以<code>C#</code>为例</p>
<pre><code class="language-csharp">enum STATUS
{
    EAT,
    SLEEP,
    HIT_DOUDOU
};
</code></pre>
<p>使用时<code>status == STATUS.EAT</code>，我们就明确知道是在判断状态等于吃饭</p>
<p>在JS中也可以实现，就是自己定义变量的方式</p>
<pre><code class="language-javascript">const STATUS = Object.freeze({
  EAT: 0,
  SLEEP: 1,
  HIT_DOUDOU: 2
})
</code></pre>
<p>其中<code>freeze</code>可以让这个对象不会被更改</p>
<h2>位</h2>
<p>位就是可以进行位运算（我的理解），比如<code>C#</code>可以用<code>Flags</code>将枚举标记为位域</p>
<pre><code class="language-csharp">[Flags]
enum STATUS
{
    EAT = 1 &lt;&lt; 0,
    SLEEP = 1 &lt;&lt; 1,
    HIT_DOUDOU = 1 &lt;&lt; 2
}
</code></pre>
<p>此时你可以这样判断<code>status.HasFlag(STATUS.EAT)</code></p>
<p>但当判断项只有一个可能看不出它的优势，别急我们先用JS演示一下</p>
<h1>JS模仿C#位枚举</h1>
<h2>定义</h2>
<p>定义方式与<code>C#</code>一毛一样</p>
<pre><code class="language-javascript">const STATUS = Object.freeze({
  EAT: 1 &lt;&lt; 0,
  SLEEP: 1 &lt;&lt; 1,
  HIT_DOUDOU: 1 &lt;&lt; 2
})
</code></pre>
<p>也可以直接设置为数字的 1、2、4，左移只是更易读</p>
<h2>封装一个 HasFlag</h2>
<pre><code class="language-javascript">function hasFlag(enums, flags) {
  return flags === (enums &amp; flags)
}
</code></pre>
<p>它的原理就是让<strong>枚举</strong>和<strong>枚举变量</strong>与(&amp;)<strong>枚举</strong>对比</p>
<p>至于这种封装方式，我也不知道对不对，应该有更好的方法</p>
<h2>发生了什么</h2>
<p>假设我们有一个状态</p>
<pre><code class="language-javascript">const status = STATUS.EAT
</code></pre>
<p>使用<code>hasFlag</code></p>
<pre><code class="language-javascript">hasFlag(status, STATUS.EAT)
</code></pre>
<p>等号左边的二进制是<code>0000 0001</code></p>
<p>等号右边的二进制进行了与运算</p>
<pre><code>0000 0001
&amp;
0000 0001
=
0000 0001
</code></pre>
<p>最终结果是两边都是<code>0000 0001</code>，所以等式成立返回<code>true</code></p>
<h2>更令人舒适的</h2>
<p>单看上面的方式，你完全可以用<code>status === STATUS.EAT</code></p>
<p>但位枚举可以同时表述<strong>一种以上</strong>的状态，比如边吃饭边打豆豆</p>
<p>只需要加个或</p>
<pre><code class="language-javascript">const status = STATUS.EAT | STATUS.HIT_DOUDOU
</code></pre>
<p>再使用<code>hasFlag</code>验证一下</p>
<pre><code class="language-javascript">hasFlag(status, STATUS.EAT)
hasFlag(status, STATUS.HIT_DOUDOU)
hasFlag(status, STATUS.EAT | STATUS.HIT_DOUDOU)
</code></pre>
<p>以上均为<code>true</code></p>
<h2>状态的切换</h2>
<p>位枚举切换状态非常简单，假设我正在边吃饭边打豆豆</p>
<pre><code class="language-javascript">const status = STATUS.EAT | STATUS.HIT_DOUDOU
// 0000 0101
</code></pre>
<p>现在我要移除吃饭状态，添加睡觉状态，我只需要一个异或</p>
<!-- eslint-disable no-global-assign -->
<pre><code class="language-javascript">status ^= STATUS.EAT | STATUS.SLEEP
</code></pre>
<p>异或就是相同为<code>0</code>，不同为<code>1</code>，所以就去掉了吃饭，添加了睡觉</p>
<pre><code>0000 0101
^
0000 0011
=
0000 0110
</code></pre>
<p>（在梦里打豆豆）</p>
<h2>除了某种状态</h2>
<p>不存在某种状态时返回<code>true</code>，但写法得倒过来，条件状态在前</p>
<pre><code class="language-javascript">hasFlag(~STATUS.EAT, status) // 除了吃饭
hasFlag(~STATUS.EAT &amp; ~STATUS.SLEEP, status) // 除了吃饭和睡觉
</code></pre>
<h1>告辞</h1>
<p>目前我在<code>javascript</code>中用位枚举还比较少，只用过一次，不过我感觉这种方式能解决不少判断的问题</p>
<p>而且二进制，多少带点快，干净又卫生</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[JSDoc 让JS有类型约束]]></title>
            <link>https://imba97.com/posts/746</link>
            <guid isPermaLink="false">https://imba97.com/posts/746</guid>
            <pubDate>Fri, 10 Sep 2021 20:45:13 GMT</pubDate>
            <content:encoded><![CDATA[<p>这次我们来谈谈<code>JSDoc</code>，首先这是个啥，本质就是个注释，可以帮助开发工具认识你的代码。你可以通过它提升开发效率、降低运行时的错误率，更好的理解强类型语言。</p>
<p>主流编辑器应该都支持<code>JSDoc</code>，但我基本没用过别的所以不太清楚，本文章中演示用的编辑器均为<code>vscode</code></p>
<!--more-->
<h1>JS中的类型</h1>
<p>在<code>js</code>中，虽然类型的概念没那么强，但你依然无时无刻不在受到类型约束</p>
<p>比如最简单的<code>document.querySelector</code></p>
<p><img src="https://imba97.com/uploads/2021/10/jsdoc-1.png" alt=""></p>
<p>它会返回一个<code>Element</code>类型的数据，页面元素的基础类型<code>HTMLElement</code>就继承了该类型</p>
<p>但这时如果你想修改<code>style</code>，你会发现它并不会给你提示，因为<code>style</code>是<code>HTMLElement</code>的属性</p>
<p><img src="/uploads/2021/10/jsdoc-2.png" alt=""></p>
<h1>JS Doc</h1>
<p>这时候，<code>JSDoc</code>就派上用场了。在你明确知道它类型的情况下，你可以直接声明变量的类型</p>
<pre><code class="language-typescript">/**
 * @type {HTMLElement}
 */
const test = document.querySelector('.test')
</code></pre>
<p>此时再看代码提示，就出现了</p>
<p><img src="/uploads/2021/10/jsdoc-3.png" alt=""></p>
<p>并且你可以开启类型检查</p>
<pre><code class="language-typescript">// @ts-check
/**
 * @type {HTMLElement}
 */
const test = document.querySelector('.test')
</code></pre>
<p>此时如果你写的代码不规范，语法检查就会报错</p>
<p><img src="/uploads/2021/10/jsdoc-4.png" alt=""></p>
<p>这样就可以在运行前排除掉大部分因为粗心导致的问题</p>
<h1>常用字段</h1>
<h2>@type</h2>
<p>声明变量类型，类型写在<code>{}</code>中</p>
<pre><code class="language-typescript">/**
 * @type {string}
 */
const str = 'string'
</code></pre>
<p>对象类型不能省略<code>{}</code></p>
<pre><code class="language-typescript">/**
 * @type {{
 *  name: string
 *  age: number
 *  info: {
 *    id: number
 *  }
 * }}
 */
const test = {}
</code></pre>
<p><img src="/uploads/2021/10/jsdoc-5.png" alt=""></p>
<h2>@typedef</h2>
<p>声明一个自定义类型，配合<code>@type</code>使用，你可以在类型后面起个类型名称</p>
<pre><code class="language-typescript">/**
 * @typedef {{
 *  status: number
 *  message: string
 *  data: {[key: string]: any}
 * }} ResultData
 */

/**
 * @type {ResultData}
 */
const test = {}
</code></pre>
<p><img src="/uploads/2021/10/jsdoc-6.png" alt=""></p>
<h2>@param</h2>
<p>声明函数形参</p>
<pre><code class="language-typescript">/**
 * 加法
 * @param {number} n1
 * @param {number} n2
 */
function sum(n1, n2) {
  return n1 + n2
}
</code></pre>
<p><img src="/uploads/2021/10/jsdoc-7.png" alt=""></p>
<h2>@template</h2>
<p>声明一个泛型，泛型相当于是一个类型的变量，我们最常用的数组，就用到了泛型<code>Array&lt;T&gt;</code></p>
<p>如果定义<code>Array&lt;string&gt;</code>，说明这是个<code>string</code>的数组</p>
<p>这样你就可以有更好的语法提示</p>
<p><img src="/uploads/2021/10/jsdoc-8.png" alt=""></p>
<p>比如我们可以封装一个简单的<code>findIndex</code>，类似<code>lodash</code>的一个函数，查找值的下标</p>
<pre><code class="language-typescript">/**
 * @template T
 * @param {T[]} arr
 * @param {T} target
 * @return {number}
 */
function findIndex(arr, target) {
  let index = -1
  for (let i = 0; i &lt; arr.length; i++) {
    let hasTarget = true
    const targetKeys = Object.keys(target)
    for (let j = 0; j &lt; targetKeys.length; j++) {
      // 如果没值 || 不相等
      if (
        typeof arr[i][targetKeys[j]] === 'undefined'
        || arr[i][targetKeys[j]] !== target[targetKeys[j]]
      ) {
        hasTarget = false
        break
      }
    }
    // 如果 index 没值 &amp;&amp; 有匹配的 target 则设置 index
    if (index === -1 &amp;&amp; hasTarget) {
      index = i
      break
    }
  }
  return index
}

/**
 * @type {{
 *  name?: string
 *  age?: number
 * }[]}
 */
const test = [
  { name: '张三', age: 18 },
  { name: '李四', age: 20 },
  { name: '王五', age: 22 }
]

console.log(
  findIndex(test, {
    name: '王五'
  }),

  findIndex(test, {
    age: 20
  })
)
</code></pre>
<p><img src="/uploads/2021/10/jsdoc-9.png" alt=""></p>
<p>此时如果类型与传递的数组不相符，则会报错</p>
<p><img src="/uploads/2021/10/jsdoc-10.png" alt=""></p>
<p>其实这个例子举的不太好，因为那个函数本身只能传一个对象数组</p>
<p>但<code>js</code>也确实不好举例子，这个泛型跟<code>ts</code>比还是有缺陷的，它没法单独传类型参数，只能通过形参传递</p>
<p>如果是<code>ts</code>你可以封装一个请求类，对请求参数进行约束：<a href="/posts/738#TypeScript">《请求接口类的封装》</a></p>
<h2>其他字段</h2>
<p>还有一些不太常用的（我个人不常用），可以参考<a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html">官方文档</a>。</p>
<h1>结语</h1>
<p>OK，这次就说到这，这篇文章憋了20天</p>
<p>我太懒了，而且有些都是现研究的，之前研究这个泛型，一直想实现一个封装请求函数，自动提示返回参数，但实际比较难实现，因为就是上面说的<code>JSDoc</code>没法传递类型参数</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[杭州电动车上牌记录]]></title>
            <link>https://imba97.com/posts/745</link>
            <guid isPermaLink="false">https://imba97.com/posts/745</guid>
            <pubDate>Wed, 18 Aug 2021 21:42:24 GMT</pubDate>
            <content:encoded><![CDATA[<p>经历了一次非常坎坷的电动车上牌之路，记录下来造福社会。</p>
<!--more-->
<p>考虑到不想看废话的人，首先说结论：</p>
<p>上牌流程</p>
<ul>
<li>支付宝搜索“杭州交警”</li>
<li>点击左下角“预约及违法处理” → “车管所预约” 在线申请</li>
</ul>
<p>需要带的资料</p>
<ul>
<li>身份证</li>
<li>电动车合格证</li>
<li>电动车发票</li>
<li>（根据地区可能还有其他的，上牌前看一下当地的注意事项）</li>
</ul>
<p><strong>重要：杭州部分上牌点，要求发票上需要备注电动车型号，如果发票上有其他信息必须与合格证上的一致，比如公司名称不能是简称</strong></p>
<h1>正片开始</h1>
<p>网上买了个小电动车，2021-08-04 到的，中间经历了艰辛的上牌之路，三周才成功上牌</p>
<p>工作日上班，所以只能周末去上牌，离家近的上牌点都约不上，只能打车带着车去远的上牌点</p>
<h2>Round 1</h2>
<p>头一回不知道如何上牌，网上搜的是到车管所或者交警大队，于是我搜了搜发现家附近有个车管所，离我公司挺近，于是找了个中午去那边看了看。去看了发现好像跟电动车上牌没太大关系，那边都是考驾照的</p>
<p>隔天我又给交警大队打了个电话，警察叔叔让我在支付宝搜“杭州交警”，可以直接在里面预约</p>
<p>就这样我预约了周末上牌，到了后没怎么等，递交资料也没怎么等，直接跟我说你这发票上<strong>没有备注</strong>车辆型号给我打回了</p>
<p>我问卖家他们说财务不上班改不了发票，语气是真的硬</p>
<p>没办法我就只能回家了</p>
<h2>Round 2</h2>
<p>转过头又是一个周末，工作日让他们改了发票等到周末又去上牌，轻车熟路</p>
<p>扫码签到、递交资料、排队等叫号，一顿操作猛如虎</p>
<p>结果是发票备注中有一段话写的公司名称不对，并且车辆型号是错的。发票备注中出现公司名称的那段是他们自己给加的，我以为是什么必须要用到的。车辆型号是我发错了个字母，他们的所有车辆型号都是TD开头，我错发成了TZ。虽然我认为有极大概率是因为之前客服跟我吵过，故意没告知我（因为他们不太可能不知道自己公司所有这类电动车都是TD开头），但毕竟是我自己写错了，没法说啥</p>
<p>不过即使我没写错，公司名称也是有问题的，依然过不了</p>
<p>没办法我就只能回家了</p>
<h2>Round 3</h2>
<p>转过头又是一个周末，合着我就是个猫头鹰（头转了360°），这次工作日的时候我预约了另一个地方，是一个交警队</p>
<p>流程相同，专业对口，警察叔叔还惊讶地问我为什么知道扫这个码签到</p>
<p>这次一切顺利，效率及高，当场就能拿到牌</p>
<p>期间说到我前两次的上牌经历，我也特意问了一下发票备注加车辆型号的事，警察叔叔说没听过要加型号的事。我给他看了看上次上牌的地点，他说那个地方可能比较严格</p>
<p>最后有一点小问题，车上挂牌照的螺丝孔太小（这点后面总结里吐槽）</p>
<h1>总结</h1>
<ol>
<li>
<p>上牌前如果可以最好确认一下上牌点有什么具体要求，没法确认的最好准备齐全点，比如在发票备注写写主要信息。以及对照合格证，不要有简写、写错</p>
</li>
<li>
<p>上牌最好去交警队，别的上牌点可能要求严格，我前两次去的上牌点，除了我有问题，在场的我还听到有人电池有问题、发票有问题</p>
</li>
<li>
<p>在一个有上牌经验的店买车。我认为让我经历这么一大圈的主要责任是卖家对上牌不了解</p>
<ul>
<li>
<p>原因一，虽然他们店有注明不同地区上牌要求不同，但一些基本的比如发票上写上车辆型号，我感觉有些地方会通用，这种写上即使用不到也不会有问题的小事，他们并没做。我说他们他们的理由是我没有要求写上，责任成了我的了</p>
</li>
<li>
<p>原因二，发票备注里的公司名称没有用全称，跟合格证的不一样，也是被打回的主要原因之一。可能是我不懂发票，但我感觉只要出现公司名称，就应该用全称吧。这点不做过多评价</p>
</li>
<li>
<p>原因三，螺丝孔大小的问题。这个问题不影响上牌，但体现了一种专业性。去上牌我知道了一个小知识，挂牌照的螺丝必须统一用交警队发的螺丝，不用被抓会被罚。他们一个卖电动车的却不知道，以及发票的一些事，我很难不怀疑他们的专业性。最后是我跑到一个电动车店，专门把孔打大</p>
</li>
</ul>
</li>
</ol>
<p>OK，就这样吧，告辞</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[TypeScript 浏览器插件开发]]></title>
            <link>https://imba97.com/posts/744</link>
            <guid isPermaLink="false">https://imba97.com/posts/744</guid>
            <pubDate>Tue, 06 Jul 2021 20:40:50 GMT</pubDate>
            <content:encoded><![CDATA[<p>总结一篇 TypeScript 浏览器插件开发经验</p>
<h1>推荐模块</h1>
<ol>
<li>
<p><a href="https://github.com/Lusito/webextension-polyfill-ts">webextension-polyfill-ts</a></p>
<p>浏览器插件API的TS包，开发插件必备</p>
<!--more-->
<pre><code class="language-javascript">import { browser } from 'webextension-polyfill-ts'
</code></pre>
<p>**避坑：**封装网络请求用<code>browser.runtime.onMessage.addListener</code>不能直接返回<code>axios</code>，虽然TS不会报错，但运行结果会是<code>undefined</code></p>
<p>正确方法：</p>
<pre><code class="language-javascript">browser.runtime.onMessage.addListener((request) =&gt; {
  const params
    = request.type === 'GET' ? { params: request.params } : { data: request.data }

  return new Promise((resolve, reject) =&gt; {
    axios({
      method: request.type,
      url: request.url,
      ...params,
      headers: request.headers || {}
    }).then((response) =&gt; {
      resolve(response.data)
    })
  })
})
</code></pre>
</li>
<li>
<p><a href="https://www.npmjs.com/package/write-json-webpack-plugin">write-json-webpack-plugin</a></p>
<p>操作<code>json</code>文件的插件，用于修改<code>manifest.json</code></p>
<pre><code class="language-javascript">// webpack.config.js
const WriteJsonWebpackPlugin = require('write-json-webpack-plugin')
// ...
module.exports = () =&gt; {
  const manifestJSON = require('./src/manifest.json')
  // 版本号
  manifestJSON.version = '2.0.0'

  const config = {
    plugins: [
      manifestJSON
      &amp;&amp; new WriteJsonWebpackPlugin({
        pretty: false,
        object: manifestJSON,
        path: '/',
        filename: 'manifest.json'
      })
    ]
  }
  return config
}
</code></pre>
</li>
<li>
<p><a href="https://www.npmjs.com/package/webpack-extension-reloader">webpack-extension-reloader</a></p>
<p>自动重新加载插件，自动刷新插件作用的网页，测试神器</p>
<pre><code class="language-javascript">const ExtensionReloader = require('webpack-extension-reloader')
// 也是 webpack plugins 以下就简单一写了
const config = {
  entry: {
    content: './src/content.ts',
    background: './src/background/background.ts',
    popup: './src/popup/popup.ts',
    options: './src/options/options.ts'
  },
  // ...
  plugins: [
    new ExtensionReloader({
      reloadPage: true,
      // entry
      entries: {
        contentScript: 'content',
        background: 'background',
        extensionPage: ['popup', 'options']
      }
    })
  ]
}
</code></pre>
</li>
</ol>
<h1>Btools</h1>
<p>最后宣传一下自己写的插件，Btools，以B站为主的网站页面优化，增强用户体验</p>
<p>现阶段正进行重构，使用<code>TypeScript</code>+<code>Vue</code>，欢迎参考：<a href="https://github.com/imba97/Btools-vue">Github</a>、<a href="https://gitee.com/imba97/Btools-vue">Gitee</a></p>
<h2>对流程的封装</h2>
<p>Btools 分为两大模块：<strong>Linstener 模块</strong> 和 <strong>Watcher 模块</strong></p>
<ul>
<li>
<p>Linstener 模块</p>
<p>用于监听来自<code>background-js</code>的反馈，<code>background-js</code>会监听网页中指定的请求</p>
<p>比如我需要获取页面上的评论，以前的做法是傻傻的用计时器循环获取页面元素，现在先通过<code>background-js</code>监听，如果监听到获取评论的API请求会发消息给<code>content-js</code>，接到消息后调用相应的模块</p>
<p>这样做的好处是虽然也会用到计时器循环获取页面元素，但至少知道页面元素马上会加载好，一定程度上避免不必要的消耗</p>
</li>
<li>
<p>Watcher 模块</p>
<p>根据不同的网址进行不同的操作，举个例子：</p>
<pre><code class="language-typescript">import { HandleOptions, WatcherBase } from '@/Watcher/WatcherBase'
export class GetPicWatcher extends WatcherBase {
  // 初始化函数中设置网址
  protected init(): void {
    this.urls[GetPicEnum.Video] = /bilibili\.com\/video/
    this.urls[GetPicEnum.Bangumi] = /bilibili\.com\/bangumi/
    this.urls[GetPicEnum.Watchlater] = /bilibili\.com\/medialist\/play\/watchlater/
    this.urls[GetPicEnum.Read] = /bilibili\.com\/read/
    this.urls[GetPicEnum.LiveRoom] = /live\.bilibili\.com/
  }

  // 处理函数
  protected handle(options: HandleOptions): void {
    // 父级调用时会把 GetPicEnum 的值传回来，这里就可以区分是哪个网址
    switch (options.index) {
      case GetPicEnum.Video:
        this.video()
        break
      case GetPicEnum.Bangumi:
        this.bangumi()
        break
      case GetPicEnum.Watchlater:
        this.watchlater()
        break

      case GetPicEnum.Read:
        this.read()
        break
      case GetPicEnum.LiveRoom:
        this.liveRoom()
        break
    }
  }

  video() {
    // ...
  }

  bangumi() {
    // ...
  }

  watchlater() {
    // ...
  }

  read() {
    // ...
  }

  liveRoom() {
    // ...
  }
}

// 用于区分网址
enum GetPicEnum {
  /**
   * 视频页面
   */
  Video,

  /**
   * 番剧、电影
   */
  Bangumi,

  /**
   * 稍后再看
   */
  Watchlater,

  /**
   * 专栏
   */
  Read,

  /**
   * 直播间
   */
  LiveRoom
}
</code></pre>
</li>
</ul>
<h2>对本地存储的封装</h2>
<p>Btools 有着非常完善的本地存储封装，不同模块之间不会互相干扰</p>
<p>首先有一个模板基类</p>
<pre><code class="language-typescript">export default class TemplateBase {
  private _name: string

  protected _data: Record&lt;string, any&gt;

  public constructor(data: Record&lt;string, any&gt;) {
    this._data = data
    this._name = (this as any).__proto__.constructor.name
  }

  public GetName(): string {
    return this._name
  }

  public GetData(): Record&lt;string, any&gt; {
    return this._data
  }

  public SetData(data: Record&lt;string, any&gt;): void {
    this._data = data
  }
}
</code></pre>
<p>这里的<code>_name</code>非常关键，其他模板类继承后被实例化事，这个<code>_name</code>就会变成子类类名，这就是不同模块之间不会互相干扰的关键</p>
<p>然后看一下某一个子类</p>
<pre><code class="language-typescript">/**
 * 找回失效视频存储模板
 */

import TemplateBase from '@base/storage/template/TemplateBase'

export interface IVideoInfo {
  /**
   * 视频标题
   */

  title: string

  /**
   * 视频图片链接
   */
  pic: string
}

export interface IRetrieveInvalidVideo {
  videoInfo: { [key: string]: IVideoInfo }
}

// 这里继承了基类
export class TRetrieveInvalidVideo extends TemplateBase {
  constructor(data: IRetrieveInvalidVideo) {
    super(data)
  }
}
</code></pre>
<p>在构造函数中会把一个类型为<code>IRetrieveInvalidVideo</code>的参数传进去</p>
<p>然后是对本地存储的读写封装</p>
<pre><code class="language-typescript">/**
 * 扩展本地存储
 */

import _ from 'lodash'

import { browser } from 'webextension-polyfill-ts'

import Singleton from '@/scripts/base/singletonBase/Singleton'
import TemplateBase from '@/scripts/base/storage/template/TemplateBase'

export default class ExtStorage extends Singleton {
  // ...
  getStorage&lt;T extends TemplateBase, TResult&gt;(
    configs: T
  ): Promise&lt;TResult&gt; {
    return new Promise((resolve) =&gt; {
      const space = new Object()
      space[configs.GetName()] = configs.GetData()

      browser.storage.local.get(space).then((items) =&gt; {
        resolve({
          ...configs.GetData(),
          ...items[configs.GetName()]
        })
      })
    })
  }

  setStorage&lt;T extends TemplateBase, TResult&gt;(
    configs: T
  ): Promise&lt;TResult&gt; {
    return new Promise((resolve) =&gt; {
      const space = {}
      space[configs.GetName()] = configs.GetData()

      browser.storage.local.set(space).then(() =&gt; {
        resolve(configs.GetData() as TResult)
      })
    })
  }
}
</code></pre>
<p>传进来的<code>config</code>类型是<code>TemplateBase</code>，这个类有个方法是<code>GetName</code>，也就是获取类名</p>
<p>存取时<code>space[configs.GetName()] = configs.GetData()</code>在外面包一层类名</p>
<p>这样即使是有重名，也是OK的</p>
<p>使用时是这样的：</p>
<pre><code class="language-typescript">import {
  IRetrieveInvalidVideo,
  IVideoInfo,
  TRetrieveInvalidVideo
} from '@/scripts/base/storage/template'

export class RetrieveInvalidVideo extends ModuleBase {
  protected handle() {
    // 获取本地数据
    const localData = ExtStorage.Instance().getStorage&lt;
      TRetrieveInvalidVideo,
      IRetrieveInvalidVideo
    &gt;(
      new TRetrieveInvalidVideo({
        videoInfo: {}
      })
    )
  }
}
</code></pre>
<p>**避坑：**谷歌插件中，获取时传入一个默认值，如果没有读取到则会返回默认值。但火狐不会自动返回默认值，所以需要自己处理</p>
<p>也就是<code>扩展本地存储</code>代码中的<code>23</code>、<code>24</code>行，先解构传过来的<code>configs</code>，再解构读取到的<code>items</code></p>
<p>比如我<code>configs</code>传入的<code>{ a: [], b: [] }</code>，但本地存储里只有<code>{ a: [1,2,3] }</code></p>
<p>那么先解构<code>configs</code>获得<code>{ a: [], b: [] }</code>，再解构<code>items</code>会把<code>a</code>替换掉，获得<code>{ a: [1,2,3], b: [] }</code></p>
<p>相当于自己封装了返回默认值</p>
<h1>收工</h1>
<p>OK，以上就是一些小总结，告辞</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Docker Nodejs 排坑笔记]]></title>
            <link>https://imba97.com/posts/743</link>
            <guid isPermaLink="false">https://imba97.com/posts/743</guid>
            <pubDate>Thu, 20 May 2021 22:54:47 GMT</pubDate>
            <content:encoded><![CDATA[<h1>原因</h1>
<p>之前做了个B站机器人，可以以图搜番，这个功能需要用到 <code>ffmpeg</code> 将视频转成动图</p>
<p>如果想给别人用那么别人配置环境会很麻烦，就这样顺理成章的想到了 <code>Docker</code></p>
<!--more-->
<h1>排坑</h1>
<p>初学 <code>Docker</code> 遇到不少坑</p>
<h2>在Dockfile执行node</h2>
<p>在这里卡了一天，我一直在搜“Dockerfile 容器 执行命令”</p>
<p>最后发现我傻了，我为什么不找个 <code>docker+node</code> 项目看看呢，官方都有说明</p>
<h2>Dockerfile 环境变量传入js中</h2>
<p>在Github搜了搜项目发现，在js文件中可以直接用 <code>process.env.XXX</code> 获取环境变量</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[程序猿复健计划之B站机器人]]></title>
            <link>https://imba97.com/posts/742</link>
            <guid isPermaLink="false">https://imba97.com/posts/742</guid>
            <pubDate>Wed, 19 May 2021 23:20:59 GMT</pubDate>
            <content:encoded><![CDATA[<h1>功能</h1>
<ol>
<li>
<p>梗百科，查询梗的意思</p>
<img src="https://imba97.com/uploads/2021/05/bili-bot-1.jpg" style="zoom:50%;" />
</li>
</ol>
<hr>
<ol start="2">
<li>
<p>以图搜番，跟微信的区别是，它最后会发送动图，因为不能发视频</p>
<img src="/uploads/2021/05/bili-bot-2.jpg" style="zoom:50%;" />
</li>
</ol>
<hr>
<p>你可以使用宝塔Docker管理器轻松配置开启</p>
<!--more-->
<h1>教程</h1>
<ol>
<li>
<p>搜索镜像：imba97/bili-bot:1.0.0</p>
<p><img src="/uploads/2021/07/bili-bot-3.png" alt=""></p>
</li>
<li>
<p>创建容器</p>
<p><img src="/uploads/2021/07/bili-bot-4.png" alt=""></p>
</li>
<li>
<p>选择镜像，点提交</p>
<p><img src="/uploads/2021/07/bili-bot-5.png" alt=""></p>
</li>
<li>
<p>点击日志获取登录二维码</p>
<p><img src="/uploads/2021/07/bili-bot-6.png" alt=""></p>
</li>
<li>
<p>扫码登录（登录后这个页面不会有提示，用的Wechaty的二维码生成页）</p>
<p><img src="/uploads/2021/07/bili-bot-7.png" alt=""></p>
</li>
<li>
<p>重新打开日志会提示登陆成功</p>
<p><img src="/uploads/2021/07/bili-bot-8.png" alt=""></p>
</li>
</ol>
<p>以上</p>
<hr>
<p>如果不想用docker，也可以下载源码直接拿Node.js跑</p>
<p>但你还需要手动在电脑上装个<code>ffmpeg</code>，以图搜番会用它把视频转成gif</p>
<p><a href="https://github.com/imba97/imba97_project/tree/master/bili_bot">Github</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[程序猿复健计划之微信机器人]]></title>
            <link>https://imba97.com/posts/741</link>
            <guid isPermaLink="false">https://imba97.com/posts/741</guid>
            <pubDate>Tue, 18 May 2021 08:49:43 GMT</pubDate>
            <content:encoded><![CDATA[<h1>起因</h1>
<p>朋友玩游戏分享到群里领游戏奖励，领完撤回，于是...</p>
<img src="https://imba97.com/uploads/2021/05/wechaty-1.jpg" style="zoom:50%;" />
<img src="/uploads/2021/05/wechaty-2.jpg" style="zoom: 50%;" />
<img src="/uploads/2021/05/wechaty-3.jpg" style="zoom: 50%;" />
<img src="/uploads/2021/05/wechaty-4.jpg" style="zoom: 50%;" />
<img src="/uploads/2021/05/wechaty-5.jpg" style="zoom: 50%;" />
<h1>功能</h1>
<p><code>node.js</code>太上头了，除了上面那个之外又做了点别的</p>
<!--more-->
<hr>
<ol>
<li>
<p>梗百科，可以查询梗的意思</p>
<img src="/uploads/2021/05/wechaty-6.jpg" style="zoom: 50%;" />
</li>
</ol>
<hr>
<ol start="2">
<li>
<p>以图搜番，根据图片搜番剧，日漫为主，使用 <a href="https://trace.moe">trace.moe</a> 的接口</p>
<img src="/uploads/2021/05/wechaty-7.jpg" style="zoom: 50%;" />
</li>
</ol>
<hr>
<ol start="3">
<li>
<p>黑白图上色，发送黑白老照片自动上色，百度的接口</p>
<img src="/uploads/2021/05/wechaty-8.jpg" style="zoom: 50%;" />
</li>
</ol>
<hr>
<ol start="4">
<li>
<p>基金查询，根据基金代码查询基金半年内单位净值，接口：<a href="https://www.doctorxiong.club/api/#api-Fund-getFundDetail">小熊同学 - 基金接口</a></p>
<img src="/uploads/2021/05/wechaty-9.jpg" style="zoom: 50%;" />
</li>
</ol>
<p>数据会转成图片发回给微信</p>
<img src="/uploads/2021/05/wechaty-10.jpg" style="zoom: 50%;" />
<h1>使用</h1>
<p>详情请见：<a href="https://github.com/imba97/imba97_project/tree/master/wechat_bot">Github</a></p>
<h1>程序猿复健计划</h1>
<p>解释一下，我二月份做了个近视手术，三个月内不能长时间看手机电脑这种需要近距离长时间看的东西</p>
<p>三月之期已到，码农，出山 ✔</p>
<p>不过目前还是得看会休息会才行</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[巴中市如何开具《动物检疫合格证明》]]></title>
            <link>https://imba97.com/posts/739</link>
            <guid isPermaLink="false">https://imba97.com/posts/739</guid>
            <pubDate>Thu, 15 Apr 2021 16:51:23 GMT</pubDate>
            <content:encoded><![CDATA[<p>打算近期回家，因为要带主子坐飞机，所以得到<strong>动物卫生监督所</strong>去开《动物检疫合格证明》</p>
<p>因为开证明的地方比较隐蔽，特此记录避雷</p>
<h1>开证须知</h1>
<ul>
<li>宠物要至少提前一个月打疫苗，拿到<strong>疫苗注射证明</strong></li>
<li>动物检疫合格证明有时效性，一周左右（具体问一下工作人员）</li>
</ul>
<h1>初步尝试</h1>
<p>提前了20天左右去的，想先去找找位置、问问情况</p>
<p>在地图搜动物卫生监督所，只能搜到一个地方。这个地方挺难找，到了之后找了半天才找到相关的办公室</p>
<p><img src="https://imba97.com/uploads/2021/04/bazhong-1.jpg" alt=""></p>
<p>当时是周六，碰到一位大叔说今天不上班，并被告知动物检疫合格证明<strong>不是在这里开</strong>，这位大叔还热心的给我指路</p>
<p>我就又去他说的那个地方看了看，转了半天并没找到动物检疫之类的部门</p>
<h1>梅开二度</h1>
<p>于是今天，我趁工作日又去了一趟巴中市动物卫生监督所，说明情况后工作人员给我一个电话，还耐心的用我听不太懂的四川话告诉我位置</p>
<p><img src="/uploads/2021/04/bazhong-2.jpg" alt=""></p>
<p>到这里后我又打电话询问具体位置</p>
<p>首先找到<strong>草坝街</strong>（绿点位置）</p>
<p><img src="/uploads/2021/04/bazhong-3.jpg" alt=""></p>
<p>长这样</p>
<p><img src="/uploads/2021/04/bazhong-4.jpg" alt=""></p>
<p>顺着草坝街往前走，遇到一个人物雕像再往前走，走到一个<strong>石头卷轴</strong>，然后右拐拐进一个过道</p>
<p>在<strong>过道里面</strong>有个牌子写着<strong>长青宾馆</strong>（大约在红点位置），跟宾馆同一栋楼4楼就是了</p>
<p>出电梯后就能看到牌子</p>
<p><img src="/uploads/2021/04/bazhong-5.jpg" alt=""></p>
<p>右转后有个楼道口，进去就能看到门了</p>
<p><img src="/uploads/2021/04/bazhong-6.jpg" alt=""></p>
<hr>
<p>本文章已发布 <span start-time="2021/04/15 18:43:11" interval-time="50" id="time"></span><script>if(typeof dateMaker !== 'undefined') dateMaker('time');</script>，可能会由于时间的变化，文章中的参照物消失或检疫站地址变更</p>
<p><strong>如果时间太长，本文章仅供参考</strong></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[请求接口类的封装]]></title>
            <link>https://imba97.com/posts/738</link>
            <guid isPermaLink="false">https://imba97.com/posts/738</guid>
            <pubDate>Tue, 19 Jan 2021 00:33:46 GMT</pubDate>
            <content:encoded><![CDATA[<h1>简述</h1>
<p>最开始知道这个方法是在公司一位大佬的代码里看到的，语言是<code>TypeScript</code>，当时感觉这个写法逻辑非常清晰，阅读极度舒适</p>
<p>后来又根据自己的需要加了参数类型约束，用在了一个浏览器插件项目中：<a href="https://github.com/imba97/Btools-vue/blob/d13bc99a72866bc6dd604bfb5097e2e52bb01ec2/src/scripts/base/Url.ts">Btools-vue</a>（但在后来的一次<a href="https://github.com/imba97/Btools-vue/commit/51785e9de75019057e217f18cec2e0ab7c0c942d">重构</a>中删掉了）</p>
<p>并且也在目前在做的<code>Unity</code>游戏中，用<code>C#</code>写了一个类似的，用起来也是非常舒适</p>
<!--more-->
<h1>使用</h1>
<p>先演示一下使用方式</p>
<h2>TypeScript</h2>
<p>Url 类</p>
<!-- eslint-disable import/no-named-default -->
<pre><code class="language-typescript">import { RequestOptions } from 'node:https'
import { ParsedUrlQueryInput, default as qs } from 'node:querystring'
import { AxiosRequestConfig } from 'axios'
import Vue from 'vue'

/**
 * URL 类型
 */
export enum UrlType {
  BILIBILI,
  IMBA97
}

/**
 * 请求类型
 */
export enum MethodType {
  GET,
  POST
}

export class Url&lt;T extends ParsedUrlQueryInput&gt; {
  private _bilibili_base_url = 'https://api.bilibili.com'

  private _imba97_base_rul = 'https://bili.imba97.com'

  public static readonly enums: Url&lt;any&gt;[] = []

  public static readonly headers: { [key: string]: { [key: string]: string } } = {}

  // ========= BILIBILI =========

  // 获取用户卡片信息
  public static readonly USER_CARD: Url&lt;{ mid: string }&gt; = new Url(MethodType.GET, UrlType.BILIBILI, '/x/web-interface/card', null)

  public static readonly LIKE: Url&lt;{ aid: number, like: number, csrf: string }&gt; = new Url(MethodType.POST, UrlType.BILIBILI, '/x/web-interface/archive/like', {
    Referer: 'https://www.bilibili.com',
    Origin: 'https://www.bilibili.com'
  })

  // ========= 测试 =========

  // 测试
  public static readonly POST_TEST: Url&lt;{ param: string | number }&gt; = new Url(MethodType.POST, UrlType.IMBA97, '/postTest.php', null)

  constructor(private _method: MethodType, private _type: UrlType, private _path: string, private _headers: any) {
    if (this._headers !== null)
      Url.headers[this.url] = this._headers
    Url.enums.push(this)
  }

  get baseUrl(): string {
    // 根据 type 获取 baseUrl
    switch (this._type) {
      case UrlType.BILIBILI:
        return this._bilibili_base_url

      case UrlType.IMBA97:
        return this._imba97_base_rul

      default:
        throw new Error('未曾设想的 URL 类型')
    }
  }

  get path(): string {
    return this._path
  }

  get url(): string {
    return `${this.baseUrl}${this._path}`
  }

  get method(): string {
    switch (this._method) {
      case MethodType.GET:
        return 'GET'
      case MethodType.POST:
        return 'POST'
      default:
        throw new Error('未曾设想的请求类型')
    }
  }

  public request(params?: T, options?: AxiosRequestConfig): Promise&lt;any&gt; {
    return new Promise((resolve, reject) =&gt; {
      Vue.chrome.runtime.sendMessage({
        type: this.method,
        url: this.url,
        ...(this._method === MethodType.GET ? { params } : { data: qs.stringify(params) }),
        headers: this._method === MethodType.GET ? { } : { 'content-type': 'application/x-www-form-urlencoded' },
        ...options
      }, (json) =&gt; {
        if (!json)
          reject(new Error('error'))
        resolve(json)
      })
    })
  }
}
</code></pre>
<p>声明一个新接口的格式是</p>
<p><code>public static readonly 名称: Url&lt;{ 发送参数名: 发送参数类型 }&gt; = new Url(请求类型, URL类型, 'URL路径', RequestHeaders)</code></p>
<p><code>request</code>函数是根据插件开发需要做了封装，普通网页的话直接在里面写请求代码就好</p>
<p>使用时</p>
<pre><code class="language-typescript">Url.USER_CARD.request({
  mid: '2198461'
}).then((json) =&gt; {
  console.log(json)
})
</code></pre>
<p>可以在这个类中声明所有请求用的URL，并且用泛型指定参数类型</p>
<p>用的时候会有代码提示，可以明确知道调用这个接口需要传哪些参数</p>
<p><img src="https://imba97.com/uploads/2021/01/url-1.png" alt=""></p>
<h2>C#</h2>
<p><code>C#</code>的文件比较多，一个一个来说</p>
<p>首先是跟上面类似的Url类</p>
<pre><code class="language-csharp">using System;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

/// &lt;summary&gt;
/// 请求类型
/// &lt;/summary&gt;
public enum MethodType
{
  GET,
  POST
}

/// &lt;summary&gt;
/// URL 类型
/// &lt;/summary&gt;
public enum UrlType
{
  /// &lt;summary&gt;
  /// 测试用接口
  /// &lt;/summary&gt;
  TEST,
  TEST2
}

public class Url
{
  /// &lt;summary&gt;
  /// POST_TEST 测试 POST 请求用
  /// &lt;/summary&gt;
  /// &lt;typeparam name=&quot;Param.POST_TEST&quot;&gt;参数类型约束&lt;/typeparam&gt;
  /// &lt;typeparam name=&quot;Responses.POST_TEST&quot;&gt;返回值类型约束&lt;/typeparam&gt;
  public static readonly Url&lt;Params.POST_TEST, Responses.POST_TEST&gt; POST_TEST = new Url&lt;Params.POST_TEST, Responses.POST_TEST&gt;(MethodType.POST, UrlType.TEST, &quot;/postTest.php&quot;);
  public static readonly Url&lt;Params.POST_TEST_COMPLEX, Responses.POST_TEST_COMPLEX&gt; POST_TEST_COMPLEX = new Url&lt;Params.POST_TEST_COMPLEX, Responses.POST_TEST_COMPLEX&gt;(MethodType.POST, UrlType.TEST, &quot;/postTest.php&quot;);
}

public class Url&lt;TParam, TResponse&gt; : Url where TParam : Params.ParamBase
{

  private MethodType _method;
  private UrlType _type;
  private string _path;

  public static readonly Dictionary&lt;string, Url&lt;TParam, TResponse&gt;&gt; enums = new Dictionary&lt;string, Url&lt;TParam, TResponse&gt;&gt;();

  public Url(MethodType method, UrlType type, string path)
  {
    this._method = method;
    this._type = type;
    this._path = path;
    enums.Add(typeof(TParam).ToString().Replace(&quot;Param.&quot;, &quot;&quot;), this);
  }

  public string GetUrl()
  {
    return string.Format(&quot;{0}{1}&quot;, this.GetBaseUrl(), this._path);
  }

  public void Request(TParam data, UnityAction&lt;TResponse&gt; success, UnityAction&lt;string&gt; error)
  {
    RequestManager.Instance.Send&lt;TResponse&gt;(this.GetMethod(), this.GetUrl(), this.ToRequestParams(data), success, error);
  }

  public void Request(TParam data, UnityAction&lt;TResponse&gt; success)
  {
    RequestManager.Instance.Send&lt;TResponse&gt;(this.GetMethod(), this.GetUrl(), this.ToRequestParams(data), success);
  }

  private string GetBaseUrl()
  {
    switch (this._type)
    {
      case UrlType.TEST:
        return &quot;https://bili.imba97.com&quot;;
      case UrlType.TEST2:
        return &quot;https://xxx&quot;;
      default:
        throw new Exception(&quot;未曾设想的 URL 类型&quot;);
    }
  }

  private string GetMethod()
  {
    switch (this._method)
    {
      case MethodType.GET:
        return &quot;GET&quot;;
      case MethodType.POST:
        return &quot;POST&quot;;
      default:
        throw new Exception(&quot;未曾设想的请求类型&quot;);
    }
  }

  /// &lt;summary&gt;
  /// 将参数类封装为请求参数字典
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;data&quot;&gt;参数类&lt;/param&gt;
  /// &lt;returns&gt;请求参数字典&lt;/returns&gt;
  private Dictionary&lt;string, string&gt; ToRequestParams(TParam data)
  {
    Dictionary&lt;string, string&gt; dict = new Dictionary&lt;string, string&gt;();

    // 获取参数类字段
    FieldInfo[] fields = typeof(TParam).GetFields();
    // 封装成字典
    foreach (FieldInfo field in fields)
    {
      dict.Add(field.Name, field.GetValue(data).ToString().ToLower());
    }

    return dict;
  }

}
</code></pre>
<p>声明一个新接口的格式是</p>
<p><code>public static readonly Url&lt;Params.参数类, Responses.返回值类&gt; 名称 = new Url&lt;Params.参数类, Responses.返回值类&gt;(请求类型, URL类型, &quot;URL路径&quot;);</code></p>
<p><code>Url&lt;TParam, TResponse&gt;</code>类继承<code>Url</code>类，接口的声明放在<code>Url</code>类中，这样调用的时候就不需要再设置泛型</p>
<p>参数传进来后通过<code>ToRequestParams()</code>转成一个字典，传入请求用的<code>RequestManager</code></p>
<p>下面是<code>RequestManager</code></p>
<pre><code class="language-csharp">using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.Events;
using Newtonsoft.Json;

public class RequestManager : SingletonBase&lt;RequestManager&gt;
{

  /// &lt;summary&gt;
  /// 发起网络请求
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;method&quot;&gt;请求类型&lt;/param&gt;
  /// &lt;param name=&quot;url&quot;&gt;链接&lt;/param&gt;
  /// &lt;param name=&quot;data&quot;&gt;参数&lt;/param&gt;
  /// &lt;param name=&quot;success&quot;&gt;成功的回调&lt;/param&gt;
  /// &lt;param name=&quot;error&quot;&gt;失败的回调&lt;/param&gt;
  public void Send&lt;TResponse&gt;(string method, string url, Dictionary&lt;string, string&gt; data, UnityAction&lt;TResponse&gt; success, UnityAction&lt;string&gt; error)
  {
    MonoManager.Instance.StartCoroutine(RSendRequest&lt;TResponse&gt;(method, url, data, success, error));
  }

  /// &lt;summary&gt;
  /// 发起网络请求（不设置失败回调）
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;method&quot;&gt;请求类型&lt;/param&gt;
  /// &lt;param name=&quot;url&quot;&gt;链接&lt;/param&gt;
  /// &lt;param name=&quot;data&quot;&gt;参数&lt;/param&gt;
  /// &lt;param name=&quot;success&quot;&gt;成功的回调&lt;/param&gt;
  public void Send&lt;TResponse&gt;(string method, string url, Dictionary&lt;string, string&gt; data, UnityAction&lt;TResponse&gt; success)
  {
    MonoManager.Instance.StartCoroutine(RSendRequest&lt;TResponse&gt;(method, url, data, success, null));
  }

  /// &lt;summary&gt;
  /// 发起网络请求
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;method&quot;&gt;请求类型&lt;/param&gt;
  /// &lt;param name=&quot;url&quot;&gt;链接&lt;/param&gt;
  /// &lt;param name=&quot;data&quot;&gt;参数&lt;/param&gt;
  /// &lt;param name=&quot;success&quot;&gt;成功的回调&lt;/param&gt;
  /// &lt;param name=&quot;error&quot;&gt;失败的回调&lt;/param&gt;
  private IEnumerator RSendRequest&lt;TResponse&gt;(string method, string url, Dictionary&lt;string, string&gt; data, UnityAction&lt;TResponse&gt; success, UnityAction&lt;string&gt; error)
  {

    string getData = &quot;&quot;;
    WWWForm postData = new WWWForm();

    // 如果有参数 构造参数
    if (data != null &amp;&amp; data.Count &gt; 0)
    {
      switch (method)
      {
        case &quot;GET&quot;:
          getData = GetQueryParams(data);
          break;

        case &quot;POST&quot;:
          foreach (KeyValuePair&lt;string, string&gt; item in data)
          {
            postData.AddField(item.Key, item.Value);
          }
          break;
      }
    }

    UnityWebRequest webRequest = method == &quot;GET&quot; ? UnityWebRequest.Get(string.Format(&quot;{0}?{1}&quot;, url, getData)) : UnityWebRequest.Post(url, postData);

    yield return webRequest.SendWebRequest();
    //异常处理，很多博文用了error!=null这是错误的，请看下文其他属性部分
    if (webRequest.isHttpError || webRequest.isNetworkError)
    {
      if (error != null) error(webRequest.error);
    }
    else
    {
      success(JsonConvert.DeserializeObject&lt;TResponse&gt;(webRequest.downloadHandler.text));
    }
  }

  /// &lt;summary&gt;
  /// 获取 GET 参数
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;param&quot;&gt;参数&lt;/param&gt;
  /// &lt;returns&gt;GET 参数&lt;/returns&gt;
  public string GetQueryParams(Dictionary&lt;string, string&gt; param)
  {
    string data = &quot;&quot;;
    bool frist = true;
    // 获取 key
    foreach (KeyValuePair&lt;string, string&gt; item in param)
    {
      string and = frist ? &quot;&quot; : &quot;&amp;&quot;;
      data = string.Format(&quot;{0}{1}{2}={3}&quot;, data, and, item.Key, item.Value);
      frist = false;
    }

    return data;
  }

}
</code></pre>
<p>这个类继承了单例，单例代码就不演示了，比较简单</p>
<p>功能就是进行网络请求，把返回值转成对应的返回值类传入<code>success</code>回调函数</p>
<p>这里的类型就是下面这个，里面指定了<strong>参数类型</strong>和<strong>返回值类型</strong>，所有的都需要自己声明</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// &lt;summary&gt;
/// 参数类型约束
/// &lt;/summary&gt;
namespace Params
{
  public class ParamBase
  {

  }

  public class POST_TEST : ParamBase
  {
    public string param;
  }

  public class POST_TEST_COMPLEX : ParamBase
  {
    public string param;
    public bool complex;
  }
}

/// &lt;summary&gt;
/// 返回值类型约束
/// &lt;/summary&gt;
namespace Responses
{
  [SerializeField]
  public class ResponsesBase : ScriptableObject
  {

  }

  public class POST_TEST : ResponsesBase
  {
    public string param;
  }

  public class POST_TEST_COMPLEX
  {

    public int code;
    public string info;

    public DATA data;

    public class DATA
    {
      public string param;
      public string param2;
    }

  }
}
</code></pre>
<p>比如<code>Responses.POST_TEST_COMPLEX</code>对应的JSON是这样的</p>
<pre><code class="language-json">{
  &quot;code&quot;: 0,
  &quot;info&quot;: &quot;成功&quot;,
  &quot;data&quot;: {
    &quot;param&quot;: &quot;test&quot;,
    &quot;param2&quot;: &quot;2333&quot;
  }
}
</code></pre>
<p>使用时</p>
<pre><code class="language-csharp">Url.POST_TEST_COMPLEX.Request(new Params.POST_TEST_COMPLEX()
{
  param = &quot;测试&quot;,
  complex = true
}, (json) =&gt;
{
  DataManager.Instance.GenerateJsonFile(&quot;test&quot;, json);
});
</code></pre>
<p>并且有完善的代码提示和类型约束</p>
<p>传参代码提示：</p>
<p><img src="/uploads/2021/01/url-2.png" alt=""></p>
<p>传参类型约束：</p>
<p><img src="/uploads/2021/01/url-3.png" alt=""></p>
<p>返回值代码提示：</p>
<p><img src="/uploads/2021/01/url-4.png" alt=""></p>
<p>这里面用的JSON格式化包是<a href="https://github.com/JamesNK/Newtonsoft.Json">Newtonsoft.Json</a>，比<code>JsonUtility</code>不知道要好用多少</p>
<h1>结束</h1>
<p>OK 以上就是这个请求接口类</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[PicGo 插件开发入门]]></title>
            <link>https://imba97.com/posts/737</link>
            <guid isPermaLink="false">https://imba97.com/posts/737</guid>
            <pubDate>Wed, 06 Jan 2021 14:10:44 GMT</pubDate>
            <content:encoded><![CDATA[<h1>PicGo</h1>
<p><a href="https://github.com/Molunerfinn/PicGo">PicGo</a>是<strong>一个用于快速上传图片并获取图片 URL 链接的工具</strong>，可以把本地图片上传到图床并返回各种格式的链接</p>
<p><img src="https://imba97.com/uploads/2021/01/picgo-1.png" alt=""></p>
<!--more-->
<h1>插件</h1>
<p>PicGo支持插件开发，但官方教程并没有从零到安装成功的具体细节演示，只说了插件开发相关的一些东西</p>
<p>所以对新手的我来说还是碰到很多坑的，在此记录分享一下</p>
<h1>入门教程</h1>
<p>下面我会用PicGo给我们提供的<a href="https://github.com/PicGo/picgo-template-plugin"><strong>插件开发模板</strong></a>进行插件从零到安装成功的开发过程演示</p>
<h2>全局安装 PicGo</h2>
<pre><code class="language-bash">yarn global add picgo
# or
npm install picgo -g
</code></pre>
<h2>下载模板</h2>
<pre><code class="language-bash">picgo init plugin &lt;your-project-name&gt;
</code></pre>
<p>根据提示进行配置</p>
<pre><code class="language-bash">[PicGo INFO]: Template files are downloading...
[PicGo SUCCESS]: Template files are downloaded!
# 插件名称
? Plugin name: test
# 插件简介
? Plugin description: 只是一个测试插件
# 作者
? author: imba97
# 开发模块（可多个，上下键移动，空格选择/取消）
? Choose modules you want to develop: uploader
# 是否只是命令行插件
? Your plugin is just used in CLI? No
# 用 TypeScript 还是 JavaScript
? Use TS or JS? ts
# 有没有快捷方式
? Your plugin has some shortcut for GUI? No
# 创建成功
[PicGo SUCCESS]:
Generate template files successfully!
Please cd C:\Users\imba97\Desktop\test, and then
# 接下来进入文件夹 安装依赖包
npm install
# or
yarn
</code></pre>
<h2>编辑代码</h2>
<p><code>src</code>目录下默认有一个<code>index.ts</code></p>
<pre><code class="language-typescript">import picgo from 'picgo'

export = (ctx: picgo) =&gt; {
  const register = () =&gt; {
    // 添加一行日志输出试一下
    ctx.log.success('test 插件加载成功')
    ctx.helper.uploader.register('test', {
      handle(ctx) {
        console.log(ctx)
      }
    })
  }
  return {
    uploader: 'test',
    register
  }
}
</code></pre>
<h2>Build</h2>
<p>这一步很重要，因为插件默认执行的是<code>dist/index.js</code>，这个配置在<code>package.json</code>中，如果没Build会安装失败</p>
<pre><code class="language-bash">npm run build
# 输出
&gt; picgo-plugin-test@1.0.0 build C:\Users\imba97\Desktop\test
&gt; tsc -p .
</code></pre>
<p>之前不知道，一直卡在这一步</p>
<h2>安装本地插件测试</h2>
<p>复制你本地插件的目录，比如我的是：<code>C:\Users\imba97\Desktop\test</code></p>
<p>来到配置文件所在的文件夹</p>
<ul>
<li>Windows: <code>%APPDATA%\picgo\data.json</code></li>
<li>Linux: <code>$XDG_CONFIG_HOME/picgo/data.json</code> or <code>~/.config/picgo/data.json</code></li>
<li>macOS: <code>~/Library/Application\ Support/picgo/data.json</code></li>
</ul>
<p>打开命令行，安装插件</p>
<pre><code class="language-bash">npm install C:\Users\imba97\Desktop\test
</code></pre>
<p>不报错的话你就可以在插件列表里看到你的插件了</p>
<p><img src="/uploads/2021/01/picgo-2.png" alt=""></p>
<p>把它设置成默认图床</p>
<p><img src="/uploads/2021/01/picgo-3.png" alt=""></p>
<p>随便上传个东西，看一下日志有没有正常输出</p>
<pre><code class="language-bash">2021-01-06 16:12:40 [PicGo SUCCESS] test 插件加载成功
2021-01-06 16:12:40 [PicGo INFO] [PicGo Server] is listening at 36677
</code></pre>
<p>修改代码后需要重新Build，或者直接<code>npm run dev</code>，开发模式有热更新，只需要重启应用即可</p>
<p>PicGo程序本体这边可以下载源码，运行<code>npm run electron:serve</code>进入开发模式，应该可以更方便的开发插件，但我用的时候会报错：</p>
<p><img src="https://camo.githubusercontent.com/3589a6c41641bcdc27fb66cb246ef03cfc957c6e4b374a2b717c540e70dd5e0d/68747470733a2f2f692e6c6f6c692e6e65742f323032302f31322f30392f63747834466935477a4b616772496c2e706e67" alt=""></p>
<p>网上常见的解决方法无效，所以我的调试是无限<code>Build-&gt;重启应用</code></p>
<h2>卸载测试插件</h2>
<p>卸载应该是需要手动删除，点卸载按钮的话会报错，具体需要删除以下几个东西，在配置文件同级目录中</p>
<ul>
<li>node_modules/测试插件<strong>目录</strong>（快捷方式）</li>
<li>package.json 中的测试插件那一<strong>行</strong></li>
<li>package-lock.json <strong>文件</strong></li>
<li>data.json 中 picgoPlugins 中的测试插件那一<strong>行</strong></li>
</ul>
<p>也许有正确的卸载方式，只是我不知道，各位自行研究吧 233</p>
<h2>Log</h2>
<p>如果遇到任何问题，首先建议检查 Log，Windows 的路径在：<code>C:\Users\&lt;用户名&gt;\AppData\Roaming\picgo\picgo.log</code>，其他系统不确定</p>
<p>应该是跟<strong>配置文件</strong>在一起的</p>
<h1>告辞</h1>
<p>OK，接下来你可以在<a href="https://picgo.github.io/PicGo-Core-Doc/zh/dev-guide/cli.html">《插件开发指南》</a>获取更多教程，开启插件开发之路了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Typescript 浏览器插件开发 chrome/browser 代码提示]]></title>
            <link>https://imba97.com/posts/736</link>
            <guid isPermaLink="false">https://imba97.com/posts/736</guid>
            <pubDate>Tue, 05 Jan 2021 00:11:21 GMT</pubDate>
            <content:encoded><![CDATA[<h1>chrome/browser</h1>
<p>这两个是浏览器为插件开发提供的API，Chrome浏览器的变量名叫<code>chrome</code>，火狐浏览器的变量名叫<code>browser</code></p>
<!--more-->
<h1>代码提示</h1>
<p>我用的编辑器是vscode，之前一直不明白怎么给这两个变量添加代码提示</p>
<h2>最早尝试</h2>
<p>最开始我想的是加一个全局变量，搜到可以在<code>.eslintrc</code>配置中添加一个<code>globals</code>，把变量名加进去，但后来发现这个东西跟代码提示没半毛钱关系。这是<code>ESLint</code>的配置文件，它的功能只是代码检查。</p>
<p>问过插件开发的大佬，大佬只说导入Library啥的，当时的我亿脸懵逼。</p>
<h2>typings</h2>
<p>看了github上的各种插件，发现有一个<code>@types/chrome</code>，<code>typings</code>是TypeScript的定义管理器，描述命名空间、接口、变量等各种东西，<code>@types/chrome</code>呢也就是别人写好的一个描述文件，去告诉编辑器我这里有个<code>chrome</code>变量，它下面有哪些方法之类的。</p>
<p>使用npm安装</p>
<pre><code class="language-bash">npm install --save-dev @types/chrome
</code></pre>
<p>安装好后发现<code>chrome</code>的代码提示有了，但只有<code>chrome</code>，火狐浏览器的<code>browser</code>依然没有。</p>
<h2>自定义typings</h2>
<p>前几天突发奇想，因为vue是可以自定义全局和实例变量的代码提示的，那么<code>browser</code>也许能自定义，于是照着这个思路，搜了一下怎么新建一个描述，去“继承”<code>chrome</code></p>
<p>其实非常简单，只需要创建个文件，比如我在<code>src/typings/</code>下创建了一个<code>browser.d.ts</code>。这里具体在哪个文件夹下无所谓，读取这个文件是在<code>tsconfig.json</code>中配置的</p>
<pre><code class="language-json">{
  &quot;include&quot;: [
    &quot;src/**/*.ts&quot;,
    &quot;src/**/*.d.ts&quot;
  ]
}
</code></pre>
<p>意思就是，加载<code>src</code>目录下的包括所有文件夹中的<code>.ts</code>、<code>.d.ts</code>文件，所以只是创建在<code>src</code>目录下就行</p>
<p>然后在这个文件里添加</p>
<pre><code class="language-typescript">declare const browser: typeof chrome
</code></pre>
<p>这样就有<code>browser</code>变量了</p>
<h2>添加vue变量</h2>
<p>再新建一个<code>vue.d.ts</code>，在里面添加</p>
<pre><code class="language-typescript">import Vue from 'vue'
declare module 'vue/types/vue' {
  interface Vue {
    $chrome: typeof chrome
  }
  interface VueConstructor {
    chrome: typeof chrome
  }
}
</code></pre>
<p>更多详情可参考vue官网的说明：<a href="https://cn.vuejs.org/v2/guide/typescript.html#%E5%A2%9E%E5%BC%BA%E7%B1%BB%E5%9E%8B%E4%BB%A5%E9%85%8D%E5%90%88%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8">《TypeScript 支持》</a></p>
<p>以上，代码提示添加完成</p>
<h1>使用示例</h1>
<p>然后你就可以给Vue的chrome赋值，以<code>background.ts</code>为例，里面封装了一个网络请求</p>
<pre><code class="language-typescript">import axios from 'axios'
import Vue from 'vue'

Vue.chrome = Vue.prototype.$chrome = chrome || browser

Vue.chrome.runtime.onMessage.addListener((request, sender, sendResponse) =&gt; {
  const headers = typeof request.headers !== 'undefined'
    ? request.headers
    : {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.87 Safari/537.36'
      }

  switch (request.type) {
    case 'get':
      axios.get(request.url, headers)
        .then((response) =&gt; {
          sendResponse(response.data)
        })
      break
    case 'post':
      axios.post(request.url, request.data, headers)
        .then((response) =&gt; {
          sendResponse(response.data)
        })
      break
  }

  return true
})
</code></pre>
<p>然后<code>popup.ts</code>中，也需要加入</p>
<pre><code class="language-typescript">import Popup from '@components/popup'
import Vue from 'vue'
import '@styles/popup'

Vue.chrome = Vue.prototype.$chrome = chrome || browser

export default new Vue({
  components: {
    Popup
  },
  render: h =&gt; h(Popup)
}).$mount('#app')
</code></pre>
<p>在<code>popup.vue</code>中使用</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;h1&gt;{{ title }}&lt;/h1&gt;
    &lt;span&gt;{{ message }}&lt;/span&gt;
    &lt;input v-model=&quot;uid&quot; type=&quot;text&quot; value=&quot;&quot; placeholder=&quot;输入UID&quot;&gt;
    &lt;button @click=&quot;btn&quot;&gt;
      请求数据测试
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Popup extends Vue {
  title = 'Popup'
  message = ''
  uid = ''

  public btn() {
    if (this.uid === '') {
      this.message = '未输入UID'
      return
    }

    this.$chrome.runtime.sendMessage({
      type: 'get',
      url: `https://api.bilibili.com/x/web-interface/card?mid=${this.uid}&amp;photo=1&amp;jsonp=jsonp`
    }, (json) =&gt; {
      console.log(json)
      if (typeof json.data !== 'undefined') { this.message = `获取成功！昵称：${json.data.card.name}，当前粉丝数：${json.data.card.fans}` }
    })
  }
}
&lt;/script&gt;
</code></pre>
<p>其他细节可查看Github项目：<a href="https://github.com/imba97/Btools-vue">Btools-vue</a></p>
<h1>总结</h1>
<ol>
<li>
<p>如果typescript项目用到的某变量没有代码提示可在npm搜索<code>@types/xxx</code>查找</p>
</li>
<li>
<p>typings完全可以手写，只是比较麻烦</p>
</li>
<li>
<p>为一个变量声明类型可以用<code>typeof xxx</code></p>
<p>之前想到了在typings文件中声明，但不知道得用typeof，一直报错：namespace不能作为类型使用</p>
</li>
</ol>
<h1>更新</h1>
<p>2021-07-06 更新，发现有一个插件API的包<a href="https://github.com/Lusito/webextension-polyfill-ts">webextension-polyfill-ts</a>，直接引入就可以用</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[魔幻的2020]]></title>
            <link>https://imba97.com/posts/735</link>
            <guid isPermaLink="false">https://imba97.com/posts/735</guid>
            <pubDate>Thu, 31 Dec 2020 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>一年过得真快，2020年，我依然是延续去年的状态</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<!--more-->
<p>这次写篇小文章总结吧</p>
<h1>工作</h1>
<p>过完春节，因为疫情的关系一直在家办公，我也非常喜欢这种办公模式，老宅男了。种种原因，公司的情况一直不是很好，年后没几个月，我从全职变成了兼职。兼职工资刚好够吃，再加上当时住的地方附近没有合适的工作，我就没急着找工作，自己做了做自媒体，于是就有了<a href="https://space.bilibili.com/2198461/channel/detail?cid=132081">《每天一个JS程序》</a>。其实从很早之前开始我也一直在做视频，只是当做一个兴趣，把做视频当正事干了之后，涨粉也算挺快（对我来说）。</p>
<p>但做视频的收入并不能养活我，公司的兼职到8月会最后一次发工资。这之后最现实的选择有两个，一是找工作，但因为我附近没有合适的，我又想在家附近找，所以只能搬家。二是先随便找个餐饮店、便利店之类的兼职，等合适的全职工作。当时挺佛系的，也没啥危机感，不紧不慢两边都在找。</p>
<p>也就在8月，出现了转机，发小突然问我要不要一起做独立游戏，经过各种探讨，最后我决定：**入坑！**于是我在 <span start-time="2020/08/21 00:00:00" interval-time="50" id="game"></span><script>if(typeof dateMaker !== 'undefined') dateMaker('game');</script> 前成为了一个独立游戏开发者。</p>
<p>工资也就这么魔幻般的接上了。</p>
<h1>生活</h1>
<p>搬了几次家，上海-&gt;成都-&gt;巴中，现在东西越来越多，搬家还挺费钱的，而且很累！越来越想把台式机换成笔记本了，毕竟我也不玩啥大型游戏，只打打魔兽争霸敲敲代码。</p>
<p>**终于有了猫！**这猫到我家快四个月了，一直没起名字。今天是今年的最后一天，我决定用朋友给它起的名字：“串串”，因为是英短和虎纹串的。</p>
<h1>程序</h1>
<ol>
<li>
<p><a href="https://github.com/imba97/js">《每天一个JS程序》视频对应的程序</a></p>
<p>里面有不少实用的网页收藏夹小程序</p>
</li>
<li>
<p>进行了两次 Pull Requests</p>
<p><a href="https://github.com/stevenyangecho/laravel-u-editor/pull/76">laravel-u-editor</a></p>
<p>这是百度富文本编辑器的<code>laravel</code>框架版本，我需要用到FTP上传图片，所以加了个FTP上传功能，虽然作者一直没更新</p>
<p><a href="https://github.com/TriDiamond/hexo-theme-obsidian/pull/111">hexo-theme-obsidian</a></p>
<p>这是我目前博客用的主题，发现了一些问题，小修了一下</p>
</li>
<li>
<p>简单学习了<code>electron</code>、<code>vue</code>、<code>typescript</code>，并做了个<a href="https://github.com/imba97/electron-vue-typescript-demo">demo</a></p>
<p><code>vue</code>和<code>typescript</code>之前就看过，想用这个重构一下<code>Btools</code>，但一直懒就没搞。那个demo是B站客户端，之前打算做的，目前只有个二维码登录功能，本来跟别人合作开发的，后来别人退坑我也就懒的搞了</p>
</li>
<li>
<p>入坑了<code>Unity3D</code>和<code>C#</code></p>
<p>以前对学习一门新语言有点抵触，但学了发现只要之前会一些编程语言，再入坑其他的还是挺容易上手的</p>
</li>
</ol>
<h1>小玩意儿</h1>
<ol>
<li>
<p><a href="/posts/682">做了个旁路由</a></p>
<p>目前用来净化网络环境，屏蔽广告和行为追踪之类的，还有DNS自动选优，以及你懂的功能</p>
</li>
<li>
<p><a href="/posts/732">做了个本地网盘</a></p>
<p>用树莓派加一块硬盘，做了一个本地网盘，并使用<code>Aria2</code>进行离线下载，使用<code>frp</code>进行内网穿透（虽然外网访问网盘速度极慢）</p>
<img src="https://imba97.com/uploads/2020/12/2020-1.jpg" style="height: 300px;" />
</li>
</ol>
<p>我倒是挺喜欢研究一个技术类的东西，并且能正反馈到生活中。</p>
<hr>
<p>2021继续混吃等死。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[WordPress 迁移至 Hexo]]></title>
            <link>https://imba97.com/posts/734</link>
            <guid isPermaLink="false">https://imba97.com/posts/734</guid>
            <pubDate>Sat, 05 Dec 2020 23:31:34 GMT</pubDate>
            <content:encoded><![CDATA[<p>记录一下本站从WordPress迁移至Hexo的过程</p>
<p>Hexo之前也接触过，不过只是看看，没摸</p>
<p>三个原因让我决定使用Hexo</p>
<!--more-->
<h1>原因</h1>
<h2>服务器出大问题</h2>
<p>第一个原因是服务器的<code>systemd</code>被我亿不小心覆盖了（当时提示是否覆盖，没仔细看直接回车了）</p>
<p>当时我就感觉，我的服务器不完整了，毕竟像开机启动的程序，都配置在里面</p>
<p>本来以为会开不了机，后来发现并不会</p>
<p>但我还是感觉，我的服务器被玷污了（草）</p>
<h2>想试试纯静态化</h2>
<p>第二个原因是偶然看到别的大佬的博客有篇文章，说的是把博客纯静态化的过程</p>
<h2>Markdown YES!</h2>
<p>第三个原因是<code>Markdown</code>写作确实是爽，之前WordPress我还专门下了<code>Markdown</code>编辑器插件</p>
<p>现在想想，还好之前用了<code>Markdown</code></p>
<h1>迁移</h1>
<p>Hexo提供了<a href="https://hexo.io/zh-cn/docs/migration#WordPress">迁移文档</a>，可以用插件把WordPress导出的数据转成对应的文章</p>
<p>但实际非常拉胯，格式基本就没对过</p>
<p>于是我就想从数据库直接读出<code>Markdown</code>，但后来发现数据库中的数据，换行等字符是被转义过的，也没法直接用</p>
<p>最后没辙，就只能展现我的手艺了：<strong>复制粘贴</strong>（老手艺人了）</p>
<p>从WordPress后台，打开每篇文章的编辑页面，一点点把自动迁移的文档内容给替换掉了</p>
<p>其实迁移插件还是有点用的，至少文章类型、标签、发布时间还是都有的，也算省了很多事吧</p>
<h1>教程</h1>
<h2>在本地安装 Hexo</h2>
<p>根据<a href="https://hexo.io/zh-cn/docs/">官网文档</a>，你需要有<code>nodejs &gt;=10.13</code>和<code>git</code>，可自行在<a href="https://nodejs.org/en/">Node.js官网</a>和<a href="https://git-scm.com/">Git官网</a>下载</p>
<pre><code class="language-bash"># 安装Hexo命令行工具
npm install hexo-cli -g
# 在合适的目录创建博客
hexo init blog
# 进入博客目录
cd blog
# 安装依赖 如果安装失败或极慢 可以考虑使用 cnpm
npm install
# 启动服务器
hexo server # 简写 hexo s
</code></pre>
<p>这样本地工作暂时告一段落</p>
<h2>服务器网站配置</h2>
<p>因为我是重装了系统，所以都需要从头来</p>
<p>开机第一步，宝塔 install</p>
<p>根据<a href="https://www.bt.cn/bbs/thread-19376-1-1.html">宝塔文档</a>，我是<code>CentOS</code>系统，使用以下组合命令下载<code>wget</code>，使用<code>wget</code>下载宝塔安装脚本，安装宝塔</p>
<pre><code class="language-bash">yum install -y wget &amp;&amp; wget -O install.sh http://download.bt.cn/install/install_6.0.sh &amp;&amp; sh install.sh
</code></pre>
<p>宝塔安装好后进入后台，自行配置一下</p>
<p>然后服务器建议用<code>Nginx</code>，其他看你需求</p>
<p><img src="https://imba97.com/uploads/2020/12/wordpress-to-hexo-1.png" alt=""></p>
<p>安装好后添加静态网站</p>
<p><img src="/uploads/2020/12/wordpress-to-hexo-2.png" alt=""></p>
<p>服务器的网站部分就到这了</p>
<p>如果你不想用宝塔这种管理软件，你也可以自行安装<code>Nginx</code>，只要网站运行起来就OK</p>
<h2>自动部署服务器配置</h2>
<p>自动部署是将你本地的<code>Hexo</code>生成的静态页面自动上传到服务器上</p>
<p>原理是用了<code>git hook</code>，当你<code>push</code>的时候，会在服务器执行命令让网站目录强制<code>checkout</code>最新的代码</p>
<p>首先我们需要在服务器上创建一个<code>git</code>仓库，这个步骤你可以参考<a href="/posts/281">《搭建自己的Git服务器》</a></p>
<p>创建好仓库后，不必克隆到本地，而是使用一个插件</p>
<h2>自动部署本地配置</h2>
<p>安装<code>hexo-deployer-git</code>插件，在本地的博客目录下执行</p>
<pre><code class="language-bash">npm install hexo-deployer-git --save
</code></pre>
<p>然后在<code>_config.yml</code>添加配置，将你的仓库地址改成对应的</p>
<pre><code class="language-yaml">deploy:
  type: git
  repo: git@IP:/home/git/blog.git # 仓库地址
  branch: master
</code></pre>
<h2>权限问题</h2>
<p>参考：<a href="https://www.cnblogs.com/jie-fang/p/13445939.html">《将Hexo部署到自己的服务器上》</a></p>
<p>为<code>git</code>用户设置权限</p>
<pre><code class="language-bash"># 修改权限配置文件的权限（默认是只读）
chmod 740 /etc/sudoers
# 编辑
vim /etc/sudoers
# 修改完可以再把权限改回来
chmod 400 /etc/sudoers
</code></pre>
<p>找到 <code>root ALL=(ALL) ALL</code>，在下面新加一条</p>
<pre><code class="language-ini">git  ALL=(ALL)  NOPASSWD: ALL
</code></pre>
<p>这里我加了一个<code>NOPASSWD</code>，是为了执行<code>sudo</code>时无需输入密码，后面会用到</p>
<h2>Git Hook</h2>
<p>在仓库地址下<code>hooks</code>目录中创建一个文件</p>
<pre><code class="language-bash">vim /home/git/blog.git/hooks/post-receive
</code></pre>
<p>添加以下内容，<code>--work-tree</code>是网站根目录，<code>--git-dir</code>是仓库目录</p>
<pre><code class="language-bash">#!/bin/sh
sudo chown -R git:git /www/wwwroot/blog
sudo git --work-tree=/www/wwwroot/blog --git-dir=/home/git/blog.git checkout -f
sudo chown -R www:www /www/wwwroot/blog
</code></pre>
<p>因为我装了宝塔，宝塔网站目录默认的用户是<code>www</code>，如果直接<code>checkout</code>会报错没权限，直接改成<code>git</code>又会让网页会加载不出来</p>
<p>所以这里就先把它变成<code>git</code>用户的，<code>checkout</code>完后改回<code>www</code>用户</p>
<h1>使用</h1>
<p>Hexo可真是太舒服了，在<code>source/_posts</code>编辑文章</p>
<p>你可以用命令创建文章模板</p>
<pre><code class="language-bash">hexo new post &quot;文章标题&quot;
</code></pre>
<p>编辑完后</p>
<pre><code class="language-bash"># 生成静态文件
hexo g
# 可以先在本地开启服务器，检查文章
hexo s
</code></pre>
<p>检查到需要修改时可以不用停止服务器，直接改完刷新浏览器的网页就能生效</p>
<p>觉得没问题后，部署到你的服务器</p>
<pre><code class="language-bash">hexo d
</code></pre>
<p>如果你改过配置或者生成的静态文件不对，你需要在生成前先清理一下</p>
<pre><code class="language-bash">hexo clean
</code></pre>
<p>OK，以上就是本期文章</p>
<h1>爷青结</h1>
<p>用了两年多的WordPress退役了，👴的青春结束了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[树莓派 Cloudreve Aria2 离线下载]]></title>
            <link>https://imba97.com/posts/733</link>
            <guid isPermaLink="false">https://imba97.com/posts/733</guid>
            <pubDate>Sat, 05 Dec 2020 19:31:50 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Aria2</h1>
<p>aria2是一个轻量级下载工具，支持HTTP、FTP、磁力链接等</p>
<p>cloudreve的离线下载功能会使用它进行下载</p>
<!--more-->
<h1>下载</h1>
<pre><code class="language-bash">sudo apt install -y aria2
</code></pre>
<h1>配置</h1>
<h2>Aria2</h2>
<p>网上找到的一份配置模板</p>
<p>需要修改的</p>
<ul>
<li><code>rpc-secret</code> 需要你自己定义 ，cloudreve的配置中会用到</li>
<li><code>dir</code>下载文件夹，这个可以在cloudreve后台配置</li>
<li><code>save-session</code>和<code>input-file</code> 会话文件的位置，这个文件也需要自己创建</li>
</ul>
<pre><code class="language-bash"># 后台运行
daemon=true
# 用户名
# rpc-user=user
# 密码
# rpc-passwd=passwd
# 设置加密的密钥
rpc-secret=token
# 允许rpc
enable-rpc=true
# 允许所有来源, web界面跨域权限需要
rpc-allow-origin-all=true
# 是否启用https加密，启用之后要设置公钥,私钥的文件路径
# rpc-secure=true
# 启用加密设置公钥
# rpc-certificate=/home/pi/.config/aria2/example.crt
# 启用加密设置私钥
# rpc-private-key=/home/pi/.config/aria2/example.key
# 允许外部访问，false的话只监听本地端口
rpc-listen-all=true
# RPC端口, 仅当默认端口被占用时修改
# rpc-listen-port=6800
# 最大同时下载数(任务数), 路由建议值: 3
max-concurrent-downloads=5
# 断点续传
continue=true
# 同服务器连接数
max-connection-per-server=5
# 最小文件分片大小, 下载线程数上限取决于能分出多少片, 对于小文件重要
min-split-size=10M
# 单文件最大线程数, 路由建议值: 5
split=10
# 下载速度限制
max-overall-download-limit=0
# 单文件速度限制
max-download-limit=0
# 上传速度限制
max-overall-upload-limit=0
# 单文件速度限制
max-upload-limit=0
# 断开速度过慢的连接
# lowest-speed-limit=0
# 验证用，需要1.16.1之后的release版本
# referer=*
# 文件保存路径, 默认为当前启动位置(我的是外置设备，请自行坐相应修改)
dir=/home/pi/aria2/downloads
# 文件缓存, 使用内置的文件缓存, 如果你不相信Linux内核文件缓存和磁盘内置缓存时使用, 需要1.16及以上版本
# disk-cache=0
# 另一种Linux文件缓存方式, 使用前确保您使用的内核支持此选项, 需要1.15及以上版本(?)
# enable-mmap=true
# 文件预分配, 能有效降低文件碎片, 提高磁盘性能. 缺点是预分配时间较长
# 所需时间 none &lt; falloc ? trunc &lt;&lt; prealloc, falloc和trunc需要文件系统和内核支持
file-allocation=prealloc
# 不进行证书校验
check-certificate=false
# 保存下载会话
save-session=/home/pi/aria2/aria2.session
input-file=/home/pi/aria2/aria2.session
# 断点续传
save-session-interval=60
</code></pre>
<p>接下来配置服务</p>
<p>与<code>cloudreve</code>程序一样，新建一个<code>service</code></p>
<pre><code class="language-bash">sudo vim /lib/systemd/system/aria.service
</code></pre>
<p>在<code>ExecStart</code>执行调用程序并加载制定的配置文件的命令</p>
<pre><code class="language-bash">[Unit]
Description=Aria2 Service
After=network.target

[Service]
User=pi
Type=forking
ExecStart=/usr/bin/aria2c --conf-path=/home/pi/.config/aria2/aria2.config

[Install]
WantedBy=multi-user.target
</code></pre>
<p>启动</p>
<pre><code class="language-bash"># 开机启动
sudo systemctl enable aria
# 启动服务
sudo service aria start
</code></pre>
<p>开机启动</p>
<h2>Cloudreve</h2>
<p>然后来到cloudreve的管理界面，这里配置都有说明就不多赘述了</p>
<p>只需要注意<code>RPC Secret</code>这项要跟aria2配置中的<code>rpc-secret</code>保持一致就行</p>
<p><img src="https://imba97.com/uploads/2020/12/raspberry-pi-cloudreve-aria2-1.png" alt=""></p>
<h1>后言</h1>
<p>cloudreve默认每过一分钟查询一次进度</p>
<p><img src="/uploads/2020/12/raspberry-pi-cloudreve-aria2-2.png" alt=""></p>
<p>实际离线下载功能还有些许问题，我下载磁力链接遇到过的比如</p>
<ul>
<li>
<p>添加离线下载任务后要等很长时间才会下载</p>
</li>
<li>
<p>下载到100%需要手动取消任务才会出现在网盘里</p>
</li>
</ul>
<p>不过HTTP我试了是没问题的，正常下载，正常保存</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[树莓派 Cloudreve 自建局域网网盘]]></title>
            <link>https://imba97.com/posts/732</link>
            <guid isPermaLink="false">https://imba97.com/posts/732</guid>
            <pubDate>Wed, 02 Dec 2020 16:57:17 GMT</pubDate>
            <content:encoded><![CDATA[<h1>Cloudreve</h1>
<p><a href="https://cloudreve.org/">Cloudreve</a> 是个网盘系统，包含前后端，解压即用，贼吉尔方便</p>
<!--more-->
<h2>下载</h2>
<p>在 <a href="https://github.com/cloudreve/Cloudreve/releases">Github</a> 找到对应的压缩包，树莓派下载<code>arm</code>的</p>
<p>复制链接，使用<code>wget</code>下载到树莓派</p>
<pre><code class="language-bash">wget https://github.com/cloudreve/Cloudreve/releases/download/x.x.x/cloudreve_x.x.x_linux_arm.tar.gz
</code></pre>
<h2>解压</h2>
<pre><code class="language-bash">tar -zxvf cloudreve.tar.gz
</code></pre>
<p>解压出来之后是个可执行文件<code>cloudreve</code></p>
<p>首先先执行一次，会自动在当前目录创建一个配置文件<code>conf.ini</code>，并显示默认帐号和随机生成的密码</p>
<p>Cluedreve 默认存储是<code>SQLite</code>本地存储，所以会在当前目录创建一个<code>.db</code>的文件</p>
<h1>Mysql</h1>
<p>不过它支持<code>mysql</code>，这里我选用<code>mysql</code></p>
<h2>安装</h2>
<pre><code class="language-bash">sudo apt install mysql-server
</code></pre>
<p>如果因为软件源的问题提示未找到<code>mysql-server</code>，你可以试试安装另一个，这两个是一样的</p>
<pre><code class="language-bash">sudo apt install mariadb-server
</code></pre>
<p>安装好后启动服务</p>
<pre><code class="language-bash">sudo service mysql start
</code></pre>
<p>设置root密码</p>
<pre><code class="language-bash">sudo mysqladmin -u root password 123456
# 顺带一提 修改密码的话执行以下命令并输入你原来的密码
sudo mysqladmin -u root -p password 123456
</code></pre>
<p>接下来就可以进入Mysql了</p>
<pre><code class="language-bash">mysql -u root -p
</code></pre>
<h2>创建数据库</h2>
<p>我们要创建一个数据库，名字可以随便起，这里我就叫<code>cloudreve</code></p>
<pre><code class="language-bash">create database cloudreve;
</code></pre>
<p>你可以输入<code>show databases;</code>查看当前有哪些数据库</p>
<p><img src="https://imba97.com/uploads/2020/12/raspberry-pi-cloudreve-1.png" alt=""></p>
<h2>启用mysql</h2>
<p>在 Cluedreve 配置文件启用mysql</p>
<pre><code class="language-ini">[System]
# 省略...

[Database]
Type = mysql
User = root
Password = root
Host = 127.0.0.1
Name = cloudreve
</code></pre>
<h1>开机启动</h1>
<p>创建一个<code>service</code>文件，配置开机启动</p>
<pre><code class="language-bash">sudo vim /lib/systemd/system/cloudreve.service
</code></pre>
<p>复制以下代码，把<code>WorkingDirectory</code>和<code>ExecStart</code>改为你的路径</p>
<pre><code class="language-ini">[Unit]
Description=Cloudreve
Documentation=https://docs.cloudreve.org
After=network.target
Wants=network.target

[Service]
Type=simple
WorkingDirectory=/home/pi/cloudreve
ExecStart=/home/pi/cloudreve/cloudreve
Restart=on-abnormal
RestartSec=5s
KillMode=mixed

StandardOutput=null
StandardError=syslog

[Install]
WantedBy=multi-user.target
</code></pre>
<p>设置开机启动并且开启服务</p>
<pre><code class="language-bash"># 开机启动
sudo systemctl enable cloudreve
# 开启服务
sudo service cloudreve start
</code></pre>
<h1>完成</h1>
<p>访问<code>树莓派IP:端口</code>，你就能看到登录页了</p>
<p><img src="/uploads/2020/12/raspberry-pi-cloudreve-2.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Unity UI 管理模块]]></title>
            <link>https://imba97.com/posts/724</link>
            <guid isPermaLink="false">https://imba97.com/posts/724</guid>
            <pubDate>Fri, 27 Nov 2020 14:48:03 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>本教程是在B站UP“<a href="https://space.bilibili.com/79983517">一唐老狮一</a>”的视频教程 <a href="https://www.bilibili.com/video/BV1C441117wU">《Unity的程序基础框架》</a> 中，<strong>UI管理模块</strong>的改进。</p>
<p>因为UP这个教程变成了付费课程，如果想看代码是怎么写的，可以去支持<a href="https://www.taikr.com/course/1062">付费课程</a></p>
<p>或者这篇文章，是别人的笔记： <a href="https://blog.csdn.net/m0_46378049/article/details/106180496">《Unity3D程序基础框架（下）》</a> 中的“<strong>UI管理模块</strong>”部分</p>
<!--more-->
<h1>主要功能</h1>
<ol>
<li>统一管理当前面板下的控件（按钮、图片、文本框等）</li>
<li>UI分层</li>
<li>反射型事件监听</li>
</ol>
<p>其中对原版的改进部分主要是反射型事件监听（我瞎起的名，不知道有没有专业名词），我不太确定这么做会不会有性能问题，还请大佬指教（在视频评论）</p>
<p>这个东西的作用是你在使用按钮等控件时，可以直接在对应的面板类里写方法</p>
<p>比如我有一个面板，上面有几个控件，分别是名叫<code>btnTest</code>和<code>btn2</code>的两个按钮和名叫<code>Test</code>的复选框</p>
<p><img src="https://imba97.com/uploads/2020/11/unity-ui-manager-1.png" alt=""></p>
<p>我只需要写一个类，继承<code>BasePanel</code>类，其他任何复杂的东西都不需要</p>
<p>就可以直接在这个面板类里写<code>控件名+OnClick</code>和<code>控件名+OnValueChanged</code>即可分别监听对应名称的按钮点击事件和复选框切换事件</p>
<p>代码：</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class LoginPanel : BasePanel
{
  public void btnTestOnClick()
  {
    Debug.Log(&quot;Click!!!&quot;);
  }

  public void btn2OnClick()
  {
    Debug.Log(&quot;btn2 Click!!!&quot;);
  }

  public void TestOnValueChanged(bool value)
  {
    Debug.Log(value);
  }
}
</code></pre>
<p>效果演示：</p>
<p><img src="/uploads/2020/11/unity-ui-manager-2.gif" alt=""></p>
<p>如果有其他事件监听，可以添加更多</p>
<h1>教程</h1>
<p>除了反射型事件监听这个功能，其实整个UI部分还包括很多，我们一个个来看</p>
<h2>CanvasManager</h2>
<p>画布管理器，因为UI的所有东西都需要<code>Canvas</code>和<code>EventSystem</code>，你可以把这两个东西拿出来，分别作为预制体</p>
<p>在<code>Canvas</code>里创建几个空物体，用于实现分层</p>
<p><img src="/uploads/2020/11/unity-ui-manager-3.png" alt=""></p>
<p>Bot、Mid、Top、Sys分别是底层、中层、顶层、系统层</p>
<p>UI分层的意义很好理解，举个例子。我有一个装备栏把它放在底层，鼠标放在装备上弹出装备信息，我需要显示在物品栏上层，点击物品信息的分解装备按钮弹出“是否确认分解”提示框，需要放在更上层</p>
<p>为了更方便的使用，我把这些统一放在了<code>CanvasManager</code>中</p>
<pre><code class="language-csharp">using System;
using System.Collections;
using UnityEngine;

public enum UI_Layer
{
  BOT,
  MID,
  TOP,
  SYS
}

public class CanvasManager : SingletonMonoBase&lt;CanvasManager&gt;
{

  private Transform _canvas;

  private Transform _bot;
  private Transform _mid;
  private Transform _top;
  private Transform _sys;

  public void Awake()
  {

    DontDestroyOnLoad(gameObject);
    // 加载 Canvas 预制体
    GameObject obj = AssetBundleManager.Instance.Load&lt;GameObject&gt;(&quot;ui&quot;, &quot;Canvas&quot;);
    _canvas = obj.transform;
    // 找到层
    _bot = _canvas.Find(&quot;Bot&quot;);
    _mid = _canvas.Find(&quot;Mid&quot;);
    _top = _canvas.Find(&quot;Top&quot;);
    _sys = _canvas.Find(&quot;Sys&quot;);
    // 加载 EventSystem 预制体
    obj = AssetBundleManager.Instance.Load&lt;GameObject&gt;(&quot;ui&quot;, &quot;EventSystem&quot;);

  }

  /// &lt;summary&gt;
  /// 获取画板
  /// &lt;/summary&gt;
  /// &lt;returns&gt;画板&lt;/returns&gt;
  public Transform GetCanvas()
  {
    return _canvas;
  }

  /// &lt;summary&gt;
  /// 获取层对象
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;layer&quot;&gt;层类型&lt;/param&gt;
  /// &lt;returns&gt;层对象&lt;/returns&gt;
  public Transform GetLayer(UI_Layer layer)
  {

    switch (layer)
    {
      case UI_Layer.BOT:
        return _bot;
      case UI_Layer.MID:
        return _mid;
      case UI_Layer.TOP:
        return _top;
      case UI_Layer.SYS:
        return _sys;
    }

    return null;
  }

  /// &lt;summary&gt;
  /// 给 UI 设置层
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;obj&quot;&gt;GameObject&lt;/param&gt;
  /// &lt;param name=&quot;layer&quot;&gt;层类型&lt;/param&gt;
  public void SetLayer(GameObject obj, UI_Layer layer)
  {
    obj.transform.SetParent(GetLayer(layer));
  }

  /// &lt;summary&gt;
  /// 给 UI 设置层
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;obj&quot;&gt;Transform&lt;/param&gt;
  /// &lt;param name=&quot;layer&quot;&gt;层类型&lt;/param&gt;
  public void SetLayer(Transform obj, UI_Layer layer)
  {
    obj.SetParent(GetLayer(layer));
  }
}
</code></pre>
<h2>BasePanel</h2>
<p>这是面板基类，其他面板需要继承这个类</p>
<p>作用是找到面板下的控件并储存起来、给控件添加事件监听</p>
<pre><code class="language-csharp">using System;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public abstract class BasePanel : MonoBehaviour
{

  private Dictionary&lt;string, Dictionary&lt;int, UIBehaviour&gt;&gt; _ui = new Dictionary&lt;string, Dictionary&lt;int, UIBehaviour&gt;&gt;();

  private void Awake()
  {
    FindChildrenControl&lt;Button&gt;();
    FindChildrenControl&lt;Image&gt;();
    FindChildrenControl&lt;Text&gt;();
    FindChildrenControl&lt;Toggle&gt;();
    FindChildrenControl&lt;Slider&gt;();
    FindChildrenControl&lt;ScrollRect&gt;();
  }

  /// &lt;summary&gt;
  /// 查找组件
  /// &lt;/summary&gt;
  /// &lt;typeparam name=&quot;T&quot;&gt;组件&lt;/typeparam&gt;
  private void FindChildrenControl&lt;T&gt;() where T : UIBehaviour
  {

    string dictName = typeof(T).ToString();

    T[] controls = this.GetComponentsInChildren&lt;T&gt;();

    for (int i = 0; i &lt; controls.Length; i++)
    {
      string name = controls[i].gameObject.name;

      // 添加按钮点击的监听
      if (controls[i] is Button)
      {
        Button btn = controls[i] as Button;
        btn.onClick.AddListener(() =&gt;
        {
          RegisterButtonOnClick(name);
        });
      }
      // 添加单选框值变化的监听
      else if (controls[i] is Toggle)
      {
        Toggle tog = controls[i] as Toggle;
        tog.onValueChanged.AddListener((value) =&gt;
        {
          RegisterToggleOnValueChanged(name, value);
        });
      }

      if (_ui.ContainsKey(name))
      {
        _ui[name].Add(controls[i].GetInstanceID(), controls[i]);
        return;
      }

      _ui.Add(name, new Dictionary&lt;int, UIBehaviour&gt;() { { controls[i].GetInstanceID(), controls[i] } });

    }
  }

  /// &lt;summary&gt;
  /// 按钮点击 注册事件
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;name&quot;&gt;组件名&lt;/param&gt;
  protected void RegisterButtonOnClick(string name)
  {
    ExecutionEvent(name, null, UIEventType.BUTTON_ON_CLICK);
  }
  /// &lt;summary&gt;
  /// 单选框改变 注册事件
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;name&quot;&gt;组件名&lt;/param&gt;
  /// &lt;param name=&quot;value&quot;&gt;单选框的值&lt;/param&gt;
  protected void RegisterToggleOnValueChanged(string name, bool value)
  {
    ExecutionEvent(name, new List&lt;object&gt; { value }, UIEventType.TOOGLE_ON_VALUE_CHANGED);
  }

  /// &lt;summary&gt;
  /// 组件事件的执行函数，用反射调用组件所在的面板的面板类中的方法
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;panelClass&quot;&gt;面板类&lt;/param&gt;
  /// &lt;param name=&quot;name&quot;&gt;组件名&lt;/param&gt;
  /// &lt;param name=&quot;parametors&quot;&gt;参数&lt;/param&gt;
  /// &lt;param name=&quot;eventType&quot;&gt;事件类型&lt;/param&gt;
  protected void ExecutionEvent(string name, List&lt;object&gt; parametors, UIEventType eventType)
  {

    // 参数
    if (parametors == null) parametors = new List&lt;object&gt;();

    // 查找控件名是否带数字
    Match match = Regex.Match(name, @&quot;(.*)_(\d+)$&quot;);
    string num = match.Groups[2].ToString();

    // 事件名
    string eventName = null;
    switch (eventType)
    {
      case UIEventType.BUTTON_ON_CLICK:
        eventName = &quot;OnClick&quot;;
        break;

      case UIEventType.TOOGLE_ON_VALUE_CHANGED:
        eventName = &quot;OnValueChanged&quot;;
        break;
    }

    // 面板类的类型
    Type panelClassType = this.GetType();

    // 方法名
    string methodName = name + eventName;

    // 如果有数字
    if (num != &quot;&quot;)
    {
      // 改变方法名 变成 “控件名+事件名+N”
      methodName = match.Groups[1].ToString() + eventName + &quot;N&quot;;
      // 并传回第几个按钮被点击
      parametors.Add(int.Parse(num));

    }

    // 获取方法
    MethodInfo method = panelClassType.GetMethod(methodName);

    // 如果存在 则让对应的类执行方法
    if (method != null) method.Invoke(this, parametors.ToArray());

  }

}
</code></pre>
<h2>UIManager</h2>
<p>这里是UI管理器，可以缓存面板类、显示隐藏面板、获取面板类</p>
<pre><code class="language-csharp">using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class UIManager&lt;T&gt; : SingletonBase&lt;UIManager&lt;T&gt;&gt; where T : BasePanel&lt;T&gt;
{

  /// &lt;summary&gt;
  /// 面板字典
  /// &lt;/summary&gt;
  /// &lt;typeparam name=&quot;string&quot;&gt;面板类型名称&lt;/typeparam&gt;
  /// &lt;typeparam name=&quot;T&quot;&gt;面板&lt;/typeparam&gt;
  /// &lt;returns&gt;&lt;/returns&gt;
  private Dictionary&lt;string, T&gt; _panel = new Dictionary&lt;string, T&gt;();

  /// &lt;summary
  /// 显示面板
  /// &lt;/summary&gt;
  /// &lt;param name=&quot;layer&quot;&gt;要显示到的层&lt;/param&gt;
  /// &lt;param name=&quot;callback&quot;&gt;回调&lt;/param&gt;
  public void ShowPanel(UI_Layer layer, UnityAction&lt;T&gt; callback)
  {

    string panelName = typeof(T).ToString();

    if (_panel.ContainsKey(panelName))
    {
      _panel[panelName].gameObject.SetActive(true);

      if (callback != null) callback(_panel[panelName]);

      return;
    }

    AssetBundleManager.Instance.LoadAsync&lt;GameObject&gt;(&quot;ui&quot;, panelName, (obj) =&gt;
    {

      Transform parent = CanvasManager.Instance.GetLayer(UI_Layer.BOT);

      switch (layer)
      {
        case UI_Layer.MID:
          parent = CanvasManager.Instance.GetLayer(UI_Layer.MID);
          break;

        case UI_Layer.TOP:
          parent = CanvasManager.Instance.GetLayer(UI_Layer.TOP);
          break;

        case UI_Layer.SYS:
          parent = CanvasManager.Instance.GetLayer(UI_Layer.SYS);
          break;
      }

      obj.transform.SetParent(parent);

      // 设置相对位置和大小

      obj.transform.localPosition = Vector3.zero;
      obj.transform.localScale = Vector3.one;

      RectTransform objRect = obj.transform as RectTransform;

      objRect.offsetMax = Vector2.zero;
      objRect.offsetMin = Vector2.zero;

      T panel = obj.GetComponent&lt;T&gt;();

      if (callback != null)
      {
        callback(panel);
      }

      _panel.Add(panelName, panel);

    });

  }

  /// &lt;summary&gt;
  /// 隐藏面板
  /// &lt;/summary&gt;
  public void HidePanel()
  {

    string panelName = typeof(T).ToString();

    if (_panel.ContainsKey(panelName))
    {
      _panel[panelName].gameObject.SetActive(false);

      return;
    }

  }

  /// &lt;summary&gt;
  /// 获取面板
  /// &lt;/summary&gt;
  /// &lt;returns&gt;面板&lt;/returns&gt;
  public T GetPanel()
  {
    string panelName = typeof(T).ToString();

    if (_panel.ContainsKey(panelName))
    {
      return _panel[panelName];
    }

    return null;
  }

}
</code></pre>
<p>最后新建各种面板类，继承面板基类，上面那个<code>LoginPanel</code>就是其中一个面板</p>
<h1>后言</h1>
<p>至于<code>SingletonBase</code>和<code>SingletonMonoBase</code>是什么，可以去唐老狮那个基础视频中第三节看（免费的）</p>
<p>作用是单例模式和继承<code>MonoBehaviour</code>的单例模式</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Unity 网格建造]]></title>
            <link>https://imba97.com/posts/720</link>
            <guid isPermaLink="false">https://imba97.com/posts/720</guid>
            <pubDate>Sun, 22 Nov 2020 19:00:05 GMT</pubDate>
            <content:encoded><![CDATA[<h1>网格建造</h1>
<p>游戏中常见的建造方式，目前在做的游戏中需要。</p>
<p>但搜出来的教程比较少，我又是个英文盲，没找到合适的方法，于是瞎研究了一个，说实话也挺简单的。</p>
<p>整体感觉还行，演示：</p>
<p><img src="https://imba97.com/uploads/2020/11/unity-grid-build-1.gif" alt=""></p>
<!--more-->
<h1>教程</h1>
<h2>变量</h2>
<p>先定义几个变量</p>
<pre><code class="language-csharp">// 建筑的大小
public Vector2 buildSize;

// 建造区域起始点
public Vector2 startPoint;

// 可建造区域大小
public Vector2 buildArea;

// 建造指示器（变红变绿的那个东西，这个是左上角的那一个方块）
private GameObject buildObj;

// 当前建造指示器的位置
private Vector3 curPosition;

// 允许建造区域，外层字典是 X 内层是 Y
// 这里之前我用的是 Vector2 当作字典的 Key
// 但效率会不会有问题，不太懂，索性改成两层字典
private Dictionary&lt;float, Dictionary&lt;float, bool&gt;&gt; allowBuildArea = new Dictionary&lt;float, Dictionary&lt;float, bool&gt;&gt;();

// 是否允许建造
private bool allowBuild = false;
</code></pre>
<h2>创建网格</h2>
<p>首先根据 可建造区域大小 和 可建造区域起始点 创建网格，放到允许建造区域字典中</p>
<pre><code class="language-csharp">private void Start()
{
  // 构造网格
  for (int gridX = Mathf.CeilToInt(this.startPoint.x); gridX &lt; this.startPoint.x + this.buildArea.x; gridX++)
  {

    for (int gridY = Mathf.CeilToInt(this.startPoint.y); gridY &lt; this.startPoint.y + this.buildArea.y; gridY++)
    {
      // 没有内层字典则创建
      if (!this.allowBuildArea.ContainsKey(gridX))
      {
        this.allowBuildArea.Add(gridX, new Dictionary&lt;float, bool&gt;());
      }
      // 把坐标加进去
      this.allowBuildArea[gridX].Add(gridY, true);
    }

  }
}
</code></pre>
<h2>建造指示器</h2>
<p>然后是创建建造指示器，也就是红绿的方块，标识该坐标是否允许建造。</p>
<p>我这里是先创建一个放在左上角，其他会作为它的子级被创建。</p>
<pre><code class="language-csharp">private void CreateBuildIndicator()
{
  // 建筑大小指示的父级（左上角那个方块）
  this.buildObj = GameObject.CreatePrimitive(PrimitiveType.Cube);
  this.buildObj.transform.localScale = new Vector3(1, 0.1f, 1);
  // 设置 layer，后面发射射线时会把 Build 这一层过滤掉
  this.buildObj.layer = LayerMask.NameToLayer(&quot;Build&quot;);
  buildGroup.Add(this.buildObj);
  // 这里是修改材质球可以设置透明度
  // 参考教程：https://www.cnblogs.com/chinarbolg/p/9601428.html
  SetTransparent(ScriptObjectPool&lt;MeshRenderer&gt;.Instance.GetComponent(this.buildObj).material);

  // 根据建筑大小 创建子级
  for (int buildX = 0; buildX &lt; this.buildSize.x; buildX++)
  {

    for (int buildY = 0; buildY &gt; -this.buildSize.y; buildY--)
    {
      // 0,0 位置是父级 所以不必创建
      if (buildX == 0 &amp;&amp; buildY == 0) continue;

      // 创建子级 后面的设置基本跟父级一样
      GameObject buildChildren = GameObject.CreatePrimitive(PrimitiveType.Cube);
      buildChildren.transform.SetParent(this.buildObj.transform);
      buildChildren.transform.localScale = Vector3.one;
      buildChildren.transform.position = new Vector3(buildX, this.buildObj.transform.position.y, buildY);
      buildChildren.layer = LayerMask.NameToLayer(&quot;Build&quot;);
      SetTransparent(ScriptObjectPool&lt;MeshRenderer&gt;.Instance.GetComponent(buildChildren).material);

      buildGroup.Add(buildChildren);
    }
  }
}
</code></pre>
<h2>发射射线</h2>
<p>然后是发射射线，让建造指示器跟随鼠标</p>
<p>这一步可以直接在 Update() 里做，我这里是放到了一个循环计时器中</p>
<p>为了测试方便，我直接改成了右键创建建造指示器，左键建造</p>
<pre><code class="language-csharp">void Update()
{
  // 左键 &amp;&amp; 允许建造
  if (Input.GetMouseButtonDown(0) &amp;&amp; this.allowBuild)
  {
    Build();
  }
  // 右键
  if (Input.GetMouseButtonDown(1))
  {
    if (this.buildObj != null) return;
    // 创建建造指示器（就是上面那个方法）
    CreateBuildIndicator();
    // 开启循环计时器
    GameUtil.Instance.SetInterval((intervalID) =&gt;
    {
      // 如果建筑指示器为空 则清除当前计时器
      if (this.buildObj == null)
      {
        GameUtil.Instance.RemoveInterval(intervalID);
        return;
      }
      // 鼠标位置的射线
      Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
      RaycastHit hit;
      // 发射射线，过滤掉 Build 层
      if (Physics.Raycast(ray, out hit, Mathf.Infinity, ~(1 &lt;&lt; LayerMask.NameToLayer(&quot;Build&quot;))))
      {
        // 让建造指示器的父级移动到鼠标左上角（让鼠标大致处于整个指示器中心）
        this.buildObj.transform.position = new Vector3(Mathf.Round(hit.point.x - this.buildSize.x / 2), hit.point.y + 0.1f, Mathf.Round(hit.point.z + this.buildSize.y / 2));
        // 如果上次保存的 当前位置 与 本次位置不相等 说明位置变了
        if (this.curPosition != this.buildObj.transform.position)
        {
          // 更新位置
          this.curPosition = this.buildObj.transform.position;
          // 调用位置改变的方法
          OnPositionChanged();
        }

      }

    }, 0.03f);

  }
}

// 当位置改变
private void OnPositionChanged()
{
  // 先允许建造
  this.allowBuild = true;

  // 循环建筑块
  foreach (var buildBlock in buildGroup)
  {
    Vector3 position = buildBlock.transform.position;
    // 判断当前格是否在可建造区域内 并且 该格是可建造的
    if (
      this.allowBuildArea.ContainsKey(position.x)
      &amp;&amp; this.allowBuildArea[position.x].ContainsKey(position.z)
      &amp;&amp; this.allowBuildArea[position.x][position.z] == true
    )
    {
      // 可建造 变成绿色
      // 这句代码等价于 buildBlock.GetComponent&lt;MeshRenderer&gt;().material......
      ScriptObjectPool&lt;MeshRenderer&gt;.Instance.GetComponent(buildBlock).material.color = new Color(0.1f, 1, 0.1f, 0.3f);
    }
    else
    {
      // 否则不允许建造
      this.allowBuild = false;
      // 变为红色
      ScriptObjectPool&lt;MeshRenderer&gt;.Instance.GetComponent(buildBlock).material.color = new Color(1, 0.1f, 0.1f, 0.3f);
    }
  }
}
</code></pre>
<p>左键执行的那个<code>Build()</code>就只是把当前的建造指示器改个颜色放在原位，实际游戏中会创建建筑，删掉建造指示器。</p>
<p>OK，以上就是这个网格建造的分享，告辞。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Unity VSCode 报错 Failed to load project file Assembly-CSharp.csproj]]></title>
            <link>https://imba97.com/posts/705</link>
            <guid isPermaLink="false">https://imba97.com/posts/705</guid>
            <pubDate>Wed, 26 Aug 2020 02:32:29 GMT</pubDate>
            <content:encoded><![CDATA[<p>这次文章探讨一个报错：</p>
<pre><code class="language-bash">[fail]: OmniSharp.MSBuild.ProjectManager

Failed to load project file 'e:\Project\Assembly-CSharp.csproj'

The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks
</code></pre>
<!--more-->
<h1>原因</h1>
<p>重装了系统，所有东西都重新下了，Unity的安装位置也变了</p>
<p>虽然<code>Assembly-CSharp.csproj</code>中有配置路径，但最后发现问题并不是路径问题</p>
<pre><code>&lt;Reference Include=&quot;UnityEngine.AIModule&quot;&gt;
  &lt;HintPath&gt;E:/Unity/2020.1.1f1c1/Editor/Data/Managed/UnityEngine/UnityEngine.AIModule.dll&lt;/HintPath&gt;
&lt;/Reference&gt;
&lt;Reference Include=&quot;UnityEngine.ARModule&quot;&gt;
  &lt;HintPath&gt;E:/Unity/2020.1.1f1c1/Editor/Data/Managed/UnityEngine/UnityEngine.ARModule.dll&lt;/HintPath&gt;
&lt;/Reference&gt;
</code></pre>
<h1>解决</h1>
<h2>报错问题</h2>
<p>其实提示里就说了，没找到<code>.NETFramework,Version=v4.7.1</code>，你可以在 <a href="https://aka.ms/msbuild/developerpacks">https://aka.ms/msbuild/developerpacks</a> 下载对应的版本</p>
<p>但注意这里必须下载<code>Runtime</code>，或者是<code>Runtime</code>和<code>Developer Pack</code>两个都需要，我最开始只下了<code>Developer Pack</code>，结果没用，可能<code>Developer Pack</code>只是一个开发包，这个开发包还是要基于<code>Runtime</code>的</p>
<h2>路径问题</h2>
<p>如果你换了Unity路径，也要解决的，不然你会看到满屏红</p>
<p><img src="https://imba97.com/uploads/2020/08/failed-to-load-project-file-assembly-csharp-1.png" alt=""></p>
<p>因为<code>C#</code>需要的一些引用丢失了，找不到位置所以就报错了</p>
<p>解决方法很简单，重新生成一下项目文件就好</p>
<p>依次点击</p>
<p><code>Edit</code>-&gt;<code>Preferences...</code>-&gt;<code>External Tools</code>-&gt;<code>Regenerate project files</code></p>
<p>就OK了</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Unity VSCode 代码自动补全失效解决方法]]></title>
            <link>https://imba97.com/posts/693</link>
            <guid isPermaLink="false">https://imba97.com/posts/693</guid>
            <pubDate>Fri, 21 Aug 2020 12:17:56 GMT</pubDate>
            <content:encoded><![CDATA[<h1>问题说明</h1>
<p>最开始我新建了一个项目A，不知道设置了什么，这个是有自动补全的。之后又建了几个项目，发现自动补全都失效。</p>
<!--more-->
<h1>搜到的解决方法</h1>
<p>网上各种解决方法，比如更改<code>Assembly-CSharp.csproj</code>中的<code>TargetFrameworkVersion</code>，与自己电脑的<code>.NET Framework</code>版本保持一致，可惜我这里依然不生效。</p>
<p>还有什么下载<code>TargetFrameworkVersion</code>写的<code>.NET Framework</code>版本，我下载安装提示已安装更高版本，不让安装</p>
<p>与我的解决方法比较接近的是在VsCode按<code>Ctrl + Shift + P</code>输入<code>OmniSharp: select project</code>，选择项目文件</p>
<h1>我的解决流程</h1>
<p>通过文件对比，可以看出项目A的<code>.sln</code>文件要多几行内容</p>
<p><img src="https://imba97.com/uploads/2020/08/unity-vscode-1.png" alt=""></p>
<p><strong>第一段不同</strong></p>
<pre><code>Project(&quot;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&quot;) = &quot;Assembly-CSharp&quot;, &quot;Assembly-CSharp.csproj&quot;, &quot;{152411fb-37df-f0ff-193c-c41c1f943a0d}&quot;
EndProject
</code></pre>
<p><code>{}</code>里面那些字符串叫Guid（全球唯一标识），<code>FAE04EC0-301F-11D3-BF4B-00C04F79EFBC</code>表示这个项目是<code>Windows (C#)</code>项目</p>
<p>后面的Guid都是项目的Guid，在项目文件夹中的<code>Assembly-CSharp.csproj</code>，我们能找到一行配置</p>
<pre><code>&lt;ProjectGuid&gt;{152411fb-37df-f0ff-193c-c41c1f943a0d}&lt;/ProjectGuid&gt;
</code></pre>
<p><strong>第二三段不同</strong></p>
<p>这个应该是一些Debug配置，具体我没细研究，有兴趣可以自行搜索<code>.sln</code>文件的配置项都是干什么的</p>
<h1>解决方法</h1>
<p>如果你的<code>.sln</code>文件没有上面所说的这几行，那么问题应该就出在这</p>
<h2>第一步</h2>
<p>复制以下代码，把项目Guid改成你项目的Guid。第一段中的<code>Project(&quot;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&quot;)</code>这个Guid应该可以不变。</p>
<pre><code>Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project(&quot;{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}&quot;) = &quot;Assembly-CSharp&quot;, &quot;Assembly-CSharp.csproj&quot;, &quot;{152411fb-37df-f0ff-193c-c41c1f943a0d}&quot;
EndProject
Global
  GlobalSection(SolutionConfigurationPlatforms) = preSolution
    Debug|Any CPU = Debug|Any CPU
  EndGlobalSection
  GlobalSection(ProjectConfigurationPlatforms) = postSolution
    {152411fb-37df-f0ff-193c-c41c1f943a0d}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
    {152411fb-37df-f0ff-193c-c41c1f943a0d}.Debug|Any CPU.Build.0 = Debug|Any CPU
  EndGlobalSection
  GlobalSection(SolutionProperties) = preSolution
    HideSolutionNode = FALSE
  EndGlobalSection
EndGlobal
</code></pre>
<p>两个文件的图</p>
<p><img src="/uploads/2020/08/unity-vscode-2.png" alt=""></p>
<h2>第二步</h2>
<p>来到VsCode，按<code>Ctrl + Shift + P</code>输入<code>OmniSharp: select project</code></p>
<p>选择项目<code>.sln</code>文件，回车</p>
<p><img src="/uploads/2020/08/unity-vscode-3.png" alt=""></p>
<p>等一会后你会发现VsCode左下角出现<code>.sln</code>文件名，这样就可以了</p>
<p><img src="/uploads/2020/08/unity-vscode-4.png" alt=""></p>
<hr>
<h1>参考</h1>
<blockquote>
<p><a href="https://www.cnblogs.com/zealic/archive/2009/06/25/1511039.html">《Visual Studio 使用的 ProjectTypeGuids》</a><br>
<a href="http://www.jq22.com/textDifference">文本对比工具</a></p>
</blockquote>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[斐讯N1 旁路由]]></title>
            <link>https://imba97.com/posts/682</link>
            <guid isPermaLink="false">https://imba97.com/posts/682</guid>
            <pubDate>Thu, 06 Aug 2020 21:24:23 GMT</pubDate>
            <content:encoded><![CDATA[<h1>烧录镜像</h1>
<p>插入空U盘，打开软件<code>balenaEtcher</code>，选择镜像，选择U盘，Flash!</p>
<p>操作过于简单就不上图了，软件和镜像都整合在云盘里了，地址在下面。</p>
<h1>盒子信息和配置</h1>
<p>首先连接显示器和鼠标，只支持HDMI，开机后可以看到固件版本和IP地址，如果不是网线连接，可以在这个界面连接WiFi。</p>
<p>然后在固件版本上点4下，会提示<code>打开adb</code>，如果关闭了再点4下打开。</p>
<p><img src="https://imba97.com/uploads/2020/08/n1-1.png" alt=""></p>
<blockquote>
<p>图片来自网络</p>
</blockquote>
<h1>连接电脑</h1>
<p>用双公USB接口连接盒子和电脑，需要做一下操作。</p>
<h2>降级</h2>
<p>如果固件版本大于<code>V2.19</code>则需要降级，打开降级工具文件夹，在<code>onekey</code>目录下有个<code>run.bat</code>批处理文件，运行它。</p>
<p>输入2选择N1降级，再输入盒子的IP</p>
<p><img src="/uploads/2020/08/n1-2.png" alt=""></p>
<p>然后根据提示操作，最后盒子会重启。降级后在界面上不会显示，只要降级软件提示<code>Boot分区降级完毕</code>之类的成功的提示就OK了。</p>
<h2>设置U盘启动</h2>
<p>这个跟降级操作类似，使用<code>N1盒子激活U盘启动.bat</code>，输入盒子的IP地址进行自动设置。</p>
<p>下面就会进入U盘中刷入的openwrt中，命令行界面不支持高分辨率屏幕的显示，但我们也不需要用命令。</p>
<h1>旁路由配置</h1>
<h2>IP</h2>
<p>首先需要配置一下IP地址，openwrt默认的地址是<code>192.168.1.1</code>，一般情况下我们可以直接把盒子接在路由器上，输入这个IP地址访问。</p>
<p>但如果你家里的光猫或路由器使用了这个地址，你需要先把盒子跟电脑连接，然后访问<code>192.168.1.1</code>来更改IP</p>
<p>比如我家光猫网关是<code>192.168.1.1</code>，我屋里的路由器网关是<code>192.168.2.1</code>，最终需要把</p>
<h2>AdGuard Home</h2>
<p><strong>1. 设置密码</strong></p>
<p>下面有个生成密码，点添加后会在上面的选项中出现“改变网页登录密码”</p>
<p><img src="/uploads/2020/08/n1-3.png" alt=""></p>
<p>输入明文密码，加载计算模块，然后点计算</p>
<p>复制里面的内容，来到说动设置，替换掉<code>password: </code>后面那些内容</p>
<p><strong>2. 5553重定向</strong></p>
<p>如果只开这个功能，可以设置为“重定向53端口到AdGuardHome”</p>
<p>如果配合SmartDNS使用，需要设置成“作为dnsmasq的上游服务器”</p>
<p><strong>3. AdGuard Home Web</strong></p>
<p>这里是 AdGuard Home 的界面，有数据统计和拦截规则等配置</p>
<p>在 过滤器 DNS封锁清单 中可以配置过滤规则</p>
<p><img src="/uploads/2020/08/n1-4.png" alt=""></p>
<p>这里放几个我在用的规则</p>
<p>名称 | 地址</p>
<ul>
<li>| -<br>
anti-AD | <a href="https://anti-ad.net/easylist.txt">https://anti-ad.net/easylist.txt</a><br>
HalfLifeL | <a href="https://gitee.com/halflife/list/raw/master/ad.txt">https://gitee.com/halflife/list/raw/master/ad.txt</a><br>
乘风-广告 | <a href="https://gitee.com/xinggsf/Adblock-Rule/raw/master/rule.txt">https://gitee.com/xinggsf/Adblock-Rule/raw/master/rule.txt</a><br>
乘风-视频 | <a href="https://gitee.com/xinggsf/Adblock-Rule/raw/master/mv.txt">https://gitee.com/xinggsf/Adblock-Rule/raw/master/mv.txt</a><br>
cjxlist | <a href="https://gitee.com/cjx82630/cjxlist/raw/master/cjx-ublock.txt">https://gitee.com/cjx82630/cjxlist/raw/master/cjx-ublock.txt</a></li>
</ul>
<p>要说一点，视频规则基本没啥用，没法屏蔽视频广告，还是得靠插件，比如：<a href="https://www.adtchrome.com/">广告终结者</a></p>
<p><strong>4. 配合SmartDNS的设置</strong></p>
<p>如果是配合SmartDNS用，你需要把上游DNS服务器设置为SmartDNS</p>
<p>在 这只 DNS设置 中 上游DNS服务器 和 Bootstrap DNS 服务器 都设置为 <code>127.0.0.1:6053</code></p>
<p>然后DNS缓存配置改为0，因为SmartDNS中已经设置了缓存</p>
<p><img src="/uploads/2020/08/n1-5.png" alt=""></p>
<p><strong>5. 我目前在尝试的配置</strong></p>
<p>也是结合SmartDNS，我现在SmartDNS的自定义设置中，添加两行代码</p>
<pre><code class="language-ini">bind :6153 -group cn
bind :6253 -group overseas
</code></pre>
<p>意思是把<code>6153</code>绑定到分组名称叫<code>cn</code>的组上，<code>6253</code>绑定到<code>overseas</code>组</p>
<p>然后在 AdGuard Home 中添加两个上游服务器，再把模式设置成“最快的IP地址”</p>
<p><strong>使用报告</strong></p>
<p>发现不用绑定端口，直接用<code>6053</code>和<code>7913</code>就可以，而且最快IP是这两个端口的，不是SmartDNS中DNS服务器</p>
<p>还有就是请求<code>6053</code>和<code>7913</code>，SmartDNS去找你连接的最快的DNS服务器，而不是当前访问域名最快的DNS服务器，所以还是有些问题，但都写上吧，没啥问题</p>
<h2>SmartDNS</h2>
<p><strong>1. 上游服务器</strong></p>
<p>点击添加，可以自行搜索添加各种国内国外的DNS服务器，国外的DNS服务器设置一下分组名称</p>
<p><strong>2. 基本设置</strong></p>
<p>域名预加载 可以勾选</p>
<p>缓存大小建议在这里设置，我设置的是 5120</p>
<p>域名TTL我这里是 300 和 3600</p>
<p><strong>3. 第二DNS服务器</strong></p>
<p>本地端口 7913</p>
<p>跳过双栈优选 和 跳过cache 打勾</p>
<p>服务器组写刚才设置的国外DNS服务器分组名称</p>
<h1>软件、镜像、工具</h1>
<p><a href="http://static.imba97.com/uploads/AdGuardHome_linux_armv6.tar.gz">AdGuardHome_linux_armv6.tar.gz</a></p>
<p><a href="https://pan.baidu.com/s/1QH08qyRv7P-6QcWEikKOuA">百度云</a> 【g4n4】</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Electron 踩坑之 加入 TypeScript]]></title>
            <link>https://imba97.com/posts/676</link>
            <guid isPermaLink="false">https://imba97.com/posts/676</guid>
            <pubDate>Wed, 05 Aug 2020 01:23:16 GMT</pubDate>
            <content:encoded><![CDATA[<h1>原由</h1>
<p>单纯用<code>javascript</code>其实还是有点局限性，比如枚举，虽然js中可以用对象实现，实际上就是定义一个对象，并让它<code>readonly</code>。但不如用<code>typescript</code>语法清晰明了。还有我一直想接触静态类型语言，包括之前想把<code>Btools</code>插件重制一下，目前也是用的<code>vue+typescript</code>起步了，不过搁置了。</p>
<!--more-->
<h1>加入 TypeScript 的方式</h1>
<h2>直接安装</h2>
<p>因为目前开发的项目是用的<code>electron-vue</code><sup>(<a href="https://github.com/SimulatedGREG/electron-vue">Github</a>)</sup>直接创建的，所以就想直接用包管理器进行安装，装好<code>typescript</code>和<code>ts-loader</code>写了个简单的测试页面，发现是能运行的。</p>
<p>但我完全不清楚<code>tsconfig.json</code>、<code>types</code>等各种配置文件需要如何配置，所以暂时放弃了这种方案。</p>
<h2>Vue 脚手架</h2>
<p>网上查阅各种资料，发现可以直接用<code>vue init</code>创建一个<code>typescript</code>项目，然后用<code>vue add electron-builder</code>把<code>electron</code>的编译器添加到项目里，这种方式非常简单，配置文件都是别人做的现成的。</p>
<p>但问题它不是用的<code>webpack</code>，而是用的<code>vue-cli-service</code>。这个还好，但还有一个它会报错<code>fs.existsSync is not a function</code>，一直没能解决。</p>
<p>网上查的是执行线程问题，<code>fs</code>需要在主线程执行，而渲染线程获取不到。但目前找到的解决方法都没用。</p>
<h2>梅开二度</h2>
<p>最后实在没啥招，于是重回直接安装方案，毕竟也有配置文件可以参考了。于是参考了脚手架生成的各种文件和安装的库，最后总算是把整个框架搭的差不多了。</p>
<p>随之而来的也是各种问题。</p>
<h1>Vue 重复加载问题</h1>
<p>转ts不容易，一路摸黑排错，以下总结一下问题和解决方式</p>
<h2>$attrs is readonly</h2>
<p>这个问题我去网上搜的解决方法，90%都说是重新<code>npm install</code>、<code>npm update</code>就好，10%是说<code>Vue</code>重复加载会导致这个问题</p>
<h2>Unknown custom element</h2>
<p><img src="https://imba97.com/uploads/2020/08/js2ts-1.png" alt=""></p>
<p><code>[Vue warn]: Unknown custom element: &lt;el-row&gt;</code></p>
<p>这个错误是<code>element-ui</code>的，在模板中使用了<code>&lt;el-row&gt;</code>应该被<code>element-ui</code>解析替换成<code>HTML</code>，但实际没有找到<code>element-ui</code>。</p>
<p>还有另一个类似的问题 ↓</p>
<h2>内页 class 中无法获取某些值</h2>
<p>在<code>main.ts</code>的<code>Vue</code>中定义了一些全局的东西，但在内页无法获取</p>
<pre><code class="language-typescript">// 全局常量
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

Vue.appSetup = Vue.prototype.$appSetup = {
  userDataDir: path.join(process.cwd(), 'userData')
}
Vue.use(ElementUI)
</code></pre>
<p>但在内页的<code>calss</code>里无效，内页：</p>
<pre><code class="language-typescript">import { Component, Vue } from 'vue-property-decorator'

@Component
export default class Index extends Vue {
  created() {
    console.log(Vue.appSetup) // undefined
    console.log(this.$appSetup) // undefined
  }
}
</code></pre>
<h1>Vue 重复加载问题的解决</h1>
<p>其实总结以上原因，大致能猜出<code>main.ts</code>中的<code>Vue</code>和内页<code>class</code>中的<code>Vue</code>不是同一个东西</p>
<h2>自己尝试的方式</h2>
<pre><code class="language-typescript">import Vue from 'vue'
import { Component } from 'vue-property-decorator'
</code></pre>
<p>就是<code>Vue</code>导出来源跟<code>main.ts</code>相同，<code>Component</code>装饰器从<code>vue-property-decorator</code>导出</p>
<p>但这种方式依然无济于事，于是各种搜索</p>
<h2>发现可用方案</h2>
<p>最后终于发现了一篇文章：<a href="https://blog.csdn.net/weixin_41320468/article/details/102991716">《关于electron-vue用typescript改写遇到的几个坑》</a><br>
引用：</p>
<blockquote>
<p>在我项目中的App.vue就加入了vue-property-decorator，它将组件改写成类，好了这里第一个大坑，就是 组件必须继承Vue,这个Vue又是从vue-property-decorator中引入的，我们必须在webpack.renderer.config.js配置一个东西:在whiteListedModules中多加一个vue-property-decorator的选项，否则每个组件都从vue-property-decorator引入一个Vue类，main.ts文件又import Vue from ‘vue’,之后webpack打包运行时会有多个vue的实例，Vue别名指向就不明确，vue组件无法识别已经注册好的&lt;router-view ；之后倘若要引入vuex做状态化管理，也无法获取vuex里store存储的数据；vuex的数据改变了，template却无法渲染新的数据显示，而是会报错:&quot;[Vue warn]: $attrs is readonly…found in …&quot;</p>
</blockquote>
<p>原来是需要在<code>webpack</code>配置文件中配置<code>whiteListedModules</code>，这样在打包的时候就不会出现多个<code>Vue</code>实例了</p>
<pre><code class="language-javascript">const whiteListedModules = [
  'vue',
  'vue-property-decorator',
  'vue-class-component'
]

const rendererConfig = {
  // ...
  externals: [
    ...Object.keys(dependencies || {}).filter(
      d =&gt; !whiteListedModules.includes(d)
    )
  ]
  // ...
}
</code></pre>
<p>但我只添加<code>vue-property-decorator</code>依然不行，应该是因为<code>vue-property-decorator</code>的<code>Component</code>是<code>vue-class-component</code>中的，不把<code>vue-class-component</code>写进去应该也会创建多个<code>Vue</code>实例，所以最后就加了个<code>vue-class-component</code>发现没问题。</p>
<h1>types 的问题</h1>
<p><code>typescript</code>有类型检查，比如<code>Vue.appSetup</code>是我自己添加的属性，代码虽然能执行，但语法检查插件和编辑器是会报错的。</p>
<p>所以就需要各种<code>@types</code>，有些主流的软件包会自带类型声明，或者有些可以额外下载，比如<code>@types/node</code>、<code>@types/node-fetch</code>、<code>@types/request</code>等。</p>
<p>但如果是自定义的，需要自己写，<code>vue</code>的官方文档中也说了这个：<span class="vue"><a href="https://cn.vuejs.org/v2/guide/typescript.html#%E5%A2%9E%E5%BC%BA%E7%B1%BB%E5%9E%8B%E4%BB%A5%E9%85%8D%E5%90%88%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8"><strong>《TypeScript 支持 #增强类型以配合插件使用》</strong></a></span></p>
<h1>types 问题的解决</h1>
<p>这里我需要给<code>Vue</code>加一个静态属性<code>appSetup</code>和一个实例属性<code>$appSetup</code>，你需要在项目目录下建一个文件，比如我是在<code>src</code>下建了个文件夹<code>types</code></p>
<p>其实你不建文件夹也无所谓，因为<code>tsconfig.json</code>的<code>include</code>已经包含了<code>src</code>目录下的所有<code>.ts</code>等文件。</p>
<p>我在<code>types</code>文件夹中创建了一个类型声明文件<code>global.d.ts</code>，文件名可以随便起。</p>
<p>照着官方文档，写了</p>
<pre><code class="language-typescript">// 模块补充
declare module 'vue/types/vue' {
  // 全局
  interface VueConstructor {
    appSetup: any
  }
  // 实例
  interface Vue {
    $appSetup: any
  }
}
</code></pre>
<p>于是编辑器就不会报错了，也有代码提示了。</p>
<p>这个地方卡了好久，最后发现自己sb了，没看到官方文档的全局、实例是分开的。我当时只写了其中一个，所以还是各种报错。</p>
<h1>补充知识</h1>
<p>如果<code>typescript</code>中用变量作为对象的下标，会报<code>any</code>类型的问题。</p>
<p>比如有个网络请求，回调返回<code>json</code></p>
<pre><code class="language-typescript">req('...', (json) =&gt; { // 绑定元素“json”隐式具有“any”类型
  console.log(json)
})
</code></pre>
<p>此时你需要声明类型。</p>
<pre><code class="language-typescript">req('...', (json: { [key: string]: any }) =&gt; {
  console.log(json)
})
</code></pre>
<p>即可解决。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Electron 踩坑之 ERR! code ELIFECYCLE]]></title>
            <link>https://imba97.com/posts/672</link>
            <guid isPermaLink="false">https://imba97.com/posts/672</guid>
            <pubDate>Fri, 31 Jul 2020 02:20:57 GMT</pubDate>
            <content:encoded><![CDATA[<h1>事情经过</h1>
<p>执行<code>npm run build</code>报错</p>
<!--more-->
<h1>报错</h1>
<pre><code class="language-bash">npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! project_name@0.0.1 build: `node .electron-vue/build.js &amp;&amp; electron-builder`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the project_name@0.0.1 build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     E:\nodejs\node_cache\_logs\2020-07-30T18_05_50_819Z-debug.log
</code></pre>
<h1>解决</h1>
<p>这个问题应该是编译出现错误，但错误日志并不会明确提示错误信息，错误种类有很多，这里是其中一种。</p>
<p>经过<span style="color:red"><strong>漫长</strong></span>的<code>git reset</code>，终于找到没报错能正常编译的位置，发现这个地方没使用<code>electron-store</code>，于是我又返回最新的一次push，注释掉所有跟<code>electron-store</code>有关的代码，发现果然能编译通过。</p>
<p>搜索<code>electron-store</code>导致的报错信息时发现一篇文章：<a href="https://blog.csdn.net/flytam/article/details/78763992">《electron-store在webpack打包中的Cannot find module &quot;.&quot; 问题》</a></p>
<p>发现问题是我把<code>electron-store</code>装到了开发环境中，也就是安装时用的<code>npm install --save-dev electron-store</code></p>
<p>看文章发现，实际上你需要安装在生产环境 <code>npm install --save electron-store</code></p>
<p>重新安装在生产环境后，编译终于通过了！！！</p>
<p><img src="https://imba97.com/uploads/2020/07/electron-store.png" alt=""></p>
<p><span style="font-size: 30px">真是个大坑，我吐了！</span></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Electron 踩坑之 路径]]></title>
            <link>https://imba97.com/posts/667</link>
            <guid isPermaLink="false">https://imba97.com/posts/667</guid>
            <pubDate>Thu, 30 Jul 2020 00:11:43 GMT</pubDate>
            <content:encoded><![CDATA[<p>用 vue 脚手架创建的 electron-vue 项目</p>
<h1>需求</h1>
<p>程序启动后，打开某个页面会下载网络图片到本地，存到指定文件夹下</p>
<!--more-->
<h1>问题</h1>
<p>electron-vue 可以启动开发版：<code>npm run dev</code> 和生成安装包：<code>npm run build</code></p>
<p>启动开发版时会创建一个 WebpackDevServer，使用的文件默认在 <code>项目/dist/electron</code></p>
<p>但如果打包，安装后默认是在 <code>C:\Users\xxx\AppData\Local\Programs\项目名</code> 下</p>
<p><code>开发</code>和<code>发布</code>路径不统一，使用<code>path</code>和<code>__dirname</code>获取到的路径也不一样，导致开发比较难受</p>
<p>electron-vue 虽然提供了一个<code>static</code>目录，但这个会在打包时被封装到 <code>...前略/项目名/resources/app.asar</code>中</p>
<p>这个<code>文件</code>中还存了所有页面、样式、库、等...</p>
<h1>解决</h1>
<p>使用<code>process.cwd()</code>获取当前进程工作目录</p>
<p>我在<code>main.js</code>中设置了一个全局变量，比如我需要一个用户数据文件夹，当用户登录时里面会存放用户的图片等相关文件</p>
<pre><code class="language-javascript">// 全局常量
Vue.appSetup = Vue.prototype.$appSetup = {
  userDataDir: path.join(process.cwd(), 'userData')
}
</code></pre>
<p>获取到的目录分别是<br>
开发：<code>...前略/项目/userData</code><br>
发布：<code>...前略/项目名/userData</code></p>
<p>使用的时候</p>
<pre><code class="language-javascript">// ...前略
created() {
  // 获取用户文件存放目录路径 拼接 UID，每个用户一个文件夹
  let userDirPath = path.join(
    this.$appSetup.userDataDir,
    this.userInfo.mid.toString()
  )
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[《每天一个JS程序》系列排行]]></title>
            <link>https://imba97.com/posts/665</link>
            <guid isPermaLink="false">https://imba97.com/posts/665</guid>
            <pubDate>Sun, 19 Jul 2020 19:51:51 GMT</pubDate>
            <content:encoded><![CDATA[<h1>排行榜说明</h1>
<p>数据每天会统计一次，数据来源：<a href="https://space.bilibili.com/2198461/channel/detail?cid=132081" target="_blank" rel="noopener noreferrer">《每天一个JS程序》</a>频道。</p>
<p>计分规则：播放 = 1分； 点赞 = 2分； 硬币 = 5分； 收藏 = 3分；</p>
<h1>排行榜</h1>
<div class="point-ranking"></div>
<script>var ranking_script=document.createElement('script');ranking_script.type='text/javascript';ranking_script.src='https://cdn.jsdelivr.net/gh/imba97/js@1.1.0/code/js_ranking.min.js';document.head.appendChild(ranking_script);ranking_script.onload=function(){new Ranking('.point-ranking').getDataWithURL('https://bili.imba97.com/ranking/api.php');document.querySelectorAll('img').forEach(function(element) { element.onclick = false; });}</script>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[生成拖拽安装]]></title>
            <link>https://imba97.com/posts/661</link>
            <guid isPermaLink="false">https://imba97.com/posts/661</guid>
            <pubDate>Sun, 19 Jul 2020 15:13:30 GMT</pubDate>
            <content:encoded><![CDATA[<p>将下面的按钮拖拽到收藏夹：</p>
<p>&lt;a id=&quot;code_a&quot; alt=&quot;生成拖拽安装&quot; href=&quot;javascript:void(0);&quot; cursor-move @click.stop.prevent&gt;生成拖拽安装</a></p>
<!--more-->
<p>再去选择你喜欢的程序：</p>
<p><a href="https://github.com/imba97/js">Github</a></p>
<p><a href="https://gitee.com/imba97/js">Gitee</a></p>
<p>对应的视频：<a href="https://www.bilibili.com/video/BV1Z5411Y7ko">BV1Z5411Y7ko</a></p>
<!-- TODO: 生成 a 标签 href -->
<!-- eslint-disable -->
<pre><code class="language-javascript">javascript:if(document.querySelector(&quot;.koe_42_a&quot;)===null){var koe_is_github=/github/.test(window.location.href);var koe_42_code=koe_is_github?document.querySelector(&quot;#read-only-cursor-text-area&quot;).value:document.querySelector(&quot;.highlight .line&quot;).innerText.replace(/[\r\n]$/,&quot;&quot;);var koe_42_style=document.createElement(&quot;style&quot;);koe_42_style.innerText=&quot;.koe_42_a { position:absolute;top:7px;left:200px;width:100px;height:30px;line-height:30px;text-align:center;background-color:#24aee6;cursor:move;font-size:0;border-radius:5px;text-decoration:none !important;z-index:999999; } .koe_42_a::before { content:'拖到收藏夹';font-size:16px;color:#FFF; }&quot;;document.head.appendChild(koe_42_style);var koe_42_a=document.createElement(&quot;a&quot;);koe_42_a.setAttribute(&quot;class&quot;,&quot;koe_42_a&quot;);var koe_42_data=JSON.parse(/\/\*(\{.*\})\*\//.exec(koe_42_code)[1]);koe_42_a.innerText=koe_42_data.name;koe_42_a.href=&quot;javascript:&quot;+koe_42_code;if(koe_is_github){koe_42_a.setAttribute(&quot;style&quot;,&quot;position: unset;&quot;);document.querySelector('[data-testid=&quot;latest-commit&quot;]').appendChild(koe_42_a)}else{document.querySelector(&quot;.file_holder&quot;).appendChild(koe_42_a)}}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[红色警戒素材转图片]]></title>
            <link>https://imba97.com/posts/651</link>
            <guid isPermaLink="false">https://imba97.com/posts/651</guid>
            <pubDate>Wed, 24 Jun 2020 11:04:30 GMT</pubDate>
            <content:encoded><![CDATA[<h1>导出素材</h1>
<p>软件：<strong>XCCMixer</strong>，用它打开<code>红警目录/ra2.mix</code></p>
<p>常用素材目录（也不一定都是）</p>
<!--more-->
<p><code>conquer.mix</code> 特效（如核弹爆炸、超时空转换）、活的（有男滴有女滴还有尼玛小动物）<br>
<code>isogen.mix</code> 战役建筑<br>
<code>isosnow.mix</code> 战役建筑（五角大楼）、中立建筑（楼房、路标、旗子）、游戏建筑的建造动画</p>
<p>双击素材预览，提示：可以用键盘操作，<strong>上下键</strong>选择素材，<strong>回车键</strong>预览，<strong>ESC键</strong>关闭预览，<strong>退格键</strong>返回上层菜单</p>
<p>找到合适的素材后按右键，选择“<strong>释放</strong>”，会导出一个<code>.shp</code>的文件</p>
<h1>转图片</h1>
<p>软件：<strong>SHP-Builder</strong>，打开<code>.shp</code>文件</p>
<p>选择<code>File</code> =&gt; <code>Export</code> =&gt; <code>SHP -&gt; Images</code></p>
<p><img src="https://imba97.com/uploads/2020/06/ra2-1.png" alt=""></p>
<p>起个名字，点保存，会弹出这样一个框</p>
<p><img src="/uploads/2020/06/ra2-2.png" alt=""></p>
<p><strong>Loop Type 动画循环类型</strong></p>
<ul>
<li>No Loop 不循环</li>
<li>Loop Continusly 循环</li>
</ul>
<p><strong>Shadows 阴影</strong></p>
<ul>
<li>All 全部</li>
<li>None 无</li>
<li>Merge info frames 合并</li>
</ul>
<p><strong>Transparency 透明</strong></p>
<ul>
<li>Use transparency 使用透明</li>
</ul>
<p><strong>Zoom 缩放</strong></p>
<h1>颜色问题</h1>
<p>有些时候颜色是会出问题的，常见是特效部分，比如下面这个核爆素材</p>
<p><img src="/uploads/2021/04/fe2d0001e0beb1837d45.gif" alt=""></p>
<p>解决方法是调整配色方案</p>
<p><code>Palette</code> =&gt; <code>Red Alert 2</code>，红警的话选这个</p>
<p><img src="/uploads/2020/06/ra2-3.png" alt=""></p>
<p>里面名字如果能对应你这个素材的目录，那就选名字对应的，如果没有可以随便试试</p>
<p>于是就正常了</p>
<p><img src="/uploads/2021/04/fe48000200f514d7ecf1.gif" alt=""></p>
<p>第一张图导出选项中 <strong>Shadows</strong> 是 <code>Merge info frames</code><br>
第二张是 <code>None</code></p>
<p>也很明显能看出动画的区别</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[一键英译中]]></title>
            <link>https://imba97.com/posts/644</link>
            <guid isPermaLink="false">https://imba97.com/posts/644</guid>
            <pubDate>Fri, 12 Jun 2020 19:28:58 GMT</pubDate>
            <content:encoded><![CDATA[<h1>服务端</h1>
<p>如果到时候用的人多了我可能会停掉。</p>
<!--more-->
<p>以下源码，可自行复制放在自己的服务器上，再把上面<code>javascript</code>中的<code>https://bili.imba97.com/translate.php</code>改成你的。</p>
<pre><code class="language-php">&lt;?php

header('content-type:application/json;charset=utf-8');
header('Access-Control-Allow-Origin: *');

function curlRequest($ipUrl) {

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $ipUrl);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13');
    $resultData = curl_exec($curl);
    curl_close($curl);

    return $resultData;

}

$kw = rawurlencode($_GET['kw']);

$data = curlRequest('https://translate.google.cn/translate_a/single?client=gtx&amp;dt=t&amp;dj=1&amp;ie=UTF-8&amp;sl=auto&amp;tl=zh_CN&amp;q=' . trim($kw));

echo $data;
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[点点点]]></title>
            <link>https://imba97.com/posts/641</link>
            <guid isPermaLink="false">https://imba97.com/posts/641</guid>
            <pubDate>Sat, 06 Jun 2020 21:02:33 GMT</pubDate>
            <content:encoded><![CDATA[<p><button id="btn" style="margin: 0 auto; display: block;">0</button></p>
<script>var num = 0;var btn = document.querySelector('#btn');btn.addEventListener('click', function() { btn.innerText = ++num; })</script>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[懂王 👐]]></title>
            <link>https://imba97.com/posts/632</link>
            <guid isPermaLink="false">https://imba97.com/posts/632</guid>
            <pubDate>Sun, 31 May 2020 22:36:35 GMT</pubDate>
            <content:encoded><![CDATA[<p>今天起，你就是懂王 👐</p>
<!--more-->
<h1>使用方法</h1>
<p><strong>第一种方式</strong>，直接把下面这个拖到收藏夹。然后在<strong>任意页面</strong>点击使用</p>
<style>#code_a{display:block;width:200px;height:200px;line-height:30px;color:#FFF;-webkit-text-stroke:.8px #000;text-align:center;background: transparent url(//imba97.com/uploads/2020/05/knowking.jpg) no-repeat scroll 0 0 / 100% auto;cursor:move;font-size:0;border:1px #000 solid;border-radius:5px;}#code_a::before{display:none;}#code_a::after{content:'拖到收藏夹';font-size:20px;font-weight:700;}#code_a:hover{text-decoration:none;color:#CCC;}</style>
<p><a id="code_a" alt="懂王👐" href="javascript:void(0);">懂王👐</a></p>
<p>图片来自：<a href="https://www.bilibili.com/video/BV1hf4y127HD">BV1hf4y127HD</a> 封面</p>
<p><strong>第二种方式</strong>，复制以下<code>javascript</code>代码，在浏览器收藏夹右键-&gt;添加网页-&gt;起个名字-&gt;在网址里粘贴代码</p>
<!-- eslint-disable -->
<pre><code class="language-javascript">javascript:if(typeof koe_knowking_clicked==='undefined'){var koe_knowking_clicked=true;if(typeof koe_knowking_get_select_text==='undefined'){function koe_knowking_get_select_text(){var resultText=window.getSelection().toString();if(selecter!=null&amp;&amp;koe_knowking_trim(selecter)!=&quot;&quot;){resultText=selecter}else if(typeof document.selection!=='undefined'){var selecter=document.selection.createRange();var s=selecter.text;if(s!=null&amp;&amp;functionkoe_knowking_trim(s)!=&quot;&quot;){resultText=s}}return resultText}function koe_knowking_trim(){return this.replace(/(^\s*)|(\s*$)/g,&quot;&quot;)}}var koe_knowking_selected_text=koe_knowking_get_select_text();if(koe_knowking_selected_text!==''&amp;&amp;document.querySelector('#koe_knowking_audio')===null){if(typeof koe_knowking_msg==='undefined'){var koe_knowking_msg=document.createElement('p');koe_knowking_msg.setAttribute('style','position:fixed;top:0;left:0;padding:5px 10px;height:30px;line-height:30px;text-align:center;background-color:#000;color:#FFF;font-size:26px;z-index:99999999');document.body.appendChild(koe_knowking_msg)}else{koe_knowking_msg.style.display='block'}koe_knowking_msg.innerText='懂王祈祷中🙏';if(typeof koe_knowking_style==='undefined'){var koe_knowking_style=document.createElement('style');koe_knowking_style.innerText='.koe_knowking_content,.koe_knowking_image_1,.koe_knowking_image_2{ position: fixed; left: 50%; transform: translate(-50%,-50%); -webkit-transform: translate(-50%,-50%); -moz-transform: translate(-50%,-50%); z-index: 99999999; display: none; } .koe_knowking_content { top: calc(50% + 200px); padding: 10px; font-size: 20px; color: #FFF; background-color: #000; } .koe_knowking_image_1,.koe_knowking_image_2{ top: 50%; height: 200px; }';document.head.appendChild(koe_knowking_style);var koe_knowking_content=document.createElement('div');koe_knowking_content.setAttribute('class','koe_knowking_content');document.body.appendChild(koe_knowking_content);var koe_knowking_image_1=document.createElement('img');var koe_knowking_image_2=document.createElement('img');koe_knowking_image_1.setAttribute('src','https://pic.downk.cc/item/5ed520b8c2a9a83be55a3848.png');koe_knowking_image_2.setAttribute('src','https://pic.downk.cc/item/5ed520b8c2a9a83be55a3843.png');koe_knowking_image_1.setAttribute('class','koe_knowking_image_1');koe_knowking_image_2.setAttribute('class','koe_knowking_image_2');document.body.appendChild(koe_knowking_image_1);document.body.appendChild(koe_knowking_image_2);var koe_knowking_timer=null;var koe_knowking_is_1=true}var koe_knowking_xhr=new XMLHttpRequest();koe_knowking_xhr.open('GET','https://bili.imba97.cn/baike.php?kw='+koe_knowking_selected_text,true);koe_knowking_xhr.onreadystatechange=function(){if(koe_knowking_xhr.readyState==4&amp;&amp;koe_knowking_xhr.status==200||koe_knowking_xhr.status==304){var json=JSON.parse(koe_knowking_xhr.responseText);var audio_url='http://tts.baidu.com/text2audio?lan=zh&amp;ie=UTF-8&amp;spd=5&amp;text=没有人比我更懂，懂了个寂寞';if(json.status===1){audio_url='http://tts.baidu.com/text2audio?lan=zh&amp;ie=UTF-8&amp;spd=5&amp;text=没有人比我更懂'+koe_knowking_selected_text+'，'+json.content;var audio=document.createElement('audio');audio.setAttribute('id','koe_knowking_audio');audio.setAttribute('autoplay','');var source=document.createElement('source');source.setAttribute('src',audio_url);audio.appendChild(source);audio.addEventListener('canplaythrough',function(){koe_knowking_msg.style.display='none';koe_knowking_content.innerText=json.content;koe_knowking_content.style.display='block';koe_knowking_image_1.style.display='block';koe_knowking_timer=setInterval(function(){if(koe_knowking_is_1){koe_knowking_image_1.style.display='none';koe_knowking_image_2.style.display='block'}else{koe_knowking_image_1.style.display='block';koe_knowking_image_2.style.display='none'}koe_knowking_is_1=!koe_knowking_is_1},300)});audio.addEventListener('ended',function(){koe_knowking_clicked=undefined;document.body.removeChild(audio);koe_knowking_content.style.display='none';koe_knowking_image_1.style.display='none';koe_knowking_image_2.style.display='none';koe_knowking_is_1=true;clearInterval(koe_knowking_timer);koe_knowking_timer=null});document.body.appendChild(audio)}else{koe_knowking_msg.innerText=json.content}}};koe_knowking_xhr.send()}else{koe_knowking_clicked=undefined}}void(0);
</code></pre>
<p>第二种方式有可能会被浏览器自动删掉<strong>最前面的</strong><code>javascript:</code>，粘贴上后检查一下，没有的话需要自己手动加一下</p>
<script>make_js();</script>
<h1>服务端</h1>
<p>这里需要PHP去获取百度百科的页面，目前是放在我服务器上的，如果到时候用的人多了我可能会停掉。</p>
<p>以下源码，可自行复制放在自己的服务器上，再把上面<code>javascript</code>中的<code>https://bili.imba97.com/baike.php</code>改成你的。</p>
<pre><code class="language-php">&lt;?php

header('content-type:application/json;charset=utf-8');
header('Access-Control-Allow-Origin: *');

function curlRequest($ipUrl) {

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $ipUrl);
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
    curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.27 Safari/525.13');
    $resultData = curl_exec($curl);
    curl_close($curl);

    return $resultData;

}

$kw = rawurlencode($_GET['kw']);

$data = curlRequest('https://baike.baidu.com/item/' . $kw);

$resultJsonArray = array(
    'status'  =&gt; 0,
    'content' =&gt; '无百科或其他错误'
);

preg_match_all('/百度百科错误页/', $data, $pregError);

if($pregError[0][0] !== NULL) {
    $resultJsonArray['status'] = 1;
    $resultJsonArray['content'] = '这个我是懂的，但我没有查到，这很可能是C国搞的鬼。';
    die(json_encode($resultJsonArray));
}

preg_match_all('/&lt;meta name=&quot;description&quot; content=&quot;(.*)&quot;/', $data, $pregData);

if(isset($pregData) &amp;&amp; $pregData[1][0] !== NULL) {
    $resultJsonArray['status']  = 1;
    $resultJsonArray['content'] = $pregData[1][0];
}

die(json_encode($resultJsonArray));
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[用JS把网页统统抬走]]></title>
            <link>https://imba97.com/posts/628</link>
            <guid isPermaLink="false">https://imba97.com/posts/628</guid>
            <pubDate>Fri, 29 May 2020 17:31:53 GMT</pubDate>
            <content:encoded><![CDATA[<p>用JS做了个黑人抬棺的搞怪类程序，可以把网页统统抬走。</p>
<!--more-->
<h1>使用方法</h1>
<p><strong>第一种方式</strong>，直接把下面这个拖到收藏夹。然后在<strong>任意页面</strong>点击使用</p>
<p><a id="code_a" alt="当场抬走" href="javascript:void(0);">当场抬走</a></p>
<p><strong>第二种方式</strong>，复制以下<code>javascript</code>代码，在浏览器收藏夹右键-&gt;添加网页-&gt;起个名字-&gt;在网址里粘贴代码</p>
<!-- eslint-disable -->
<pre><code class="language-javascript">javascript:if(document.querySelector('#koe_msg')===null){if(document.querySelector('#koe_video')===null){var koe_msg=document.createElement('p');koe_msg.innerText='正在去世';koe_msg.setAttribute('id','koe_msg');koe_msg.setAttribute('style','position:fixed;top:0;left:0;width:130px;height:30px;line-height:30px;text-align:center;background-color:#000;color:#FFF;font-size:26px;z-index:99999999');document.body.appendChild(koe_msg);var koe_played=true;var koe_video=document.createElement('video');koe_video.setAttribute('id','koe_video');koe_video.setAttribute('autoplay','');koe_video.setAttribute('name','media');koe_video.addEventListener('canplaythrough',function(){koe_msg.remove();document.body.setAttribute('style','filter: grayscale(100%); -moz-filter: grayscale(100%); -o-filter: grayscale(100%); -webkit-filter: grayscale(100%);')});koe_video.addEventListener('pause',function(){document.body.setAttribute('style','');koe_played=false});koe_video.addEventListener('play',function(){document.body.setAttribute('style','filter: grayscale(100%); -moz-filter: grayscale(100%); -o-filter: grayscale(100%); -webkit-filter: grayscale(100%);');koe_played=true});var koe_source=document.createElement('source');koe_source.setAttribute('src','https://onedrive.gimhoy.com/1drv/aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBcDVHUVdqS1ViZm5nLWwtTk5nMVlSVlFJRThJclE/ZT1HbFZ5VHE=.mp3');koe_source.setAttribute('type','audio/mpeg');koe_video.appendChild(koe_source);document.body.appendChild(koe_video)}else{if(koe_played){document.querySelector('#koe_video').pause()}else{document.querySelector('#koe_video').play()}}}void(0);
</code></pre>
<p>第二种方式有可能会被浏览器自动删掉<strong>最前面的</strong><code>javascript:</code>，粘贴上后检查一下，没有的话需要自己手动加一下</p>
<script>make_js();</script>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[永久封禁]]></title>
            <link>https://imba97.com/posts/604</link>
            <guid isPermaLink="false">https://imba97.com/posts/604</guid>
            <pubDate>Sun, 17 May 2020 12:33:02 GMT</pubDate>
            <content:encoded><![CDATA[<p>2020年5月17日11时左右，帐号被永久封禁了。</p>
<p>一个人终究还是敌不过网络这些黑暗的存在。</p>
<p><img src="https://imba97.com/uploads/2020/05/yongfeng-1.png" alt=""></p>
<!--more-->
<p>7年的热爱，感觉瞬间被泼了一盆冷水。但我也比较冷静，没了就没了吧。</p>
<h1>我被威胁了</h1>
<p>视频：<a href="https://www.bilibili.com/video/BV1nt4y1y7KW">BV1nt4y1y7KW</a></p>
<p>我的视频和文章没任何问题，如果你们觉得侵犯到了你们的权益，那你们可以去B站和网监局举报。我认为我做的没问题。</p>
<p>公道自在人心。</p>
<h1>主站解封</h1>
<p><code>2020-06-01 01:50</code> 首先，儿童节快乐。</p>
<p>1点40左右 <a href="https://space.bilibili.com/5790446">硫酸钡Barite</a> 大佬说他解封了，于是我也去申诉了，然后光速解封！</p>
<img width="300" src="/uploads/2020/05/yongfeng-14.jpg">
<p>最上面那个图和文字我就留着不删了，毕竟是我当时被永久封禁的感受。</p>
<h1>直播间减刑</h1>
<p><code>2020-06-03</code> 因为直播间依然是永久封禁，所以我又去联系了客服，问能不能给解封。最后客服说因为查证还需要时间，让我先提交工单，等有结果了再告诉我。过了一会我在整理视频素材的时候，看了一眼直播间，发现已经从<strong>永久封禁</strong>变成了<strong>到2020-06-13解封</strong>。</p>
<p><code>2020-06-06</code> 反馈的工单回复了，依然是要等封禁结束，不能提前解封。</p>
<p><img src="/uploads/2020/05/yongfeng-15.png" alt=""></p>
<h1>最新进展</h1>
<h2>工单</h2>
<p><code>2020-05-17 16:11</code> 工单终于回复，但还是一样，对<font color="red"><strong>“恶意举报”</strong></font>只字未提。</p>
<p><img src="/uploads/2020/05/yongfeng-4.png" alt=""></p>
<p><code>2020-05-17 16:41</code> 工单第二次回复，其实挺令人伤心的。我只想说，我提交工单的<font color="red"><strong>意义</strong></font>是什么，是为了被说<font color="red"><strong>“你回忆一下”</strong></font>？</p>
<p><img src="/uploads/2020/05/yongfeng-5.png" alt=""></p>
<p><code>2020-05-17 16:54</code> 放弃工单</p>
<p><img src="/uploads/2020/05/yongfeng-6.png" alt=""></p>
<h2>我最后的评论被举报</h2>
<p>我让朋友帮我发的评论，<strong>原文：</strong></p>
<blockquote>
<p>各位好，我是imba久期，借号最后发声。<br>
这个号被永久封禁了。具体情况写在了博客里：/posts/604<br>
感谢支持，有缘再见。勿回。</p>
</blockquote>
<p>我不知道有啥问题。</p>
<img width="400" src="/uploads/2020/05/yongfeng-7.png">
<p>看来是有人躲在暗处，持续关注我的视频。</p>
<p>感谢有人帮我发评论，你可能会被举报。虽然不至于被封号。</p>
<p>建议不要发这个了，我也不会置顶并且会在24小时后删除。能帮我转发就非常感谢了，我也不能连累别人。</p>
<p>目前被举报删评：<font color="red"><strong>2</strong></font>条，被我手动删除：<font color="red"><strong>1</strong></font>条。</p>
<h2>被捕捉</h2>
<p><code>2020-05-18 23:00</code>左右，被 <a href="https://space.bilibili.com/367543617"><strong>永封账号收集姬</strong></a> 捉到。</p>
<p><img src="/uploads/2020/05/stamp-1.jpg" alt=""></p>
<h2>文章快照</h2>
<p><code>2020-05-19 01:03</code>避免本站被恶意举报，虽然不知道有没有用，姑且留了快照：<a href="https://web.archive.org/web/*///imba97.com/archives/604">archive</a>。并且本文章每次更新都会存一份快照。</p>
<h2>只能注销</h2>
<p>每个身份证只能绑定一个号，并且不能解绑，只能注销账号。</p>
<p><img src="/uploads/2020/05/yongfeng-8.png" alt=""></p>
<h2>大会员无法退款</h2>
<p>因为16年大会员，注销我不成王八了么，我就去问了问大会员能不能转到另一个号或者退款，结果是<font color="red"><strong>都不行</strong></font></p>
<p><img src="/uploads/2020/05/yongfeng-9.png" alt=""></p>
<p><img src="/uploads/2020/05/yongfeng-10.png" alt=""></p>
<p><img src="/uploads/2020/05/yongfeng-11.png" alt=""></p>
<h2>NGA帖子被锁定</h2>
<p><code>2020-05-21 09:34</code> 左右，<a href="https://nga.178.com/read.php?tid=21831581">帖子</a>被锁定，存活约13小时。</p>
<h2>打开了“关于”页面</h2>
<p>因为“关于”页面有很多我的其他平台，避免被别人说“引导流量”之类的，封禁期间关闭了这个页面，现在解封了就重新开放浏览。</p>
<h1>封号原因</h1>
<p>原本觉得可能是<a href="https://www.bilibili.com/video/BV1BC4y1H71R">《我被栽赃举报直播间封禁一个月！》</a>视频的评论或私信，我回复了很多“好奇违禁图内容是什么”。（当然也是比较隐晦的回的）</p>
<p>后来问了一下原因是“直播违禁内容同步主站永久封禁”，就是上面的客服截图。</p>
<p>永封是能登陆的，也可以点赞、收藏视频，但没法评论、发弹幕、投币，也没法给评论点赞、回复，所以只能看到大家的点赞、评论消息但没法回复，这里统一回复：<strong>感谢支持</strong>。<br>
此号就看番用吧 233，毕竟还有个几年大会员。</p>
<img width="500" src="/uploads/2020/05/yongfeng-2.jpg">
<p>（阴阳怪气：没错放这个图的原因就是，哎呀你看我都充了16年大会员了，你看我多么爱B站，结果被永久封禁了）</p>
<p>不管你信不信，我<font color="red"><strong>并没</strong></font>这么想 ↑，我只能尝试理解官方的做法，然后反思自己。也没多高尚，我就这性格。</p>
<h1>为什么玩这个你画我猜</h1>
<p><code>2020-05-12 21:08</code> 本来跟朋友玩的是QQ游戏的你画我猜。(点击图片放大)</p>
<img width="500" src="/uploads/2020/05/yongfeng-12.png">
<p><code>2020-05-12 21:09</code> 中途朋友掉线，我俩就决定换一个玩吧，后来就找到了这个。也有全程录播。</p>
<img width="500" src="/uploads/2020/05/yongfeng-13.png">
<h1>被恶意举报的其他受害人</h1>
<table>
<thead>
<tr>
<th>UP主</th>
<th>被封</th>
<th>解封</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://space.bilibili.com/5790446">硫酸钡Barite</a></td>
<td>直播间+主站</td>
<td>直播间+主站</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/604245">东风谷舞以</a></td>
<td>直播间+主站</td>
<td>直播间+主站</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/8014651">朱九儿Official</a></td>
<td>直播间+主站</td>
<td>直播间+主站</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/6970600">天使垣Official</a></td>
<td>直播间+主站</td>
<td>直播间+主站</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/4689550">莓莉雅Melia_channel</a></td>
<td>直播间</td>
<td>直播间</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/165621">九条海Official</a></td>
<td>直播间</td>
<td>直播间</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/33060681">飞翔的海豚Channel</a></td>
<td>直播间</td>
<td>直播间</td>
</tr>
<tr>
<td><a href="https://space.bilibili.com/503770403">Bob_Cattier华仔</a>（小号）</td>
<td>直播间+主站</td>
<td><font color="#CCC">UP自行注销帐号</font></td>
</tr>
</tbody>
</table>
<p>数据来自：</p>
<ul>
<li><a href="https://nga.178.com/read.php?tid=21804074">《关于近期b站&lt;你画我猜&gt;游戏直播事故致多名国v账号永封事件的深入挖掘》</a>，无账号可查看：<a href="http://ftp462122.host508.900303.top/images/blog/nga_178_com_21804074.png">截图</a></li>
<li><a href="https://www.bilibili.com/video/BV1cK4y1471R">BV1cK4y1471R</a></li>
</ul>
<h1>此次事件影响下我帐号的数据</h1>
<p><code>2020-05-16</code> 至 <code>2020-05-31</code> 涨粉：<font color="red"><strong>404</strong></font>（not found 哈哈哈）</p>
<p>截止到<code>2020-05-31</code>维权视频累计收益：<font color="red"><strong>106.37</strong></font>（ &gt;=1BC4y1H71R ? 播放量 : ÷视频数 ）</p>
<p>截止到<code>2020-05-31</code>硬币数<font color="red"><strong>123.5</strong></font>，以前一直是缺币状态，现在终于能补交一下了。</p>
<p>解释一下<font color="red">（ &gt;=1BC4y1H71R ? 播放量 : ÷视频数 ）</font>这个是什么意思，是因为有3位UP帮我做了视频，所以想分摊一下收益。括号里是一个叫“三目运算符”的东西，作为分摊规则，换成人话：如果任意一个UP的视频播放量 大于等于 我视频 <a href="https://www.bilibili.com/video/BV1BC4y1H71R">BV1BC4y1H71R</a> 的播放量，那么就按播放量的比例分配，否则就除以视频数。</p>
<p>截止到我解封，<code>&gt;=1BC4y1H71R</code>的返回值为<code>false</code>，故按<code>else</code>中的“÷视频数”分配。</p>
<h1>感谢</h1>
<p><code>2020-05-20 17:00</code> 左右，播放量激增。后来知道是谁了，非常感谢。</p>
<hr>
<p><code>2020-05-23 21:00</code> 左右，又有一波小高峰。发现了来源，有两位UP做了相关视频。</p>
<p><a href="https://www.bilibili.com/video/BV1NA411q7g5">《望转发！有人疑似有组织的恶意举报UP，导致8位UP直播间永封！》</a></p>
<p><a href="https://www.bilibili.com/video/BV1nV411C7dv">《望转发！有人疑似有组织的恶意举报各UP，导致8位UP直播间永封！》</a></p>
<p>非常感谢两位UP，顺便更正一下，被封时间是<code>21:51</code>，<code>22:36</code>是我发现了被封禁。</p>
<hr>
<p>另说一下，看到之前点赞的观众好多是Minecraft游戏粉丝，应该也是被大佬转了，非常感谢。</p>
<p>也感谢所有关心此事的朋友们。</p>
<h1>以后的打算</h1>
<p>可能还会建一个号，继续发发视频。生活乏味，爱好还是要继续，无论有没有粉丝、有没有关注都无所谓。</p>
<p>新号并不会用旧号去搞宣传，如果你能发现我我会很开心。</p>
<p>这个世界太乱，我们有缘再见。</p>
<h1>最后</h1>
<p>当然，万一能解封还是解封比较好，这方面我也会在不为难官方的前提下努努力。</p>
<p>我在想，如果被永封的不是我这么佛系的一个人，而是容易动粗，那说不定就让这些人的阴谋得逞了。</p>
<p><img src="/uploads/2020/05/yongfeng-3.jpg" alt=""></p>
<p>我的祖国伟大是我根深蒂固信念，日常中也能感受到，就比如中国控制疫情的速度和为病人免费治疗，除了中国没任何一个国家能做到！</p>
<p>所以心态不会被这种小事击溃，<font color="red"><strong>我爱中国！</strong></font></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[我被栽赃举报直播间封禁一个月！]]></title>
            <link>https://imba97.com/posts/602</link>
            <guid isPermaLink="false">https://imba97.com/posts/602</guid>
            <pubDate>Sat, 16 May 2020 13:27:38 GMT</pubDate>
            <content:encoded><![CDATA[<h1>视频</h1>
<p><a href="https://www.bilibili.com/video/BV1BC4y1H71R/">https://www.bilibili.com/video/BV1BC4y1H71R/</a></p>
<h1>事情经过</h1>
<p>直奔主题，首先是事情经过。</p>
<p>2020年5月13日20时41分我在跟朋友玩一个你画我猜的游戏并直播，本着友好互动的想法我把游戏房间地址放在了直播屏幕上，却被别有用心的人利用并栽赃我，导致我的直播间被封禁一个月。</p>
<p>开始是有一名路人在当晚21时40分进入游戏房间，先跟我们愉快玩了几回合后在21时48分，轮到他画的时候，先是空白画面。倒计时条一点点下降，然后突然出现一个画面。</p>
<p>下面是完整视频。</p>
<p>那张画突然出现，看到之后我愣了2秒说了个“嚯？”，可能这路人是鼠标写得字，并不是很好认</p>
<p>举两个例子大概这样子，这个是天空“天”，这个是空中“中”</p>
<p>我看了3秒才明白过来，然后切到了别的页面，这之后我又关闭游戏房间，这个网页弹出提示框又露了1秒的画面。展现违规画面共计6秒。</p>
<p>为避免别有用心的人猜测，我先声明一下，视频中有一声“哈！”，听语气也能听出来那个并不是笑，只是一时语塞的一声感叹。</p>
<p>当时发现直播间被封已经是22时36分，被封的话OBS是不会断的，我是看手机发现了系统消息说45分钟前直播间被封了，也就是21时51分。</p>
<h1>推测原因</h1>
<p>接下来是个人推测，因为一名路人玩家在21时48分画了违禁图，直播间被封时间点是21时51分，前后只有3分钟。</p>
<p>又因为21时16分后就没什么人了，当天这次直播的平均观看时长也只有4分钟。</p>
<p>所以我的初步推测是这名路人首先是看到了我在直播间玩这个游戏，并且游戏房间是公开的，于是他就进入了房间，最后在21时48分用一些技术手段让图瞬间出现，这里为什么说是“技术手段”呢，因为我朋友看到的也是突然出现的，这个游戏是如果你网速出现了延迟，但是它那个图还是会一笔一笔显示的，不会突然出现。所以我感觉他是有预谋的已经准备好的东西。</p>
<p>然后这张违禁图出现后再跑到我直播间，等我直播间画面有这个画面了再点了举报。</p>
<h1>联系客服</h1>
<p>虽然真相八九不离十，但只有自己的推测还是不够的，于是我进一步收集证据，联系了客服。</p>
<p>第一次联系客服是在22时45分，也就是我发现被封的9分钟后，最后只说让我提交工单，但是两天过去了工单没有回复。</p>
<p>于是我又在5月15日第二次联系客服，这次让我获得了一些证据。</p>
<p>我先是跟之前一样汇报了一下基本情况，并且强调这可能是一起栽赃恶意举报，但客服的答复一直是“经核实您的直播间违规”、“这边核实涉及到违禁”、“这边核实的确有违禁内容”，即使我强调“违禁并不是我想违禁的”，得到的答复依然是“小伙伴很抱歉这边是核实到您有违禁内容的”。</p>
<p>客服这样的答复显然让我这个受害人雪上加霜，感叹自己的冤屈何时了，也不禁引发了我的思考（押韵了）。会不会有这样一种可能，可能这位客服根据我提供的证据和TA那边查到的证据，TA也明白这极有可能是一起栽赃恶意举报，但由于某些我不明白的限制，并不能为我洗脱冤屈，于是只能用“很抱歉，经核实您确实有违禁内容”来回复我。</p>
<p>啊，想到这儿我豁然开朗，反思自己心胸狭小，万幸于能够自晓。于是我从“强调恶意举报”转为“想取更多情报”，后来终于被我确认到，直接让我自信曰：“95%恶意举报”。</p>
<p>后来我确认到的是举报人有截图证据，并且截图就是那幅画。</p>
<p>以上就是整个事件的经过。</p>
<p>我希望让更多的人看到这个视频，就需要大家的支持。<br>
接下来我试着为自己拉一拉选票，也许有观众是第一次看到我，所以也需要让视频前的观众了解一下我是怎样的人。</p>
<h1>自夸秀</h1>
<p>以下是我的自夸秀。</p>
<p>我是一个不图名利咸鱼UP，2013年来到B站，一直默默做着《魔兽争霸地图编辑器教程》。几年下来帮助过一些人解答过一些问题但还算谦虚并不会觉得自己高高在上。</p>
<p>我还是一个佛系开发者，2018年12月22日开始开发一款叫Btools的浏览器插件并维护至今，功能以优化B站用户体验为主，还有几率帮你找回收藏夹中失去的回忆。从学习、制作、发布再到官网的建设和维护，都在工作之余独立完成，两年服务器、域名成本，也是自掏腰包。并不是卖惨也不是求赞助，至今我也从未发过任何收款码。</p>
<p>视频没要过三连，开发没求过赞助，因为类似的事儿连家人都说“你这是清高”，虽然他们的不理解让我感觉没人懂我，但也怪我没做好沟通。不要利益并不是因为我有钱，而是我有几斤几两我自己知道，没有人比我更懂我自己，以我现在的技术并不能为我产生太大的利益，如果接受了这些小利益反而有可能会让我满足于现状停止进步。这样的话有点得不偿失，就不如不断进步并积累口碑和人脉。</p>
<p><strong>我不感觉“知识私有”可耻，但以“知识共享”为荣。</strong></p>
<p>嗯，这句话可以作为我的名言了，虽然我没有名。<br>
如果有人在我之前说了这句话，那么，英雄所见略同。</p>
<h1>想对那位举报人说</h1>
<p>然后我想对那位举报人说，反正不管看到看不到吧。</p>
<p>假设真的是那95%的可能，假设这位举报人真的就是画那幅画的人，假设这是一场栽赃恶意举报。我只想说，你这种行为不但不会让我感到挫败和委屈，反而你帮我把平时不会在视频中说的心里话说了出来。</p>
<p>感谢你“帮”我录了这期视频。<br>
感谢你“帮”我夸了自己半天。<br>
感谢你“帮”我宣传了Btools插件。<br>
感谢你“帮”我说出“我爱B站”和“我对B站的爱”。</p>
<p>以上就是对这位举报人兼画画人说的话。</p>
<p>但是如果是另外5%的可能，假设这位举报人不是画画人，那我想对您说。（还是录个视频吧，不然感觉这个“您您您”的跟阴阳怪气儿似得）我想对您说，您一定跟我一样非常爱国并且非常爱B站，虽然顺带着把自己夸了，但我说的并不假都是大实话。从这个角度出发我对您的举报行为表示赞同。</p>
<p>如果您是事后发现我是友军，那也不要担心，封禁不会对我造成损失。因为您看，这弹幕姬的报错信息比弹幕都多，平时也都是这个状态，所以有没有直播间没区别。直播间对于我来说也只是个在我学习的时候有个监督、在我玩游戏的时候有人能看到我万一打出的骚操作。</p>
<p>以上就是对这位5%可能性并非恶意举报的人说的话。</p>
<h1>结尾</h1>
<p>最后来个结尾，如果你觉得我做的“正道的光”的话，如果可以麻烦帮我转发一下，非常感谢。也希望B站能完善相关制度，不放过任何一个坏人也不冤枉任何一个好人。害，我又夸自己是个好人了。</p>
<h1>视频收益</h1>
<p>最后我想说一下关于本视频收益，诶你是不是觉得我要说捐出去，但可能要让你失望了。综合现状考虑，如果这个视频有幸能有些收益，我会自己留着。</p>
<h1>告辞</h1>
<p>就是这样，告辞</p>
<h1>视频中没说的</h1>
<p>也许视频过不了审，我也不知道，先这样吧。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[封装了个 getElements()]]></title>
            <link>https://imba97.com/posts/591</link>
            <guid isPermaLink="false">https://imba97.com/posts/591</guid>
            <pubDate>Fri, 08 May 2020 00:56:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>做插件需要用到的，因为插件比页面元素加载的快，并且有些页面还是js渲染内容的</p>
<p>所以就需要一个计时器循环检查页面中是否有我需要的元素，于是这个<code>getElements()</code>就出来了</p>
<!--more-->
<p>放在了一个工具类中</p>
<pre><code class="language-typescript">export default class Util {
  private static _instance: Util

  public static instance(): Util {
    if (typeof this._instance === 'undefined') {
      this._instance = new Util()
    }
    return this._instance
  }

  /**
   * 获取页面上的元素，10秒内如果没获取到则停止获取
   * @param selector 选择器
   */
  public getElements(selector: string): Promise&lt;NodeListOf&lt;Element&gt;&gt; {
    return new Promise((resolve, reject) =&gt; {
      let timeout = 20
      const timer = setInterval(() =&gt; {
        const elements: NodeListOf&lt;Element&gt; = document.querySelectorAll(selector)

        // 成功获取
        if (elements.length !== 0) {
          resolve(elements)
          clearInterval(timer)
        }

        // timeout
        if (timeout === 0) {
          reject(new Error('Empty NodeListOfElement'))
          clearInterval(timer)
        }

        timeout--
      }, 500)
    })
  }
}
</code></pre>
<p><a href="https://github.com/imba97/Btools-vue/blob/master/src/scripts/util.ts">Github →</a></p>
<p>用了<code>Promise</code>，简洁易读。多个Elements还可以直接用<code>Promise.all()</code>挨个遍历出来，舒适</p>
<pre><code class="language-typescript">import HKM from './hotKeyMenu'
import Util from './util'
const UtilInstance = Util.instance()

const avatar = UtilInstance.getElements('.avatar')
const span = UtilInstance.getElements('#primaryChannelMenu&gt;span')

Promise.all([avatar, span]).then((nodeLists) =&gt; {
  nodeLists.forEach((elements) =&gt; {
    elements.forEach((element) =&gt; {
      new HKM(element).add([
        {
          key: 83,
          title: '测试',
          action() {
            alert('测试')
          }
        }
      ])
    })
  })
}).catch((error) =&gt; {
  UtilInstance.console(error, 'error')
})
</code></pre>
<p><a href="https://github.com/imba97/Btools-vue/blob/master/src/scripts/viv.ts">Github →</a></p>
<p>不过有个问题，<code>Promise.all()</code>的话如果其中一个没获取到，那整个就返回错误了。如果我想让获取到的返回成功，没获取到的返回错误，大概得遍历<code>[avatar, span]</code>，其实也OK</p>
<p>那个<code>new HKM()</code>，是最近要做的“快捷键菜单”，至于是个什么东西，有兴趣的话可以看一下：<a href="https://btools.cc/hot-key-menu/">《快捷键菜单》</a>，用jQuery写的，这次打算用原生js重构一遍</p>
<p>快捷键菜单源码：<a href="https://github.com/imba97/Btools/blob/master/core/Btools.js#L139">Github →</a><br>
TypeScript版快捷键菜单：<a href="https://github.com/imba97/Btools-vue/blob/master/src/scripts/hotKeyMenu.ts">Github →</a></p>
<h1>后言</h1>
<p>被ES6爽到，也被TypeScript语法爽到，可以指定类型，vs code 直接就有相应的接口提示，太舒服了 —— 来自PHP程序猿的叫好</p>
<p>我可能是个假程序猿</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Vue TypeScript Webpack 浏览器插件开发]]></title>
            <link>https://imba97.com/posts/585</link>
            <guid isPermaLink="false">https://imba97.com/posts/585</guid>
            <pubDate>Wed, 06 May 2020 00:13:24 GMT</pubDate>
            <content:encoded><![CDATA[<p>折腾了两天，终于把这个做成了能打包成浏览器加载的插件了</p>
<p>姑且在这里记录一下，也算是前端学习笔记</p>
<!--more-->
<h1>目录结构</h1>
<pre><code>📦src
 ┣ 📂assets
 ┃ ┣ 📂icon
 ┃ ┃ ┣ 📜128.png
 ┃ ┃ ┣ 📜16.png
 ┃ ┃ ┗ 📜48.png
 ┃ ┗ 📂styles
 ┃ ┃ ┣ 📜global.scss
 ┃ ┃ ┣ 📜options.scss
 ┃ ┃ ┗ 📜popup.scss
 ┣ 📂background
 ┃ ┗ 📜background.ts
 ┣ 📂components
 ┃ ┣ 📂ui
 ┃ ┃ ┗ 📜button.vue
 ┃ ┣ 📜options.vue
 ┃ ┗ 📜popup.vue
 ┣ 📂options
 ┃ ┣ 📜options.html
 ┃ ┗ 📜options.ts
 ┣ 📂popup
 ┃ ┣ 📜popup.html
 ┃ ┗ 📜popup.ts
 ┣ 📂scripts
 ┃ ┣ 📂base
 ┃ ┃ ┣ 📂enums
 ┃ ┃ ┃ ┗ 📜url.ts
 ┃ ┃ ┗ 📜extConfig.ts
 ┃ ┣ 📜hotKeyMenu.ts
 ┃ ┣ 📜ui.ts
 ┃ ┣ 📜util.ts
 ┃ ┗ 📜viv.ts
 ┣ 📂typings
 ┃ ┣ 📜global.d.ts
 ┃ ┗ 📜hotKeyMenu.d.ts
 ┣ 📂_locales
 ┃ ┣ 📂en
 ┃ ┃ ┗ 📜messages.json
 ┃ ┣ 📂ja
 ┃ ┃ ┗ 📜messages.json
 ┃ ┗ 📂zh_CN
 ┃ ┃ ┗ 📜messages.json
 ┣ 📜btools.ts
 ┣ 📜manifest.json
 ┗ 📜vue-shims.d.ts
</code></pre>
<h1>作用</h1>
<p>不清楚这么分好不好，姑且来说说它们都有什么作用</p>
<p><code>_locales</code> 存的是多语言文件</p>
<p><code>assets</code> 是图标和 sass 文件，图标会在打包时直接复制到编译后的目录</p>
<p><code>components</code> 放的是 vue 组件，目前只有俩，之后随着需要添加</p>
<p><code>background</code> 插件的 <code>background.js</code> 所需要的，这个不需要页面所以只放了一个 <code>.ts</code></p>
<p><code>options</code> 插件的配置页面</p>
<p><code>popup</code> 插件右上角弹出页面</p>
<p><code>scripts</code> 插件功能所需要的代码文件，里面那些 ui 啥的我还没想好怎么写，姑且先放着</p>
<p><code>btools.ts</code> 插件功能的入口文件，这个文件会引入<code>scripts</code>中的所有插件功能相关的文件</p>
<p><code>manifest.json</code> 插件配置文件</p>
<h1>具体写法</h1>
<p>这些是怎么打包成一个插件的呢，用<code>popup</code>举个例子</p>
<p>首先是<code>webpack.config.js</code>指定好入口文件</p>
<pre><code class="language-javascript">entry: {
  btools: './src/btools.ts',
  background: './src/background/background.ts',
  popup: './src/popup/popup.ts',
  options: './src/options/options.ts',
},
</code></pre>
<p>入口文件是<code>popup.ts</code></p>
<pre><code class="language-javascript">import Popup from '@components/popup'
import Vue from 'vue'
import '@styles/popup'

export default new Vue({
  data: { test1: 'World' },
  components: {
    Popup
  },
  render: h =&gt; h(Popup)
}).$mount('#app')
</code></pre>
<p>这里面引入了 Vue、popup.vue、popup.sass</p>
<p>然后 <code>popup.vue</code> 是这么写的</p>
<pre><code class="language-vue">&lt;template&gt;
  &lt;div&gt;
    &lt;h1&gt;{{ message }}&lt;/h1&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script lang=&quot;ts&quot;&gt;
import { Component, Vue } from 'vue-property-decorator'

// @Component 修饰符注明了此类为一个 Vue 组件
@Component
export default class Popup extends Vue {
  // 初始数据可以直接声明为实例的 property
  message = 'Popup'
}
&lt;/script&gt;
</code></pre>
<p><code>popup.sass</code>没写啥就不用看了</p>
<p>最后页面会输出到<code>popup.html</code>上</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;title&gt;popup&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id=&quot;app&quot;&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>这个HTML文件是个模板，使用插件<code>html-webpack-plugin</code>，最后会打包到编译后的目录，这里指定了<code>chunks</code>是只让这个页面引入<code>options.js</code></p>
<pre><code class="language-javascript">new HtmlWebpackPlugin({
  filename: 'options.html',
  template: './src/options/options.html',
  minify: {
    collapseWhitespace: true,
    removeComments: true
  },
  chunks: ['options']
})
</code></pre>
<p>最后配置好<code>manifest.json</code>，就可以直接用浏览器导入插件看效果了</p>
<p><img src="https://imba97.com/uploads/2020/05/btools-vue.png" alt=""></p>
<p><code>manifest.json</code>可以用一个叫<code>write-json-webpack-plugin</code>，这个插件可以加载<code>.json</code>文件，转成一个对象，就可以修改</p>
<p>使用这个比如这样可以修改版本号和根据浏览器做不同的修改</p>
<pre><code class="language-javascript">let manifestJSON = require('./src/manifest.json')

// 版本号
manifestJSON.version = '2.1.0'

// 火狐浏览器
manifestJSON.browser_specific_settings = {
  gecko: {
    id: 'mail@imba97.cn',
    strict_min_version: '57.0'
  }
}
manifestJSON.applications = {
  gecko: {
    id: 'mail@imba97.cn',
    strict_min_version: '57.0'
  }
}
</code></pre>
<p>最后在使用插件把修改后的对象打包成<code>.json</code>文件</p>
<pre><code class="language-javascript">(manifestJSON &amp;&amp; new WriteJsonWebpackPlugin({
  pretty: false,
  object: manifestJSON,
  path: '/',
  filename: 'manifest.json'
}))
</code></pre>
<p>这个方法是从<a href="https://github.com/bilibili-helper/bilibili-helper-o">bilibili-helper</a>抄来的，大佬的插件，可以说是看B站网页版必装的插件</p>
<p>先总结到这里，今后边学 Vue 边学 TypeScript 边学 Webpack 边开发，大概会更新文章</p>
<p>本插件也放在了 Github 上，有兴趣的话可以看看 <a href="https://github.com/imba97/Btools-vue">Btools-vue</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】找打工险些被坑]]></title>
            <link>https://imba97.com/posts/584</link>
            <guid isPermaLink="false">https://imba97.com/posts/584</guid>
            <pubDate>Tue, 28 Apr 2020 10:29:43 GMT</pubDate>
            <content:encoded><![CDATA[<h1>文稿</h1>
<p>无。</p>
<!--more-->
<h1>后言</h1>
<p>当时找的两家店，都是属于他们管理的。我倒是能理解中介公司，但你说的时候清清楚楚说自己是家公司，别整那些套路的话。</p>
<p>其实这种如果不是必须找打工的人，基本都不会交钱让他们帮忙找。因为我本身在APP上就是找打工的，去了发现并不是店家，而是个中介，还让你交钱，真的挺影响心情的，就白跑一趟。</p>
<p>出来的时候我好像看到了敬汉卿？？？带着口罩，反正眼睛挺像的，当时地址在泰合国际中心某栋某单元，出电梯看到的。错觉么 233。</p>
<h1>后来搜的结果</h1>
<p><a href="http://china.findlaw.cn/ask/question_49035359.html">《请问在成都做兼职需要交300元的人力资源的档案费吗？》</a><br>
<a href="https://www.lawtime.cn/ask/question_22829298.html">《成都创世纪人力资源公司，要我交了300押金···》</a><br>
<a href="http://www.9ask.cn/souask/q/y9765631.htm">《成都人力资源部是不是骗子···》</a><br>
<a href="https://www.baidu.com/s?ie=UTF-8&amp;wd=%E6%88%90%E9%83%BD%20%E6%89%93%E5%B7%A5%20%E4%BA%BA%E5%8A%9B%E8%B5%84%E6%BA%90%20300">“成都 打工 人力资源 300”的百度结果</a></p>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/BV1CK4y1b7JW">https://www.bilibili.com/video/BV1CK4y1b7JW</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[抖音去水印]]></title>
            <link>https://imba97.com/posts/546</link>
            <guid isPermaLink="false">https://imba97.com/posts/546</guid>
            <pubDate>Sat, 25 Apr 2020 19:32:11 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>如果你是为了盗视频而看本教程，那就是本教程最大的失败。</p>
<p>我的初衷是因为网上那些小程序广告太多，功能区太鸡儿隐秘，令我感到极度不适。</p>
<p>至于为什么说是本教程最大的失败，因为这个社会什么牛鬼蛇神都有，我无法控制。盗视频这种弟弟行为，即使不看这个也会去搜其他教程、用其他软件。</p>
<h1>手动打开无水印视频</h1>
<p>抖音去水印教程，不借助其他去水印工具。</p>
<p>最后的PHP程序参考：<a href="https://blog.csdn.net/a1258531/article/details/103775094">《【PHP】抖音无水印视频解析源码》</a></p>
<!--more-->
<h2>1.获取分享地址</h2>
<p>点击抖音右下角的更多按钮</p>
<p><img src="https://imba97.com/uploads/2020/04/douyin_1.jpg" alt=""></p>
<p>点击“复制链接”</p>
<p><img src="/uploads/2020/04/douyin_2.jpg" alt=""></p>
<h2>2.浏览器打开</h2>
<p><strong>先点一下播放</strong>，让播放器加载出来，随后在视频上右键，点击“检查”</p>
<p><img src="/uploads/2020/04/douyin_3.png" alt=""></p>
<h2>3.复制视频地址并修改</h2>
<p>右键<code>video</code>中的<code>src</code>的地址，选择“Copy link address”</p>
<p><img src="/uploads/2020/04/douyin_4.png" alt=""></p>
<pre><code>https://aweme.snssdk.com/aweme/v1/playwm/?s_vid=93f1b41336a8b7a442dbf1c29c6bbc56fa15389321801622b5036fe13b074f3fa77d6dadede77214dd28b0028f215f16a6eed3e3dc42e96288df31343cd9228b&amp;line=0
</code></pre>
<p>把最前面的网址<code>https://aweme.snssdk.com/aweme/v1/playwm/</code>中的<code>playwm</code>改成<code>play</code></p>
<p>就变成：<code>https://aweme.snssdk.com/aweme/v1/play/</code>，后面不变，这个就是<strong>视频地址</strong></p>
<h2>4.修改 User-Agent</h2>
<p>如果直接打开是空白页面，我们需要自定义<code>User-Agent</code></p>
<p>打开“Network conditions”</p>
<p><img src="/uploads/2020/04/douyin_5.png" alt=""></p>
<p>复制以下内容</p>
<pre><code>Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25
</code></pre>
<p>先去掉“Select automatically”的勾，再粘贴到下面的输入框中</p>
<p><img src="/uploads/2020/04/douyin_6.png" alt=""></p>
<h2>5.打开视频地址</h2>
<p>最后再把视频地址粘贴到浏览器，打开即可。此时已经没有水印了。</p>
<p><img src="/uploads/2020/04/douyin_7.png" alt=""></p>
<style>#share_url,#share_code {width: 230px;text-align: center; border: none;border-bottom: 1px #000 solid;border-radius: 0;} #share_url {outline: none;} #share_btn{border:1px #000 solid; border-radius: 5px; background: none; outline: none; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;} #share_btn:active{background-color: #CCC;}</style>
<script>function analysis(){var share_url=document.querySelector('#share_url').value;var share_code=document.querySelector('#share_code').value;if(!/https?:\/\/v\.douyin\.com\/[a-zA-Z0-9]+/i.test(share_url)){ alert('分享地址格式错误');return;} $.get('https://bili.imba97.com/douyin/api.php?url='+share_url+'&code='+share_code, function(json){if(json.code==0){alert(json.msg);return;} var output=document.querySelector('#output'); output.href=json.url; output.innerText='点击此处跳转至视频' }); }</script>
<h1>去印员</h1>
<p>以上就是本期教程，如果你喜欢我的教程，请支持imba久期博客网</p>
<p>最后，我们imba久期博客网还有一个假会员频道，叫去印员，里面有我们非独家的抖音去水印服务和精选文章</p>
<p>现在您可以直接把分享地址粘贴到这里：<input type="text" id="share_url" placeholder="https://v.douyin.com/xxxxxx/"></p>
<p>再输入我的专属邀请码：<input type="text" id="share_code" style="width:60px"> 就可以 <input type="button" id="share_btn" value="立即解析" onclick="analysis()"></p>
<p><a id="output" href="javascript:void(0);" target="_blank" rel="noopener noreferrer"></a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[frp内网穿透 树莓派+阿里云服务器]]></title>
            <link>https://imba97.com/posts/506</link>
            <guid isPermaLink="false">https://imba97.com/posts/506</guid>
            <pubDate>Thu, 02 Apr 2020 17:01:30 GMT</pubDate>
            <content:encoded><![CDATA[<h1>前言</h1>
<p>本教程以树莓派+阿里云服务器为例，两者都是linux系统，不涉及其他配置方案</p>
<h1>下载</h1>
<p>首先打开frp在github上的发布版本，找到你要下载的版本，复制压缩包的地址。<br>
服务器：<code>frp_版本号_linux_amd64.tar.gz</code><br>
树莓派：<code>frp_版本号_linux_arm.tar.gz</code></p>
<p>示例：<code>https://github.com/fatedier/frp/releases/download/v0.32.1/frp_0.32.1_linux_amd64.tar.gz</code><br>
树莓派：<code>https://github.com/fatedier/frp/releases/download/v0.32.1/frp_0.32.1_linux_arm.tar.gz</code></p>
<p>发布地址：<a href="https://github.com/fatedier/frp/releases">https://github.com/fatedier/frp/releases</a></p>
<p>在阿里云和树莓派使用命令下载</p>
<pre><code class="language-bash"># 先到合适的目录 使用wget命令
wget https://github.com/fatedier/frp/releases/download/v0.32.0/frp_0.32.0_linux_amd64.tar.gz
</code></pre>
<!--more-->
<h1>解压</h1>
<p>使用以下命令解压刚才下好的压缩包</p>
<pre><code class="language-bash">tar -zxvf frp_0.32.0_linux_amd64.tar.gz
</code></pre>
<p>解压好后会出现<code>frp_0.32.0_linux_amd64</code>文件夹，首先进入文件夹</p>
<pre><code class="language-bash">cd frp_0.32.0_linux_amd64
</code></pre>
<h1>服务器（服务端）配置</h1>
<p>配置文件是<code>frps.ini</code>，默认内容如下</p>
<pre><code class="language-ini">[common]
bind_port = 7000
</code></pre>
<p>如果你服务器上的7000端口没有其他用途，默认这个配置就可以</p>
<h1>树莓派（客户端）配置</h1>
<p>配置文件是<code>frpc.ini</code>，默认内容如下</p>
<pre><code class="language-ini">[common]
server_addr = 127.0.0.1
server_port = 7000

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000
</code></pre>
<p><code>[common]</code>和<code>[ssh]</code>是个名称</p>
<pre><code class="language-ini">server_addr = 阿里云服务器的IP地址
server_port = frp服务端的端口号

type = 协议类型
local_ip = 本地地址
local_port = 本地端口
remote_port = 远程端口
</code></pre>
<h1>客户端新增配置案例</h1>
<p>假设</p>
<p>服务器的IP地址是<code>50.60.70.80</code><br>
我电脑的内网IP地址是<code>192.168.1.10</code><br>
电脑上开了个网站，端口是<code>80</code></p>
<p>那么客户端的<code>frpc.ini</code>配置是这样的，首先<code>[common]</code>是必须的，它指向了服务器的<code>frp</code><br>
然后自定义一个，比如<code>[website]</code>，下面是具体写法</p>
<pre><code class="language-ini">[common]
server_addr = 50.60.70.80
server_port = 7000

[website]
type = http
local_ip = 192.168.1.10
local_port = 80
remote_port = 6000
</code></pre>
<p>配置完后重启frp，我在电脑浏览器输入<code>50.60.70.80:6000</code>就能打开我本地电脑的网站了</p>
<h1>客户端的启动和开机启动</h1>
<p>启动可以在<code>frpc</code>所在的文件夹下执行以下命令</p>
<pre><code class="language-bash">./frpc -c ./frpc.ini
</code></pre>
<p>开机启动则需要在<code>/etc/init.d</code>目录下创建一项服务</p>
<pre><code class="language-bash"># 移动
cd /etc/systemd/system
# 创建文件
vim frpc.service
</code></pre>
<p>添加以下内容，里面的<code>ExecStart</code>就是要执行的代码，需要用绝对路径</p>
<pre><code class="language-ini">[Unit]
Description=frpc
After=syslog.target network.target
Wants=network.target

[Service]
Type=simple
ExecStart=/home/pi/frpc -c /home/pi/frpc.ini
Restart= always
RestartSec=1min

[Install]
WantedBy=multi-user.target
</code></pre>
<p>使用以下命令启动、停止、重启</p>
<pre><code class="language-bash">service frpc start
service frpc stop
service frpc restart
</code></pre>
<p>使用以下命令添加开机启动</p>
<pre><code class="language-bash">systemctl enable frpc.service
</code></pre>
<h1>服务器的启动和自动启动</h1>
<p>同理，操作步骤一样的，只不过服务器的是<code>frps</code></p>
<pre><code class="language-bash">./frps -c ./frps.ini
</code></pre>
<p>创建服务</p>
<pre><code class="language-bash"># 移动
cd /etc/systemd/system
# 创建文件
vim frps.service
</code></pre>
<p>添加以下内容</p>
<pre><code class="language-ini">[Unit]
Description=frps
After=syslog.target network.target
Wants=network.target

[Service]
Type=simple
ExecStart=/home/imba97/frps -c /home/imba97/frps.ini
Restart=always
RestartSec=1min

[Install]
WantedBy=multi-user.target
</code></pre>
<p>使用以下命令启动、停止、重启</p>
<pre><code class="language-bash">service frps start
service frps stop
service frps restart
</code></pre>
<p>使用以下命令添加开机启动</p>
<pre><code class="language-bash">systemctl enable frps.service
</code></pre>
<h1>阿里云</h1>
<p>如果要用阿里云的话：<a href="https://www.aliyun.com/minisite/goods?userCode=ujl29kt5">推广链接</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】跨省搬家 找房交了100智商税]]></title>
            <link>https://imba97.com/posts/505</link>
            <guid isPermaLink="false">https://imba97.com/posts/505</guid>
            <pubDate>Thu, 02 Apr 2020 01:04:01 GMT</pubDate>
            <content:encoded><![CDATA[<h1>文稿</h1>
<p>无</p>
<p>记录搬家过程遇到的事，上海 -&gt; 成都</p>
<p>还有带货</p>
<!--more-->
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/BV17A411h7Mo">https://www.bilibili.com/video/BV17A411h7Mo</a></p>
<h1>带货</h1>
<img src="https://i.loli.net/2020/04/02/r5JZYPD3wiy6SkH.png" width="200">
<p>售价：<code>29.70元</code><br>
佣金：<code>4.16元</code></p>
<p>最后更新日期：<code>2020-04-02</code></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[iOS 开发笔记 – 内购]]></title>
            <link>https://imba97.com/posts/502</link>
            <guid isPermaLink="false">https://imba97.com/posts/502</guid>
            <pubDate>Sun, 08 Mar 2020 13:20:52 GMT</pubDate>
            <content:encoded><![CDATA[<h2>内购流程</h2>
<ol>
<li>请求商品列表</li>
<li>返回有效商品</li>
<li>显示购买项</li>
<li>支付</li>
</ol>
<!--more-->
<h2>代码实现</h2>
<pre><code class="language-objective-c">// 引入头文件
#import &lt;StoreKit/StoreKit.h&gt;
@interface ViewController ()&lt;SKProductsRequestDelegate, SKPaymentTransactionObserver&gt;
// 记录商品信息
@property (nonatomic, strong) NSArray *products;
@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  // 请求可售商品列表
  // 创建商品ID无序集
  NSSet *set = [NSSet setWithObjects: @&quot;cn.imba97.item1&quot;, @&quot;cn.imba97.item2&quot;, nil];
  // 创建商品请求对象
  NSProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers: set];
  // 设置代理
  request.delegate = self;
  // 开始请求
  [request start];
}

// 在代理方法中返回商品列表

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0) {
  // 如果标示符不正确，会返回array集合供开发者调试
  // 无效标示符大于0，
  if (response.invalidProductIdentifiers.count &gt; 0) {
    return;
  }
  // 成功返回则保存
  self.products = response.products;
  // 显示到界面
}

// 支付

- (void)pay: (Int) productIndex {
  // 选择商品
  SKProduct *product = self.products[productIndex];
  // 开具凭证
  SKPayment *payment = [SKPayment paymentWithProduct: product];
  // 进入交易队列
  [[SKPaymentQueue defaultQueue] addPayment: payment];
  // 监听支付流程
  [[SKPaymentQueue defaultQueue] addTransactionObserver: self];
}

// 实现监听支付流程协议的方法

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray&lt;SKPaymentTransaction *&gt; *)transactions {
  for (SKPaymentTransaction *transaction in transactions) {
    switch (transaction.transactionState) {
      case SKPaymentTransactionStatePurchasing:
        NSLog(@&quot;正在购买&quot;);
        break;
      case SKPaymentTransactionStatePurchased:
        NSLog(@&quot;购买完成&quot;);
        // 结束交易
        [[SKPaymentQueue defaultQueue] finishTrancation: trancation];
        break;
      case SKPaymentTransactionStateFailed:
        NSLog(@&quot;购买失败&quot;);
        break;
      case SKPaymentTransactionStateRestored:
        NSLog(@&quot;恢复购买&quot;);
        // 结束交易
        [[SKPaymentQueue defaultQueue] finishTrancation: trancation];
        break;
      case SKPaymentTransactionStateDeferred:
        NSLog(@&quot;无法判断&quot;);
        break;
    }
  }
}

@end
</code></pre>
<p><code>SKPaymentTransactionState</code>的类型</p>
<ol>
<li>SKPaymentTransactionStatePurchasing - 正在购买</li>
<li>SKPaymentTransactionStatePurchased - 购买完成（客户端必须完成交易）</li>
<li>SKPaymentTransactionStateFailed - 购买失败</li>
<li>SKPaymentTransactionStateRestored - 恢复购买（客户端必须完成交易）</li>
<li>SKPaymentTransactionStateDeferred - 用户未决定（信用卡信息未配置等情况）</li>
</ol>
<h2>恢复购买</h2>
<p>恢复之前购买过的商品</p>
<pre><code class="language-objective-c">// 按钮点击事件
- (IBAction)restoreClick: (id)sender {
  [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
</code></pre>
<h2>优化</h2>
<p>监听支付流程无需每次购买时添加，只要在购买界面被加载时添加即可</p>
<pre><code class="language-objective-c">// 视图将要出现时
- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  [[SKPaymentQueue defaultQueue] addTranstactionObserver: self];
}
// 视图消失时
- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];
  [[SKPaymentQueue defaultQueue] removeTransactionObserver: self];
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Swift 笔记 – 归档和解档]]></title>
            <link>https://imba97.com/posts/497</link>
            <guid isPermaLink="false">https://imba97.com/posts/497</guid>
            <pubDate>Thu, 20 Feb 2020 22:56:30 GMT</pubDate>
            <content:encoded><![CDATA[<h1>使用</h1>
<p>使用归档和解档来保存用户数据</p>
<p>归档：把当前对象保存到磁盘前，将对象编码成二进制数据<br>
解档：从磁盘加载二进制文件，转换成对象调用</p>
<!--more-->
<pre><code class="language-swift">import UIKit
// 使用归档解裆前需要遵守 NSCoding 协议
class UserAccount: NSObject, NSCoding {
  // NSCoding 协议中必须有两个方法需要实现
  var userName: String?
  var userToken: String?
  var expiresDate: NSDate?
  var userDevices: String?
  func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(userName, forKey: &quot;userName&quot;)
    aCoder.encodeObject(userToken, forKey: &quot;userToken&quot;)
    aCoder.encodeObject(expiresDate, forKey: &quot;expiresDate&quot;)
    aCoder.encodeObject(userDevices, forKey: &quot;userDevices&quot;)
  }
  // 需要用 required 关键词修饰，标识必须的、没有继承性，所有的对象只能解档出当前的类对象，每个类必须各自实现解档方法
  required init?(coder aDecoder: NSCoder) {
    userName = aDecoder.decodeObjectForKey(&quot;userName&quot;) as? String
    userToken = aDecoder.decodeObjectForKey(&quot;userToken&quot;) as? String
    expiresDate = aDecoder.decodeObjectForKey(&quot;expiresDate&quot;) as? String
    userDevices = aDecoder.decodeObjectForKey(&quot;userDevices&quot;) as? String
  }
  // 调用归档解裆方法
  func saveUserAccount() {
    // 保存路径
    var path = NSSearchPathForDirctoriesInDomains(.DocumentDirectory, .UserDomainMask, true).last!
    // 拼接文件名
    path = (path as NSString).stringByAppendingPathComponent(&quot;account.plist&quot;)
    // 调用归档，self 是归档对象
    NSKeyedArchiver.archiveRootObject(self, toFile: path)
  }
}
</code></pre>
<p>一般归档不需要保存成<code>plist</code>，但开发中必须确认数据是否正确的保存下来了，为方便查看保存成<code>plist</code>格式</p>
<p>使用视图模型进行解裆</p>
<pre><code class="language-swift">import Foundation
class UserAccountViewModel {
  // 用户模型
  var account: UserAccount?
  // 用户登录标记
  var userLogin: Bool {
    // 如果 token 有值，并且没过期，则是有效登录
    return account?.userToken != nil &amp;&amp; !isExpired
  }
  // 归档保存的路径
  private var accountPath: String {
    let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).last!
    return (path as NSString).stringByAppendingPathComponent(&quot;account.plist&quot;)
  }
  // 判断 token 是否过期
  private var isExpired: Bool {
    // 用户数据与当前系统时间作比较
    if account?.expiresDate?.compare(NSDate()) == NSComparisonResult.OrderedDescending {
      return false
    }
    return true
  }
  // 构造函数
  init() {
    account = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
    // token 过期清空解裆数据
    if isExpired {
      print(&quot;已经过期&quot;)
      account = nil
    }
  }
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[iOS 开发笔记 - UI]]></title>
            <link>https://imba97.com/posts/496</link>
            <guid isPermaLink="false">https://imba97.com/posts/496</guid>
            <pubDate>Thu, 20 Feb 2020 21:55:02 GMT</pubDate>
            <content:encoded><![CDATA[<h2>UI控件关联代码（Objective-C）</h2>
<pre><code class="language-objective-c">// ViewController
#import &quot;ViewController.h&quot;

// 扩展类
@interface ViewController ()
// (IBAction)是返回值，等于void，但如果想关联UI控件的执行动作，就必须设置为(IBAction)
- (IBAction) test;
@end

@implementation ViewController
// 实现
- (void) test {
  NSLog(@&quot;test&quot;);
}
@end
</code></pre>
<!--more-->
<h2>IBAction、IBOutlet</h2>
<p><code>IBAction</code>从返回值角度看，作用相当于void，只有返回值声明<code>IBAction</code>的方法，才能跟<code>storyboard</code>中的控件连线<br>
<code>IBOutlet</code>是属性，只有声明<code>IBOutlet</code>的属性才能跟<code>storyboard</code>中的控件连线</p>
<p>多控件关联同一个方法只需拖到同一个<code>IBAction</code>上即可</p>
<h2>获取当前控件</h2>
<pre><code class="language-objective-c">- (IBAction) test:(UIButton *) sender {
  // 此时 serder 就是触发事件的按钮
  // 打印tag
  NSLog(sender.tag);
}
</code></pre>
<h2>获取父控件和子控件</h2>
<pre><code class="language-objective-c">// superview 获取父控件
self.textTest.superview.backgroundColor = [UIColor redColor];
// subviews 获取所有子控件
for (UIView *view in self.view.subviews) {
  view.backgroundColor = [UIColor redColor];
}
</code></pre>
<h2>根据tag获取控件</h2>
<p>首先需要在 storyboard 设置控件的 tag</p>
<pre><code class="language-objective-c">// 比如获取某个文本框
UITextField *txt = (UITextField *) [elf.view viewWithTag: 1000];
</code></pre>
<h2>移动位置</h2>
<pre><code class="language-objective-c">// 1. 获取控件 center 或 frame，center是中心点坐标，frame是左上角坐标
CGPoint centerPoint = self.btnTest.center;
// 2. 修改
centerPoint.x -=10;
centerPoint.y +=10;
// 3. 重新赋值 center
self.btnTest.center = centerPoint;
</code></pre>
<h2>动画效果</h2>
<pre><code class="language-objective-c">// 1. 开启动画
[UIView beginAnimations: nil context:nil];
// 2. 设置动画执行时间
[UIView setAnimationDuration:2];

// 要执行动画的代码，比如设置位置
self.btnTest.center = centerPoint;

// 3. 提交动画
[UIView commitAnimations];
</code></pre>
<p>block 方式执行动画</p>
<pre><code class="language-objective-c">[UIView animateWithDuration:1 animations:^{
  // 要执行动画的代码
  self.btnTest.center = centerPoint;
}];
</code></pre>
<h2>加载图片</h2>
<p>使用<code>imageNamed:</code>方法可以让图片缓存在内存中，直到应用退出</p>
<pre><code class="language-objective-c">UIImage *img = [UIImage imageNamed:@&quot;test&quot;];
</code></pre>
<p>使用<code>imageWithContentsOfFile:</code>方法则不会有缓存</p>
<pre><code class="language-objective-c">NSString *path = [[NSBundle mainBundle] pathForResource:@&quot;test.jpg&quot; ofType:nil];
UIImage *img = [UIImage imageWithContentsOfFile:path];
</code></pre>
<h2>Swift创建、销毁视图</h2>
<pre><code class="language-swift">import UIKit
class DemoViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    // 视图最好指定背景颜色
    view.backgroundColor = UIColor.whiteColor()
  }
}
// 创建视图
func open() {
  let vc = DemoViewController()
  presentViewController(vc, animated: true, completion: nil)
}
// 销毁视图
func close() {
  dismissViewControllerAnimated(true, completion: nil)
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[iOS 开发笔记 - CocoaPods]]></title>
            <link>https://imba97.com/posts/493</link>
            <guid isPermaLink="false">https://imba97.com/posts/493</guid>
            <pubDate>Mon, 17 Feb 2020 17:59:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>简介</h2>
<p>CocoaPods是专门为iOS工程提供第三方依赖库的管理工具，类似Node.js中的npm</p>
<!--more-->
<h2>安装</h2>
<pre><code class="language-bash"># 移除原生镜像
gem sources --remove https://rubygems.org/
# 添加国内镜像
gem sources --add https://gems.ruby-china.com/
# 安装CocoaPods
gem install cocoapods
# 设置
pod setup
</code></pre>
<h2>使用</h2>
<p>在<code>.xcodeproj</code>文件所在目录下</p>
<pre><code class="language-bash"># 初始化
pod init
</code></pre>
<p>此时目录下会创建一个<code>Podfile</code>的文件，编辑文件</p>
<p>如果使用<code>Swift</code>，需要在里面添加<code>use_frameworks!</code>这行代码，里面默认应该是注释的，打开即可</p>
<pre><code class="language-bash"># 查找需要的框架
pod search afn
</code></pre>
<p>如果报错<code>Unable to find a pod with name, author, summary, or description matching AFNetworking</code>，使用另一种方式安装<code>Cocoapods</code></p>
<pre><code class="language-bash">gem install cocoapods --pre
</code></pre>
<p>比如我们需要用<code>AFNetworking</code>，在<code>Podfile</code>中添加</p>
<pre><code class="language-bash">pod 'AFNetworking'
</code></pre>
<p>保存后在终端输入命令安装，普通安装会默认升级CocoaPods的spec仓库，速度会很慢，此时需要在安装时添加参数<code>--no-repo-update</code></p>
<pre><code class="language-bash"># 安装框架
pod install --no-repo-update
</code></pre>
<p>完成后在项目中会多一个<code>.cworkspace</code>的文件，关闭当前Xcode工程，之后打开项目都用<code>.cworkspace</code>这个文件</p>
<p><code>install</code>只是第一次安装时用，之后如果修改<code>Podfile</code>使用<code>update</code></p>
<pre><code class="language-bash">pod update --no-repo-update
</code></pre>
<p>如果报错<br>
<code>[!] Unable to add a source with url https://github.com/CocoaPods/Specs.git named cocoapods.</code><br>
<code>You can try adding it manually in /Users/bluepanda/.cocoapods/repos or via pod repo add.</code></p>
<p>使用以下方式手动进入<code>repo</code>目录，从Git上克隆master分支，其实主要原因应该是被墙，用梯子给git上一个代理，就可以了</p>
<pre><code class="language-bash"># 设置git代理，具体端口看梯子的设置
git config --global http.proxy 'socks5://127.0.0.1:1086'
git config --global https.proxy 'socks5://127.0.0.1:1086'
# 执行手动克隆
pod setup
pod repo remove master
cd ~/.cocoapods/repos
git clone https://github.com/CocoaPods/Specs master
</code></pre>
<p>如果报错<code>[!] CDN: trunk URL couldn't be downloaded: https://raw.githubusercontent.com/CocoaPods/Specs/master/Specs/0/0/e/AFNetwork/0.1.0/AFNetwork.podspec.json Response: Couldn't connect to server</code></p>
<p>需要在<code>Podfile</code>中添加以下代码</p>
<pre><code class="language-bash">source 'https://github.com/CocoaPods/Specs.git'
</code></pre>
<p>开启梯子，再执行安装命令</p>
<pre><code class="language-bash">pod install
</code></pre>
<h2>使用</h2>
<p>OC使用第三方框架时需要加载头文件</p>
<pre><code class="language-objective-c">#import &lt;AFNetworking/AFNetworking.h&gt;
</code></pre>
<p>Swift使用第三方框架需要加载Pods项目下Pods目录中的文件夹名称</p>
<pre><code class="language-swift">import AFNetworking
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Swift 笔记 - 网路访问]]></title>
            <link>https://imba97.com/posts/491</link>
            <guid isPermaLink="false">https://imba97.com/posts/491</guid>
            <pubDate>Sat, 15 Feb 2020 18:48:05 GMT</pubDate>
            <content:encoded><![CDATA[<h2>基本的网络请求</h2>
<pre><code class="language-swift">let url = NSURL(string: &quot;http://imba97.com/wp-json/wp/v2/posts/491&quot;)
NSURLSession.sharedSession().dataTaskWithURL(url!) {
  // 尾随闭包的参数，可省略回调函数的参数
  (data, _, _) -&gt; Void in
  // 函数体
  // 1. 反序列化
  NSJSONSerialization.JSONObjectWithData(data!, options: [NSJSONReadingOptions.MutableContainers, NSJSONReadingOptions.MutableLeaves])
}.resume()
</code></pre>
<!--more-->
<p><code>JSONObjectWithData</code>的<code>options</code>选项，OC中的按位枚举的方式改为数组的方式设置，如果不设置选项可以直接设为空数组<code>[]</code></p>
<h2>封装网络方法</h2>
<p>使用<code>AFNetworking</code>框架进行二次封装，首先使用Cocoapods加载第三方框架</p>
<p><code>NetworkTools.swift</code>文件</p>
<pre><code class="language-swift">import UIKit
import AFNetworking

// 枚举
enum requestMethod: String {
  case GET = &quot;GET&quot;
  case POST = &quot;POST&quot;
}

class NetworkTools: AFHTTPSessionManager {
  // 单例
  static let sharedTools: NetworkTools = {
    let tools = NetworkTools(baseURL: nil)
    // 设置反序列化数据格式，系统会自动将OC框架中的NSSet转换成Set
    tools.responseSerializer.acceptableContentTypes?.insert(&quot;text/html&quot;)
    return tools
  }()
}

// 扩展方法
extension NetworkTools {
  func request(method: requestMethod, URLString: String, parameters: [String: Any]?, finished: (result: Any?, error: NSError?)-&gt;()) {
    // 成功回调
    let success = { (task: NSURLSessionDataTask, result: Any) -&gt; Void in
      finished(result: result, error: nil)
    }
    // 失败回调
    let failure = { (task: NSURLSessionDataTask, error: NSError) -&gt; Void in
      print(error)
      finished(result: nil, error: error)
    }
    // 判断GET或POST
    if method == requestMethod.GET {
      GET(URLString, parameters: parameters, success: success, failure: failure)
    } else {
      POST(URLString, parameters: parameters, success: success, failure: failure)
    }
  }
}
</code></pre>
<p>调用，<code>ViewController.swift</code>文件</p>
<pre><code class="language-swift">import UIKit
class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    NetworkTools.sharedTools.request(requestMethod.GET, URLString: &quot;http://imba97.com/wp-json/wp/v2/posts/491&quot;, parameters: nil) { (result, error) -&gt; () in
      print(result)
    }
  }
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Swift 笔记 - 面向对象]]></title>
            <link>https://imba97.com/posts/489</link>
            <guid isPermaLink="false">https://imba97.com/posts/489</guid>
            <pubDate>Sun, 09 Feb 2020 22:59:20 GMT</pubDate>
            <content:encoded><![CDATA[<h2>类的定义</h2>
<p>所有的属性和方法都是全局共享的，不需要 import</p>
<pre><code class="language-swift">import UIKit
// 定义 Person 类，继承自 NSObject
class Person: NSObject {

  // 对象属性应该可变的，可选项允许变量为空
  var name: String

}
</code></pre>
<!--more-->
<h2>构造函数</h2>
<p>给属性分配空间，设置初始值，父类提供了构造函数需要对父类的构造函数进行扩展，使用关键字<code>override</code></p>
<pre><code class="language-swift">import UIKit
class Person: NSObject {

  var name: String

  override init() {
    // name初始化只能在 super.init() 上面
    name = &quot;Zhang San&quot;
    super.init()
    // name = &quot;Zhang San&quot; 再此处写会报错
  }
}
// 实例化
let p = Person()
print(p.name); // Zhang San
</code></pre>
<h2>函数重载</h2>
<p>函数名相同，参数个数和类型不同</p>
<pre><code class="language-swift">class Person: NSObject {
  var name: String
  var age: Int
  // 如果重载构造函数，系统默认构造函数就不能再被访问
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}
</code></pre>
<h2>KVC</h2>
<p>Key-value coding，通过Key名直接访问对象属性或赋值，而不需要调用明确的存取方法</p>
<pre><code class="language-swift">class Person: NSObject {
  var name: String?
  // 设置 age 的初始值，因为Int是基本类型，不存在 nil 的概念，所以不能写成 var age: Int?
  var age: Int = 0

  init(dict: [String: Any]) {
    super.init()
    setValuesForKeysWithDictionary(dict)
  }
}
// 实例化并传入参数 dict
let p = Person([&quot;name&quot;: &quot;Zhang San&quot;, &quot;age&quot;: 20])
</code></pre>
<h2>便利构造函数</h2>
<p>能够提供条件检测，允许返回<code>nil</code>，默认的构造函数必须要创建对象，便利构造函数必须以<code>self.</code>的方式调用其他构造函数创建对象</p>
<pre><code class="language-swift">class Person: NSObject {
  var name: String?
  var age: Int = 0

  init(dict: [String: Any]) {
    super.init()
    setValuesForKeysWithDictionary(dict)
  }

  convenience init?(name: String, age: Int) {
    if age &lt; 0 || age &gt; 100 {
      return nil
    }
    // 调用其他构造函数初始化属性
    self.init(dict: [&quot;name&quot;: name, &quot;age&quot;: age])
  }
}
let p = Person(&quot;name&quot;: &quot;Zhang San&quot;, age: 101)
// 一般解包时需要用 ! ，但此时会报错
print(&quot;name: \(p!.name), age: \(p!.age)&quot;)
// 需要用 ? 来解包，表示如果p为nil，就不会继续调用后续的属性或方法
print(&quot;name: \(p?.name), age: \(p?.age)&quot;); // name: nil, age: nil
</code></pre>
<p>对象释放方法，与<code>dealloc</code>类似，主要负责对象被销毁前的内存释放工作，不允许重载，不允许带参数，不允许直接调用，会先调用子类的释放函数再调用父类的</p>
<p>需要程序员销毁的内容</p>
<ul>
<li>通知 (不注销不会崩溃)</li>
<li>KVO (不注销会崩溃)</li>
<li>NSTimer (会对target (self) 进行强引用)</li>
</ul>
<pre><code class="language-swift">class Person: NSObject {
  deinit {
    print(&quot;Person deinit&quot;)
  }
}
</code></pre>
<h2>懒加载</h2>
<p>本质上是一个闭包，第一次访问属性时会执行后面的闭包，将返回值保存在属性中，下次访问就不会执行闭包，直接获取属性的值</p>
<p>如果没有lazy，会在<code>initWithCoder</code>方法中被调用，当二进制的<code>storyboard</code>被还原成视图控制器对象后，就会被调用</p>
<pre><code class="language-swift">import UIKit
class ViewController: UIViewController {
  // 懒加载 Person 类
  lazy var person: Person = {
    print(&quot;懒加载&quot;)
    return Person()
  }()

  // 还可以写成以下格式
  let personFunc = { () -&gt; Person in
    print(&quot;懒加载&quot;)
    return Person()
  }
  lazy var demoPerson: Person = self.personFunc()

  // 懒加载的简单写法
  lazy var demoPerson: Person = Person()

  // 闭包中的 self. 是不能省略的
  var name: String? = &quot;Zhang San&quot;
  lazy var title: String? = {
    return &quot;Mr &quot; + (self.name ?? &quot;&quot;)
  }
  // 输出 Mr Zhang San
}
</code></pre>
<h2>getter和setter</h2>
<p>在Swift中极少用，仅供参考<br>
OC中使用<code>getter</code>进行懒加载，而Swift提供了<code>lazy</code>关键字</p>
<pre><code class="language-swift">class Person: NSObject {
  private var _name: String
  var name: String? {
    get {
      // 返回 _成员变量的值
      return _name
    }
    get {
      // 使用 _成员变量记录新的值
      _name = newValue
    }
  }
}
// 使用
let p = Person()
p.name = &quot;Zhang San&quot;
print(p.name); // Zhang San
</code></pre>
<h2>read only</h2>
<p>只读属性，也叫<font color="red">计算型属性</font>，每次调用时都会执行大括号中的代码</p>
<p>只写<code>getter</code>方法</p>
<pre><code class="language-swift">print var _name: String?
var name: String? {
  get {
    return _name
  }
}
// 只读属性可以简写，省略 get{}
var name: String? {
  return _name
}
</code></pre>
<h3>懒加载与计算型属性的区别</h3>
<p>懒加载</p>
<ul>
<li>只计算一次</li>
<li>需要开辟单独的空间保存计算结果</li>
<li>闭包的代码再也不会被调用</li>
<li>如果计算量很大，需要提前准备</li>
</ul>
<p>计算型属性</p>
<ul>
<li>每次都要计算，浪费性能</li>
<li>不需要开辟额外的空间</li>
</ul>
<h2>类函数(静态函数)</h2>
<pre><code class="language-swift">import UIKit
class Person: NSObject {
  // 定义静态成员属性
  static var text: String = &quot;Test&quot;
  // 定义类函数(静态函数)
  class func staticFunc() {
    print(&quot;类函数&quot;)
  }
}
// 调用
</code></pre>
<h2>单例</h2>
<pre><code class="language-swift">import UIKit
class Person: NSObject {
  // 定义静态成员属性
  static var instance: Person?
  // 定义标识，只让diepatch_once中的代码运行一次
  static var onceToken: dispatch_once_t = 0
  // 提供全局访问类
  class func person() -&gt; Person {
    dispatch_once(&amp;onceToken) { () -&gt; Void in
      // 实例化类
      instance = Person()
    }
    // 强制解包并返回
    return instance!
  }
  // 单例的懒加载写法
  static let person2: Person = {
    return Person()
  }()
}
</code></pre>
<h2>extension</h2>
<p>对在已存在的类、类型进行扩展，但不能覆盖现有功能</p>
<h3>计算属性</h3>
<pre><code class="language-swift">extension Double {
  var km: Double { return self * 1000.0 }
  var m: Double { return self }
  var cm: Double { return self / 100.0 }
  var mm: Double { return self / 1000.0 }
  // 英尺
  var ft: Double { return self / 3.28084 }
}
// 使用
let threeFeet = 3.ft
print(&quot;3英尺是 \(threeFeet) 米&quot;); // 3英尺是 0.914399970739201 米
</code></pre>
<h3>扩展方法</h3>
<p>以下例子是对<code>Int</code>使用，也可以对自定义的类使用</p>
<pre><code class="language-swift">extension Int {
  func repetitions(task: () -&gt; Void) {
    for _ in 0..&lt;self {
      task()
    }
  }
}
// 使用（尾随闭包的形式）
3.repetitions {
  print(&quot;Hello&quot;); // Hello Hello Hello
}
// 使用（普通传参）
3.repetitions(task: {
    print(&quot;Hello&quot;); // Hello Hello Hello
})
</code></pre>
<h2>视图模型</h2>
<p>模型通常继承自<code>NSObject</code>，可以使用KVC设置属性，简化对象构造<br>
而视图模型没有父类，所有内容都要从头构造</p>
<p>视图模型是封装业务逻辑，通常没有复杂的属性</p>
<pre><code class="language-swift">import Foundation
class UserAccountViewModel {
  // 用户模型
  var account: UserAccount?
  // 构造函数
  init() {

  }
}
</code></pre>
<h2>协议</h2>
<p>类似接口，用于制定属性和方法，让其他遵守协议的地方实现</p>
<pre><code class="language-swift">import UIKit
protocol Pet {
  // 协议属性，如果只有 get 则是只读属性
  var name: String { get set }
  // 协议方法
  func playWith()
  func fed()
}
class Animal {
  var type: String = &quot;mammal&quot;
}
// 继承父类同时实现协议，需要先写父类，再写协议
class Dog: Animal, Pet {
  var name: String = &quot;Puppy&quot;
  func playWith() {
    print(&quot;wong&quot;)
  }
  func fed() {
    print(&quot;I love Bones&quot;)
  }
}
</code></pre>
<h2>属性观察者</h2>
<p>在属性即将赋值、赋值后进行操作，默认赋值和<code>init</code>初始化方法中的赋值不会触发属性观察者</p>
<pre><code class="language-swift">import UIKit
class person {
  var age = 0 {
    // 即将赋值时
    willSet(newAge) {
      print(&quot;原值：\(age)，即将被赋值为：\(newAge)&quot;)
    }
    // 赋值后
    didSet(oldAge) {
      print(&quot;原值：\(oldAge)，即将被赋值为：\(age)&quot;)
    }
  }
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Swift 笔记 - 基础语法]]></title>
            <link>https://imba97.com/posts/484</link>
            <guid isPermaLink="false">https://imba97.com/posts/484</guid>
            <pubDate>Fri, 07 Feb 2020 23:51:41 GMT</pubDate>
            <content:encoded><![CDATA[<h2>变量、常量</h2>
<pre><code class="language-swift">// 定义变量
var a = 10;
// 定义常量
let b = 10;
</code></pre>
<!--more-->
<p>数字可以使用<code>_</code>或添加额外的<code>0</code>增加可读性</p>
<pre><code class="language-swift">let a = 100_0000; // 1000000
let b = 001999; // 1999
</code></pre>
<p>字符串的拼接使用<code>+</code>，字符串嵌入变量使用<code>\()</code></p>
<pre><code class="language-swift">let hello = &quot;hello&quot;
print(hello + &quot; world&quot;); // hello world

let n = 100
let str = &quot;n is \(n)&quot;; // n is 100
</code></pre>
<p>可选类型，定义变量时设置可选类型，让变量可为 nil，因为调用某些函数时不一定会返回正确的结果</p>
<pre><code class="language-swift">import Foundation
let url = NSURL(string: &quot;http://www.baidu.com&quot;)
// 如果确认里面不是空，也可以使用“!”强制解析
print(url!)
</code></pre>
<h2>布尔型</h2>
<p>swift的布尔型只有<code>true</code>和<code>false</code>，不存在非零即真</p>
<h2>数组</h2>
<pre><code class="language-swift">let array = [&quot;a&quot;, &quot;b&quot;]
// 此时 array 自动获取到的类型是 String，之后就无法向数组追加其他类型的值
// 但可以手动指定 Any 类型
var array2 : [Any] = []
array2.append(&quot;string&quot;)
array2.append(10)
print(array2); // [&quot;string&quot;, 10]
// 打印数组
for item in array2 {
  print(item)
}
// 输出 string 10
</code></pre>
<h2>字典</h2>
<p>与数组定义一样，也是用方括号，是键值对</p>
<pre><code class="language-swift">let dict: [String: Any] = [&quot;name&quot;:&quot;Zhang San&quot;,&quot;age&quot;:17]
// 遍历
for (index, item) in dict {
  print(&quot;index: \(index), item: \(item)&quot;)
}
// 输出 index: name, item: Zhang San index: age, item: 17
</code></pre>
<h2>元组类型</h2>
<p>元组是由多个任意类型数据组成</p>
<pre><code class="language-swift">let position = (x: 10, y: 20, z: 1.5)
// 访问
print(position.x)
print(position.0)
</code></pre>
<p>定义时可省略名称</p>
<pre><code class="language-swift">let position = (10, 20, 1.5)
// 访问
print(position.0)
</code></pre>
<p>明确指定类型，但指定类型后就不能使用元素名了</p>
<pre><code class="language-swift">let position: (Int, Int, Float, String) = (10, 20, 1.5, &quot;hello&quot;)
</code></pre>
<p>声明变量可用元组来接收</p>
<pre><code class="language-swift">var (x, y) = (10, 20)
print(x); // 10
print(y); // 20

var position = (10, 20)
var (newX, newY) = position
print(newX); // 10
print(newY); // 20
</code></pre>
<p>使用<code>_</code>来忽略接收某个值</p>
<pre><code class="language-swift">var position = (10, 20)
var (_, newY) = position
print(newY); // 20
</code></pre>
<p><strong>空元组 === Void 类型</strong></p>
<h2>范围运算符</h2>
<p>范围运算符是<code>...</code>和<code>..&lt;</code>，可以指定一个范围</p>
<pre><code class="language-swift">let a = 0...5
let b = 0..&lt;5
for i in a {
  print(i)
}
// a 会输出 0 到 5
// b 会输出 0 到 4
print(a); // 0..&lt;6 会自动转换成 ..&lt;
print(b); // 0..&lt;5
</code></pre>
<h2>溢出运算符</h2>
<p>可以避免变量值超出该变量类型的限制<br>
溢出运算符：<code>&amp;+</code>、<code>&amp;-</code>、<code>&amp;*</code>、<code>&amp;/</code>、<code>&amp;%</code></p>
<pre><code class="language-swift">// 获取 UInt8 类型的最大值
let x = UInt8.max; // 255
let y = x + 1; // 此时会报错 因为超出最大值
// 改用溢出运算符
let y = x &amp;+ 1; // 超出部分会从该类型的最小值开始计算  y == 0
</code></pre>
<h2>as</h2>
<p>没有符号修饰的as一般只有三个地方会用，意义是做桥接</p>
<ol>
<li>String as NSString</li>
<li>NSArray as [array]</li>
<li>NSDictionary as [String: AnyObject]</li>
</ol>
<h2>流程控制</h2>
<p>流程控制语句必须要有大括号（除三目运算符）</p>
<h3>for-in</h3>
<p>for-in 和 范围运算符</p>
<pre><code class="language-swift">for i in 1...3 {
  print(i)
}
// 会打出 1 到 3
</code></pre>
<p>使用<code>_</code>忽略范围中的值</p>
<pre><code class="language-swift">for _ in 1...3 {
  print(&quot;hi&quot;)
}
// 会打出3次“hi”
</code></pre>
<p>设置标签，可以在<code>break</code>指定标签，跳出指定的循环体</p>
<pre><code class="language-swift">name:
for _ 1...3 {
  print(&quot;一次新的外层循环&quot;)
  for i in 1...5 {
    print(i)
    break name // 会跳出 name 这个循环，也就是外层循环
  }
}
// 会输出1次 “一次新的外层循环”和1次“1”
</code></pre>
<h3>if</h3>
<p>可以不写括号</p>
<pre><code class="language-swift">let n = 10
if n == 10 {
  print(n); // 10
}
</code></pre>
<h3>if-let</h3>
<p>判断时声明变量，如果不为 nil 则给变量赋值，多用于判断可选类型</p>
<pre><code class="language-swift">// 通常
func say(str: String?) {
  let v: String! = str
  if v != nil {
    print(v)
  }
}
// 使用 if-let
func say2(str: String?) {
  if let v = str {
    print(v)
  }
}
let s: String? = &quot;hi&quot;
say2(str: s); // hi
</code></pre>
<h3>guard-let</h3>
<p>如果if中的代码会很长，可以使用 guard</p>
<pre><code class="language-swift">// 通常
func say(str: String?) {
  let v: String! = str
  if v == nil {
    return
  }
}
// 使用 guard-let
func say2(str: String?) {
  guard let v = str else { return }
}
</code></pre>
<h3>switch</h3>
<p>可以接收绝大多数类型，结束时不需要<code>break</code>关键字，每个<code>case</code>中至少包含一条可执行语句，必须包含所有可能的条件</p>
<p>多条件使用<code>,</code>分割</p>
<pre><code class="language-swift">let number = 10;
switch number {
  case 10,11,12:
    print(&quot;通过&quot;)
  case 20:
    print(&quot;未授权&quot;)
  default:
    print(&quot;未知状态&quot;)
}
// 输出“通过”
</code></pre>
<p>switch 和 范围运算符</p>
<pre><code class="language-swift">let number = 10;
switch number {
  case 10...12:
    print(&quot;通过&quot;)
  case 20:
    print(&quot;未授权&quot;)
  default:
    print(&quot;未知状态&quot;)
}
// 与上面的代码完全一致 输出“通过”
</code></pre>
<p>使用 switch 和 元组数据 和 用<code>_</code>忽略某个判断值，来判断某个点在下图什么位置</p>
<p><img src="https://imba97.com/uploads/2020/02/swift-1-1.png" alt=""></p>
<pre><code class="language-swift">let point = (0, 0)
switch point {
  case (0, 0):
    print(&quot;在原点&quot;)
  case (_, 0):
    print(&quot;在X轴&quot;)
  case (0, _):
    print(&quot;在Y轴&quot;)
  case (-2...2, -2...2):
    print(&quot;在蓝色矩形框内&quot;)
  default:
    print(&quot;在蓝色矩形框外&quot;)
}
</code></pre>
<p>switch 的数据绑定，在匹配同时，可以将 switch 中的值绑定给一个常量或者变量</p>
<pre><code class="language-swift">let point = (10, 5)
switch point {
  case (let x, 0):
    print(&quot;在X轴，X坐标：\(x)&quot;)
  case (0, let y):
    print(&quot;在Y轴，Y坐标：\(y)&quot;)
  case let (x, y):
    print(&quot;X坐标：\(x)，Y坐标：\(y)&quot;)
}
</code></pre>
<p>使用 switch 的数据绑定 和 where 判断条件成立，来判断某个点是否在下图紫线或绿线上</p>
<p><img src="/uploads/2020/02/swift-1-2.png" alt=""></p>
<pre><code class="language-swift">let point = (0, 0)
switch point {
  case let (x, y) where x == y:
    print(&quot;在绿线上&quot;)
  case let (x, y) wehre x == -y
    print(&quot;在紫线上&quot;)
  default:
    print(&quot;在其他位置&quot;)
}
</code></pre>
<h4>fallthrough</h4>
<p>执行完当前 case 后接着执行下一个 case 或 default，等于PHP不写 break 的效果</p>
<pre><code class="language-swift">let num = 10;
switch num {
  case 10:
    print(&quot;10&quot;)
    fallthrough
  case 20:
    print(&quot;20&quot;)
  default:
    print(&quot;num 是其他值&quot;)
}
// 会输出 10 20
</code></pre>
<h2>函数语法</h2>
<h3>定义函数</h3>
<p>func 函数名(形参名) -&gt; 返回类型</p>
<pre><code class="language-swift">func sum(num1, num2) -&gt; Int {
  return num1 + num2
}
</code></pre>
<p>定义无返回值函数</p>
<ol>
<li>
<p>func 函数名(形参名) { 函数体 }</p>
</li>
<li>
<p>func 函数名(形参名) -&gt; Void { 函数体 }</p>
</li>
<li>
<p>func 函数名(形参名) -&gt; () { 函数体 }</p>
</li>
</ol>
<p>返回元组的函数</p>
<pre><code class="language-swift">func getPoint() -&gt; (Int, Int) {
  return (10, 10)
}

func getPepole(id: Int) -&gt; (name: String, age: Int) {
  if id == 1 {
    return (&quot;Zhang San&quot;, 20)
  } else id id == 2 {
    return (&quot;Li Si&quot;, 21)
  }

  return (&quot;Unknown&quot;, 0)
}

var pepole = getPepole(id: 1)
print(pepole); // (&quot;Zhang San&quot;, 20)
</code></pre>
<h3>外部参数</h3>
<p>定义外部参数，增加代码可读性</p>
<p>func 函数名(<strong>外部参数名</strong> 形参名) -&gt; 返回类型</p>
<pre><code class="language-swift">func addStudent(name: String, stu_age age: Int, stu_no no: Int) {
  print(&quot;添加学生：name: \(name)，age: \(age)，no: \(no)&quot;)
}
// 调用
addStudent(name: &quot;Zhang San&quot;, stu_age: 20, stu_no: 19)
// 输出“添加学生：name: Zhang San，age: 20，no: 19”
</code></pre>
<p>函数默认是常量参数，如果需要变动参数的值，需要额外声明变量参数</p>
<pre><code class="language-swift">func addStar(str: String) -&gt; String {
    var str = str;
  for _ in 1...3 {
    str += &quot;*&quot;;
  }
  return str;
}
var starStr = addStar(str: &quot;hello&quot;)
print(starStr); // hello***
</code></pre>
<p>使用“_”忽略外部参数</p>
<pre><code class="language-swift">func sum(_ num1: Int, _ num2: Int) -&gt; Int {
  return num1 + num2;
}
var numSum = sum(1, 2)
print(numSum); // 3
</code></pre>
<p>忽略后就不能再写成 <code>sum(num1: 1, num2: 2)</code></p>
<h3>输入输出参数</h3>
<p>类似于C语言中的指针</p>
<pre><code class="language-c">// 定义指针参数
void change(int * a) {
  *a = 10;
}
int num = 20;
change(&amp;num);
print(num); // 10
</code></pre>
<p>对比</p>
<pre><code class="language-swift">func change(_ a: inout Int) {
  a = 10
}
var num = 20
change(&amp;num)
print(num); // 10
</code></pre>
<h3>闭包</h3>
<p>类似于 OC 中的 block，在大括号内定义参数、返回值和函数体。<br>
用 in 区分参数、返回值和函数体</p>
<pre><code class="language-swift">let sumFunc = {
  (num1: Int, num2: Int) -&gt; Int
  in
  let sumNum: Int = num1 + num2
return sumNum
}
let n = sumFunc(10, 20)
print(n)
</code></pre>
<p>如果没参数和返回值，那么 in 可以省略</p>
<pre><code class="language-swift">let printStar = {
  print(&quot;***&quot;)
}
</code></pre>
<p>一般用于耗时操作的回调</p>
<pre><code class="language-swift">func loadData(finshed: (html: String) -&gt; Void) {
  dispatch_async(dispatch_get_global_queue(0, 0), { () -&gt; Void in
    print(&quot;耗时操作 - 开启新线程&quot;)

    dispatch_async(dispatch_get_main_queue(), {
      print(&quot;完成 - 主线程&quot;)
      let htmlData = &quot;&lt;p&gt;获取的HTML数据&lt;/p&gt;&quot;
      finshed(html: htmlData); //回调函数
    })
  })
}
// 调用并传入回调函数
loadData { (html) -&gt; Void in
  print(&quot;执行回调函数&quot;)
}
</code></pre>
<h3>尾随闭包</h3>
<p>只是写法格式，如果一个函数的最后一个参数是函数类型，则可以使用以下写法</p>
<pre><code class="language-swift">// 普通写法
dispatch_async(dispatch_get_global_queue(0, 0), { () -&gt; Void in
  // 函数体
})
// 尾随闭包写法
dispatch_async(dispatch_get_global_queue(0, 0)) { () -&gt; Void in
  // 函数体
}
</code></pre>
<h3>异常捕获</h3>
<pre><code class="language-swift">let url = NSURL(string: &quot;http://imba97.com/wp-json/wp/v2/posts/484&quot;)
NSURLSession.sharedSession().dataTaskWithURL(url!) {
  // 尾随闭包的参数，可省略回调函数的参数
  (data, _, _) -&gt; Void in
  // 函数体
  do {
    let resule = try NSJSONSerialization.JSONObjectWithData(data!, options: [NSJSONReadingOptions.MutableContainers, NSJSONReadingOptions.MutableLeaves])
    print(result)
  } catch { // 如果反序列化失败，能够捕捉到 json 失败的原因而不会崩溃
    print(error)
  }

  // guard let 方式
  guard let resule = try? NSJSONSerialization.JSONObjectWithData(data!, options: [NSJSONReadingOptions.MutableContainers, NSJSONReadingOptions.MutableLeaves]) else {
    print(&quot;反序列化失败&quot;)
    return
  }
}.resume()
</code></pre>
<h2>与OC混写</h2>
<p>Swift与OC可以互相调用</p>
<p>首先引入Swift的头文件，引用格式为<code>ProductName-Swift.h</code>，ProductName可以纯中文，但不能有包含中文、数字和“-”的组合，如果有可在<code>Build Settings -&gt; Product Name</code>中更改</p>
<p>假设<code>Product Name</code>为<code>SwiftTest</code></p>
<p>Swift的Person类：</p>
<pre><code class="language-swift">import UIKit
class Person: NSObject {
  static func say() {
    print(&quot;这里是Swift的Person类&quot;)
  }
}
</code></pre>
<p>Objective-C调用Person</p>
<pre><code class="language-objective-c">// OC 代码，加载Swift头文件
#import &quot;SwiftTest-Swift.h&quot;

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];
}

- (void)touchesBegan:(NSSet&lt;UITouch *&gt; *)touches withEvent:(UIEvent *)event {
  NSLog(@&quot;%@&quot;, [Person say]);
}
@end

// DemoController.h
+ (void)say
// DemoController.m
import UIKit
class DemoController: UIViewController {
+ (void)say {
  NSLog(@&quot;这里是OC的静态方法&quot;);
}
}
</code></pre>
<p>Swift调用OC的say只需引用相应头文件即可直接调用</p>
<pre><code class="language-swift">#import DemoController.h
DemoController.say(); // 这里是OC的静态方法
</code></pre>
<p>Swift调用OC一般不会有问题，但OC无法访问Swift中的特输入法，如枚举</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[2019总结]]></title>
            <link>https://imba97.com/posts/478</link>
            <guid isPermaLink="false">https://imba97.com/posts/478</guid>
            <pubDate>Tue, 31 Dec 2019 23:59:59 GMT</pubDate>
            <content:encoded><![CDATA[<p>2019依然没目标没方向，引用“<a href="https://www.bilibili.com/blackboard/activity-goodbye2019.html">再见2019</a>”的评论：</p>
<blockquote>
<p>间歇性踌躇满志，持续性混吃等死。</p>
</blockquote>
<!--more-->
<p>干了几件事：</p>
<h1>程序</h1>
<ol>
<li><strong>Btools</strong></li>
</ol>
<p>这个算是件比较大的事了，从 2018-12-22 开始边看教程边做的，然后今年的1月16日发布到了 Chrome web store，在这之前其实我做了不少JS的小程序，都整合到了插件里，目前为止插件使用人数：<img src="https://camo.githubusercontent.com/524dbe2d6528fd4adc63ec418aaf94092561cb7f/68747470733a2f2f696d672e736869656c64732e696f2f6368726f6d652d7765622d73746f72652f75736572732f636f64676f666b676f62626d67676c636963636a61626970646c6765666e63682e737667" alt=""> 。</p>
<p><a href="https://github.com/imba97/Btools">GitHub →</a></p>
<ol start="2">
<li><strong>とあるCMS</strong></li>
</ol>
<p>一个生成静态文章用的，目前的功能只有简单的生成首页、详情页、文章页，但可以高度自由定制。使用PHP框架Laravel做的，也是现学现卖。</p>
<p><a href="https://github.com/imba97/to-a-ru-CMS">GitHub →</a></p>
<ol start="3">
<li><strong>WdnmdApi</strong></li>
</ol>
<p>自己看教程撸的PHP框架，专注API开发。进度并没多少，边开发应用边完善框架。</p>
<p><a href="https://github.com/imba97/WdnmdApi">GitHub →</a></p>
<h1>网站</h1>
<ol>
<li><a href="http://btools.cc">btools.cc</a></li>
</ol>
<p>Btools浏览器插件的官网。</p>
<ol start="2">
<li><a href="http://imba97.com">imba97.com</a></li>
</ol>
<p>本站，个人博客。</p>
<ol start="3">
<li>其他还有一些零星的都是之前的了，现在也没怎么维护。</li>
</ol>
<h1>运动、健康</h1>
<p>间接性玩起了滑板，偶尔一时兴起去跑个步，睡觉前做几个俯卧撑，公司组织的羽毛球，其他基本就没了。</p>
<p>可能患上了鼻炎。</p>
<hr>
<p>2020继续混吃等死。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[《难忘的初中一课》]]></title>
            <link>https://imba97.com/posts/482</link>
            <guid isPermaLink="false">https://imba97.com/posts/482</guid>
            <pubDate>Sun, 29 Dec 2019 23:53:00 GMT</pubDate>
            <content:encoded><![CDATA[<style>.article-text-indent p{text-indent:2em}</style>
<div class="article-text-indent">
<p>回想起曾经最讨厌的就是学习，如今才明白学习并不是为了一句“考大学才有出路”，也不能因为一句“现在这个社会不需要学历”作为理由不去学习。曾经以为只是长辈的大道理，如今不禁感叹——人在无时无刻都需要学习。</p>
<!--more-->
<p>初中是一个重要的分水岭，而初中的学习并不是去理解掌握知识，因为这些知识本身并没有太大的难度，所以我认为它是教会你怎么去学习和自身修养提高，甚至是如何做人。记得在我初中的一堂音乐课里，老师说：“之后某一次音乐课校领导会来参观，有没有谁想到台上唱歌的？流行歌、音乐课本上的歌什么都可以”。我想着就当一次免费的卡拉OK，于是举了手。环顾四周发现原来只有我一个人举了手。</p>
<p>这种简单的心态让我有了一次难忘的经历，当时音乐课上就唱了一首林俊杰的《三国》，虽然我认为歌声并不是很美妙，但下课后老师把我留下来说：“我真是对你刮目相看，原来以为你只是一个课上调皮捣蛋的学生······”。我也没想到以前经常点名批评我、告状到班主任那里的音乐老师，竟然会对我表示认可。那次之后又在同学们面前唱过几次，虽然最后不知道什么原因校领导没有参观音乐课，但这节课对我的意义远比在校领导面前表演要重大。</p>
<p>这节课教会了我被认可的感受，所以我现在也会去认可他人，不会轻易否定他人。那时虽然并没想到这些，但现在我的这种“认可他人”思想的形成，一切的起源都是一节课、一个简单的心态和一个简单的认可。</p>
<p>我很感谢我的老师，没有因为对原来的我印象差就会去否定我、觉得我做任何事都不行，反而发现了我的特长就会去支持我。希望每个人都能经历自己最难忘的一课，也希望每个人都不要轻易否定他人。</p>
<p>这就是我难忘的初中一课。</p>
</div>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[PhpStorm 破解]]></title>
            <link>https://imba97.com/posts/480</link>
            <guid isPermaLink="false">https://imba97.com/posts/480</guid>
            <pubDate>Wed, 25 Dec 2019 16:37:13 GMT</pubDate>
            <content:encoded><![CDATA[<p>打扰了，捣鼓了很久一直没成功，直到看到一个教程，所以做个记录。</p>
<p>教程出自：<a href="https://zhile.io/2018/08/17/jetbrains-license-server-crack.html">《Jetbrains系列产品2019.3.1最新激活方法[持续更新]》</a>，本文中部分内容是直接摘抄。</p>
<!--more-->
<ol>
<li>
<p>下载破解用的jar包，并放到当前用户目录下，比如我的就是：<code>C:\User\imba97</code>这个目录下，放在<code>bin</code>目录好像不行，我反正是不行。<a href="https://pan.baidu.com/s/148Nqzerpt_9cCrbJ_WnoPQ"><strong>百度云下载</strong></a>，提取码：<code>7i1d</code></p>
</li>
<li>
<p>打开IDE，第一次开的话选试用，然后从菜单栏打开<code>Help -&gt; Edit Custom VM Options ...</code>，在最后新增以下代码：</p>
</li>
</ol>
<pre><code>-javaagent:C:\Users\imba97\jetbrains-agent.jar
</code></pre>
<p>用户名和文件名根据情况变更。</p>
<ol start="3">
<li>
<p>重启IDE</p>
</li>
<li>
<p>点击<code>Help -&gt; Register...</code>，支持两种注册方式：License server 和 Activation code。</p>
<p>(1) License server方式：点击<code>Discover Server</code>自动填充或者手动填写<code>http://jetbrains-license-server</code>。<br>
(2) Activation code方式：使用下面的激活码激活：</p>
</li>
</ol>
<pre><code class="language-text">3AGXEJXFK9-eyJsaWNlbnNlSWQiOiIzQUdYRUpYRks5IiwibGljZW5zZWVOYW1lIjoiaHR0cHM6Ly96aGlsZS5pbyIsImFzc2lnbmVlTmFtZSI6IiIsImFzc2lnbmVlRW1haWwiOiIiLCJsaWNlbnNlUmVzdHJpY3Rpb24iOiIiLCJjaGVja0NvbmN1cnJlbnRVc2UiOmZhbHNlLCJwcm9kdWN0cyI6W3siY29kZSI6IklJIiwiZmFsbGJhY2tEYXRlIjoiMjA4OS0wNy0wNyIsInBhaWRVcFRvIjoiMjA4OS0wNy0wNyJ9LHsiY29kZSI6IkFDIiwiZmFsbGJhY2tEYXRlIjoiMjA4OS0wNy0wNyIsInBhaWRVcFRvIjoiMjA4OS0wNy0wNyJ9LHsiY29kZSI6IkRQTiIsImZhbGxiYWNrRGF0ZSI6IjIwODktMDctMDciLCJwYWlkVXBUbyI6IjIwODktMDctMDcifSx7ImNvZGUiOiJQUyIsImZhbGxiYWNrRGF0ZSI6IjIwODktMDctMDciLCJwYWlkVXBUbyI6IjIwODktMDctMDcifSx7ImNvZGUiOiJHTyIsImZhbGxiYWNrRGF0ZSI6IjIwODktMDctMDciLCJwYWlkVXBUbyI6IjIwODktMDctMDcifSx7ImNvZGUiOiJETSIsImZhbGxiYWNrRGF0ZSI6IjIwODktMDctMDciLCJwYWlkVXBUbyI6IjIwODktMDctMDcifSx7ImNvZGUiOiJDTCIsImZhbGxiYWNrRGF0ZSI6IjIwODktMDctMDciLCJwYWlkVXBUbyI6IjIwODktMDctMDcifSx7ImNvZGUiOiJSUzAiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiUkMiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiUkQiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiUEMiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiUk0iLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiV1MiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiREIiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiREMiLCJmYWxsYmFja0RhdGUiOiIyMDg5LTA3LTA3IiwicGFpZFVwVG8iOiIyMDg5LTA3LTA3In0seyJjb2RlIjoiUlNVIiwiZmFsbGJhY2tEYXRlIjoiMjA4OS0wNy0wNyIsInBhaWRVcFRvIjoiMjA4OS0wNy0wNyJ9XSwiaGFzaCI6IjEyNzk2ODc3LzAiLCJncmFjZVBlcmlvZERheXMiOjcsImF1dG9Qcm9sb25nYXRlZCI6ZmFsc2UsImlzQXV0b1Byb2xvbmdhdGVkIjpmYWxzZX0=-WGTHs6XpDhr+uumvbwQPOdlxWnQwgnGaL4eRnlpGKApEEkJyYvNEuPWBSrQkPmVpim/8Sab6HV04Dw3IzkJT0yTc29sPEXBf69+7y6Jv718FaJu4MWfsAk/ZGtNIUOczUQ0iGKKnSSsfQ/3UoMv0q/yJcfvj+me5Zd/gfaisCCMUaGjB/lWIPpEPzblDtVJbRexB1MALrLCEoDv3ujcPAZ7xWb54DiZwjYhQvQ+CvpNNF2jeTku7lbm5v+BoDsdeRq7YBt9ANLUKPr2DahcaZ4gctpHZXhG96IyKx232jYq9jQrFDbQMtVr3E+GsCekMEWSD//dLT+HuZdc1sAIYrw==-MIIElTCCAn2gAwIBAgIBCTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTE4MTEwMTEyMjk0NloXDTIwMTEwMjEyMjk0NlowaDELMAkGA1UEBhMCQ1oxDjAMBgNVBAgMBU51c2xlMQ8wDQYDVQQHDAZQcmFndWUxGTAXBgNVBAoMEEpldEJyYWlucyBzLnIuby4xHTAbBgNVBAMMFHByb2QzeS1mcm9tLTIwMTgxMTAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5ndaik1GD0nyTdqkZgURQZGW+RGxCdBITPXIwpjhhaD0SXGa4XSZBEBoiPdY6XV6pOfUJeyfi9dXsY4MmT0D+sKoST3rSw96xaf9FXPvOjn4prMTdj3Ji3CyQrGWeQU2nzYqFrp1QYNLAbaViHRKuJrYHI6GCvqCbJe0LQ8qqUiVMA9wG/PQwScpNmTF9Kp2Iej+Z5OUxF33zzm+vg/nYV31HLF7fJUAplI/1nM+ZG8K+AXWgYKChtknl3sW9PCQa3a3imPL9GVToUNxc0wcuTil8mqveWcSQCHYxsIaUajWLpFzoO2AhK4mfYBSStAqEjoXRTuj17mo8Q6M2SHOcwIDAQABo4GZMIGWMAkGA1UdEwQCMAAwHQYDVR0OBBYEFGEpG9oZGcfLMGNBkY7SgHiMGgTcMEgGA1UdIwRBMD+AFKOetkhnQhI2Qb1t4Lm0oFKLl/GzoRykGjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBggkA0myxg7KDeeEwEwYDVR0lBAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQBonMu8oa3vmNAa4RQP8gPGlX3SQaA3WCRUAj6Zrlk8AesKV1YSkh5D2l+yUk6njysgzfr1bIR5xF8eup5xXc4/G7NtVYRSMvrd6rfQcHOyK5UFJLm+8utmyMIDrZOzLQuTsT8NxFpbCVCfV5wNRu4rChrCuArYVGaKbmp9ymkw1PU6+HoO5i2wU3ikTmRv8IRjrlSStyNzXpnPTwt7bja19ousk56r40SmlmC04GdDHErr0ei2UbjUua5kw71Qn9g02tL9fERI2sSRjQrvPbn9INwRWl5+k05mlKekbtbu2ev2woJFZK4WEXAd/GaAdeZZdumv8T2idDFL7cAirJwcrbfpawPeXr52oKTPnXfi0l5+g9Gnt/wfiXCrPElX6ycTR6iL3GC2VR4jTz6YatT4Ntz59/THOT7NJQhr6AyLkhhJCdkzE2cob/KouVp4ivV7Q3Fc6HX7eepHAAF/DpxwgOrg9smX6coXLgfp0b1RU2u/tUNID04rpNxTMueTtrT8WSskqvaJd3RH8r7cnRj6Y2hltkja82HlpDURDxDTRvv+krbwMr26SB/40BjpMUrDRCeKuiBahC0DCoU/4+ze1l94wVUhdkCfL0GpJrMSCDEK+XEurU18Hb7WT+ThXbkdl6VpFdHsRvqAnhR2g4b+Qzgidmuky5NUZVfEaZqV/g==
</code></pre>
<p>如果你需要自定义License name，请访问：<a href="https://zhile.io/custom-license.html">《Custom license》</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】快递被盗事件]]></title>
            <link>https://imba97.com/posts/469</link>
            <guid isPermaLink="false">https://imba97.com/posts/469</guid>
            <pubDate>Tue, 26 Nov 2019 00:14:38 GMT</pubDate>
            <content:encoded><![CDATA[<p>莫得稿子。</p>
<!--more-->
<p>我，看着他的表情<br>
先是从蒙圈，到怀疑<br>
再到四处张望有没有摄像头 特别好玩<br>
当他发现并没有被摄像头拍下，的那种失落感<br>
接着一丝念头略过他的脑中促使他拿起手机拍下证据<br>
但又转念一想自己拍下来也没什么用的时候<br>
又失落的把包装作为垃圾，分了分类丢进垃圾桶</p>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/av77042183">https://www.bilibili.com/video/av77042183</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[搭建PHP环境 Apache PHP MySQL]]></title>
            <link>https://imba97.com/posts/467</link>
            <guid isPermaLink="false">https://imba97.com/posts/467</guid>
            <pubDate>Tue, 19 Nov 2019 19:02:43 GMT</pubDate>
            <content:encoded><![CDATA[<p>PHP非集成环境的搭建笔记。</p>
<p>下载地址：</p>
<ul>
<li><a href="https://www.apachehaus.com/cgi-bin/download.plx">Apache</a></li>
<li><a href="https://windows.php.net/download">PHP</a></li>
<li><a href="https://dev.mysql.com/downloads/mysql/">MySQL</a></li>
</ul>
<!--more-->
<h1>Apache 和 PHP 的配置</h1>
<p>Apache 配置文件：<code>Apache/conf/httpd.conf</code></p>
<pre><code class="language-shell"># 监听端口
Listen 80

# 服务器名 可能是改了监听端口才需要改这里
ServerName localhost:80

# 设置常量 根据你的Apache路径设置
Define SRVROOT &quot;E:/Apache&quot;

# 设置 Apache 的路径
ServerRoot &quot;${SRVROOT}&quot;

# php7_module (新增)
LoadModule php7_module &quot;E:/phprumtime/php-7.3.11/php7apache2_4.dll&quot;

# 线程安全 (新增)
LoadFile &quot;E:/phprumtime/php-7.3.11/php7ts.dll&quot;

# 关联PHP (新增)
&lt;FilesMatch &quot;\.php$&quot;&gt;
  setHandler application/x-httpd-php
&lt;/FilesMatch&gt;

# 设置 php.ini 所在文件夹 (新增)
&lt;IfModule php7_module&gt;
    PHPIniDir &quot;E:/phprumtime/php-7.3.11&quot;
&lt;/IfModule&gt;

# 设置项目目录
DocumentRoot &quot;${SRVROOT}/htdocs&quot;
&lt;Directory &quot;${SRVROOT}/htdocs&quot;&gt;
    # 略···
&lt;/Directory&gt;
</code></pre>
<p>PHP 配置文件：<code>PHP/php.ini</code></p>
<p>如果不存在你需要复制<code>php.ini-development</code>或<code>php.ini-production</code>，在重命名为<code>php.ini</code>即可。</p>
<pre><code class="language-shell"># 配置扩展目录 根据你的路径设置
extension_dir = &quot;E:/PHP/ext&quot;

# 根据需要开启扩展
extension=mysqli
# ···

# 设置中国时区 (可选)
date.timezone = PRC
</code></pre>
<h1>使用 Apache</h1>
<p>首先用管理员打开终端，切换目录到<code>Apache/bin</code>下</p>
<pre><code class="language-shell"># 安装 不成功的话在前面加 .\ 试试
httpd -k install
</code></pre>
<p>成功会显示以下信息：</p>
<p><code>The 'Apache2.4' service is successfully installed.</code><br>
<code>Testing httpd.conf....</code><br>
<code>Errors reported here must be corrected before the service can be started.</code></p>
<p>然后开启 Apache</p>
<pre><code class="language-shell">httpd -k start
</code></pre>
<p>至此你就可以访问并运行PHP程序了</p>
<h1>MySQL 的配置</h1>
<p>MySQL 配置文件：<code>MySQL/my.ini</code>，如果没有则自己创建</p>
<pre><code class="language-shell">[client]
# 设置客户端字符集
default-character-set=utf8mb4

[mysqld]
# 设置端口
port = 3306
# 设置mysql的安装目录
basedir=E:\\MySQL
# 设置 mysql数据库的数据的存放目录
datadir=E:\\MySQL\\sqldata
# 允许最大连接数
max_connections=20
# 设置服务器字符集
character-set-server=utf8mb4
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
</code></pre>
<p>使用管理员打开终端，进入 MySQL/bin 目录</p>
<p>初始化数据库</p>
<pre><code class="language-shell">mysqld --initialize --user=root --console
</code></pre>
<p>成功会提示：<code>[Server] A temporary password is generated for root@localhost: _9k;Por&lt;3!NN</code></p>
<p>后面的<code>_9k;Por&lt;3!NN</code>就是初始化默认的随机密码</p>
<pre><code class="language-shell"># 安装服务
mysqld --install

# 开启服务
net start mysql
</code></pre>
<p>安装时如果提示<code>Install/Remove of the Service Denied!</code>说明你没使用管理员打开终端</p>
<p>如果没记录下密码，只需要删掉my.ini中设置的<code>datadir</code>目录，再重新初始化一遍即可</p>
<p>接下来就可以使用服务器连接工具或者终端，使用上面的密码连接数据库后再修改密码即可</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[如何手动找回B站失效视频]]></title>
            <link>https://imba97.com/posts/463</link>
            <guid isPermaLink="false">https://imba97.com/posts/463</guid>
            <pubDate>Tue, 05 Nov 2019 17:29:45 GMT</pubDate>
            <content:encoded><![CDATA[<!-- BV转AV - 参考 http://bv2av.com/ -->
<script>var avbv={table:'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF',xor:177451812,add:8728348608,s:[11,10,3,8,4,6]};var isIziToast=typeof iziToast==='object'&&typeof iziToast.show==='function';function bv2av(bv){var errorMessage='';if(!/[bB][vV][fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF]{10}/.test(bv)){errorMessage='BV号有误'}if(errorMessage!==''){if(isIziToast){iziToast.show({title:'错误',message:errorMessage,class:'shadow',position:'topRight',backgroundColor:'#FF6666',titleColor:'#ffffff',messageColor:'#ffffff',iconColor:'#ffffff',progressBarColor:'#ffffff',icon:'fa fa-check',timeout:3000})}else{alert(errorMessage)}return false}var result=0;for(var i=0;i<6;i++){result+=avbv.table.indexOf(bv[avbv.s[i]])*Math.pow(58,i)}var av=(result-avbv.add^avbv.xor);return 'av'+av}</script>
<p>以Chrome浏览器举例，首先右键失效视频，检查</p>
<p><img src="https://imba97.com/uploads/2019/11/1.png" alt=""></p>
<!--more-->
<p>找到父级的<code>li</code>标签，有一个<code>data-aid</code>属性，这里就是BV号</p>
<p><img src="/uploads/2019/11/2.png" alt=""></p>
<p>打开 <a href="https://www.biliplus.com/">https://www.biliplus.com/</a> 在里面搜索这个AV号或BV号</p>
<p>或者直接把AV号或BV号粘贴在这：<input type="text" id="avNo"><input type="button" id="avBtn" onclick="msg()" style="border:none;border-radius:5px;margin-left:5px;padding:6px;cursor:pointer;" value="搜索"></p>
<script>function msg(){var avNo=document.querySelector('#avNo');var bv2avNo=bv2av(avNo.value);if(bv2avNo===false) return;if(!/^(a|A)(v|V)(\d+)$/.test(bv2avNo)){if(isIziToast){iziToast.show({title:'错误',message:'AV号有误',class:'shadow',position:'topRight',backgroundColor:'#FF6666',titleColor:'#ffffff',messageColor:'#ffffff',iconColor:'#ffffff',progressBarColor:'#ffffff',icon:'fa fa-check',timeout:3000})}else{alert('AV号有误')}return}avNo.value=bv2avNo;window.open('https://www.biliplus.com/video/'+avNo.value)}</script>
<p>如果有缓存，就找到，没缓存，那就没办法了</p>
<p>封面有些情况可能是虽然有缓存，但B站把封面原图文件删了，那也找不回来</p>
<p><img src="/uploads/2019/11/3.png" alt=""></p>
<p>另外浏览器插件 Btools 1.1.2 发布了，新版本中的其中一个功能就是用这个原理自动搜索失效视频。</p>
<p>火狐浏览器、谷歌浏览器均已上架。</p>
<p>下面是截图。</p>
<p><img src="/uploads/2019/10/announcement-2019-10-04-1.gif" alt=""></p>
<p>插件里功能更丰富，获取过一次缓存的视频信息会被保存下来，下次就不会查询视频而是把保存下来的视频信息直接加载到页面上，感兴趣的可以看一下这篇文章</p>
<p><a href="https://btools.cc/announcement-2019-11-04/">《关于查看失效视频半全恢复说明》</a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】《大会员在2019》]]></title>
            <link>https://imba97.com/posts/462</link>
            <guid isPermaLink="false">https://imba97.com/posts/462</guid>
            <pubDate>Sun, 03 Nov 2019 23:12:23 GMT</pubDate>
            <content:encoded><![CDATA[<p>(久期) 十年会员刚开通，两手颤抖脑子空。我——IMBA Vlog 久期 程序兵。我听B站里给我发玩意，天到这般时分不见寄来你说这是怎么了尼。<br>
(久期) IMBA架坐公寓卧室中，叫一声бирибири客服——上前辨分明，我听说b站里给我把玩意䏻，天到了这般时分不见派送嗯嗯 嗯嗯嗯 嗯嗯~</p>
<!--more-->
<p>(客服)叫一声会员同志，你不要着急，为此事我问过B站老管理，他也说十年契约才刚刚签立，䏻到了玩意才刚找快递，顺丰厂的速度是一向很有力<br>
你就放心吧，我的十年大会员IMBA久期~!</p>
<p>(久期) 吧哒cyan 且诶 cyan得郎唐 隆冬 里个隆滴cyan</p>
<p>(快递员)杀出了总部呜呜~往IMbra家跑嗷嗷嗷，况且况且况且况，携着包裹带礼包，忙向久期——去报告嗷嗷嗷嗷嗷嗷嗷~！<br>
(快递员)IMBA同志，呢把玩意给您䏻过来俩！</p>
<p>(久期)忒好俩(急忙回卧室架相机)<br>
(久期)闻听得顺丰把玩意送到，不由我IMBAvlog久期喜上眉梢！时间紧，任务重，困难不少。你真是哈拉少哈拉少敖琴哈拉少！</p>
<p>(快递员)这件会员礼包是bilibili的同志们孝敬您老人家的！</p>
<p>(久期)接过了大礼包，满心欢喜，赶紧回公寓卧室打开相机，讲这个大礼包留给自己，本代表与B站的契约关系，这大礼包~</p>
<p>禁铺又禁盖<br>
禁洗又禁晒<br>
禁拉又禁拽<br>
禁蹬又禁踹</p>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/av74473146">https://www.bilibili.com/video/av74473146</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】十年大会员礼物 开箱！]]></title>
            <link>https://imba97.com/posts/458</link>
            <guid isPermaLink="false">https://imba97.com/posts/458</guid>
            <pubDate>Sat, 02 Nov 2019 01:28:28 GMT</pubDate>
            <content:encoded><![CDATA[<p>这次没稿子 GG</p>
<hr>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/av74209617">https://www.bilibili.com/video/av74209617</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】关于做视频的探讨]]></title>
            <link>https://imba97.com/posts/455</link>
            <guid isPermaLink="false">https://imba97.com/posts/455</guid>
            <pubDate>Wed, 23 Oct 2019 23:52:49 GMT</pubDate>
            <content:encoded><![CDATA[<p>Hello各位好 我是imba久期，这是我的第一期vlog<br>
第零期没成为最后一期真是太好了，但这期很可能是最后一期</p>
<p>这期的主题是“关于做视频”，聊一聊我做视频的过程、对做视频的一些看法和一直坚持着的原则</p>
<!--more-->
<h1>过程</h1>
<p>我是从13年开始做视频，是魔兽争霸地图编辑器的教程，那时候大部分教程都是在论坛、手册这种文字性的居多，而且针对新手的一整个系列的教程比较少，所以我想着把我会的东西拿出来分享一下，就有了《魔兽争霸地图编辑器教程》系列。</p>
<p>刚开始做视频啥也不懂，看别人用的啥工具，我也整了一个，结果弄了半天录出来的视频还有水印。而且也不会后期剪辑，都是直出视频，所以基本每个视频有一半的时间是找错误、解决错误，一个教程类视频被我做成了实况视频，某种意义上来说还是有点强。众所周知我是个教程实况UP主。</p>
<p>之后投过一些游戏视频，但精彩镜头这种很少，也是一些对游戏的记录。后来学了编程，视频内容又变成了程序演示和记录开发过程，总之全是长篇记录比较多。基本已经把B站当网盘这么传了，最长到了35个小时[图]，一个网页输入法的程序。这时候有些视频就有剪辑了，但也只是粗剪一下。一直到现在做视频，粗剪一下，去掉中间停顿的时间。</p>
<h1>看法</h1>
<p>然后聊一下对做视频的看法，做视频或者说通过视频分享技术、分享生活是我的一个兴趣爱好。并且至少在5到10年内不会作为主业，因为目前并不打算靠做视频赚钱。在不影响主业的前提下，做做视频分享一下。</p>
<p>目前来说我对主业和做视频之间的平衡还是不错的，我是个程序猿，做视频会促使我去学一些东西，比如那个35小时的网页输入法，实际加上没有录视频的总用时肯定超过35小时的，在这个时间里我大部分时间都在百度，学习一些方法怎么用到我的程序里，这就使得做视频对我的主业有个促进的作用。相反程序猿这个主业也对做视频有促进作用，比如我之前的视频是编辑器教程、游戏实况，做程序的视频是学习程序之后才有的，让我拓展了做视频的领域。</p>
<h1>原则</h1>
<p>接下来聊一下我做视频的原则，其实这里面也有很多是做人的原则，做事做人分不开嘛。我一直坚持的原则首先是“自己讨厌的事不会对别人做”，体现在视频里就是，你会发现我几乎在视频放其他东西，比如二维码、求赞助，并且从不要三连和关注。当然这里并不是说放二维码要关注要三连就是不好的，并且官方做视频的教程里也说合理暗示三连关注是对自己有帮助的行为。只是我个人不喜欢，所以我不会做，你甚至找不到我微博。还一个原因是我更喜欢简洁的视频，特别是教程，噼里啪啦说完告辞完事儿。</p>
<p>还一个不喜欢的事就是寻求大佬帮忙宣传视频，当然能认识大佬能有大佬的帮助我认为是特别好的一件事，但还是我个人不喜欢这么做，只是不喜欢帮忙宣传这一块，认识大佬跟大佬请教技术我还是非常愿意的。然后不喜欢被宣传的原因是我认为别人的粉丝永远是别人的。比如大佬今天说帮你转发一下，可能大佬的一堆粉丝过来关注你，它确实是一个涨粉的途径。但保不齐哪天你跟大佬闹掰了，大佬发了条动态说你，这个UP怎么这样怎么怎么样，那以前支持大佬的粉丝肯定还是支持大佬，取关一波必不可免。不过这个例子也不恰当，因为会做这种事的大佬很少，但终归闹掰了没好下场的，就算不是闹掰你也欠了个人情，我个人是欠人情没还就感觉必须马上得还点啥，不然感觉怪怪的。总之我认为只有被我这个人或者我的作品吸引来，并且在我没有暗示他关注我的前提下，关注我的人才能叫“我的粉丝”。</p>
<p>还有其他不喜欢的事比如标题党、封面党我就不一一说了，反正我没做就说明八成我是讨厌这件事的。</p>
<p>接下来我们说第二个原则是“正能量”，不管是B站、微博、空间、朋友圈，虽然我不能保证没有，但极少会看到我发抱怨、怼人，这种负能量的动态或视频，一直对外输出正能量也是我坚持的原则。往后也请监督我，也希望各位一起努力输出正能量。</p>
<p>然后第三个原则，就是“自由”，我的视频比较放纵自我，不希望被限制，想做啥做啥，所以什么类型都有。这个不好保证以后怎么样，但会努力让视频质量越来越高。</p>
<p>姑且做视频的几个主要原则就说到这。</p>
<h1>结尾</h1>
<p>好的本期视频就是这样，喜欢本期视频的小伙伴不妨[眼神疯狂暗示]，喜欢我的请[眼神疯狂暗示]，告辞完事儿。</p>
<hr>
<h1>学了点啥</h1>
<p>视频抖动效果：下载蓝宝石插件-&gt;效果里搜索“shake”</p>
<p>参数说明：</p>
<ul>
<li>Amplitude 振幅</li>
<li>Frequency 频率</li>
<li>Phase 相位</li>
<li>Z Dist Z轴缩放</li>
<li>Motion Blur 运动模糊</li>
<li>Mo Blur Length 运动模糊长度</li>
</ul>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/av73205553">https://www.bilibili.com/video/av73205553</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[【VLOG】零]]></title>
            <link>https://imba97.com/posts/454</link>
            <guid isPermaLink="false">https://imba97.com/posts/454</guid>
            <pubDate>Sun, 20 Oct 2019 23:15:11 GMT</pubDate>
            <content:encoded><![CDATA[<p>Hello 各位好，我是imba久期<br>
今天开始打算拍vlog</p>
<p>之前虽然拍过但那感觉算不上vlog<br>
然后从这期开始，这是第0期，主题是我之后打算怎么拍</p>
<!--more-->
<h1>之后的vlog主题</h1>
<p>最开始可能像这样杂谈类的比较多，因为还没什么设备，之后有了稳定器之类的设备后可能会去拍点户外的视频，反正vlog嘛，拍点日常吃喝玩乐，应该就是这样。</p>
<p>但各位也别太把我这个vlog计划放在心上，我就这么想想，说不定这期是最后一期。</p>
<h1>为什么要拍vlog</h1>
<p>挺早之前就想过，也尝试过，但没做下来，大概因为懒，遇事不决量子力学，用在我这就是遇事不决那就是懒。现在又想试试，记录一下生活，拓展一下社交圈。</p>
<h1>对一些事的看法</h1>
<p>以下所有说的内容是我有一定影响力后才会发生的，如果这是最后一个视频，就当单纯听个我的看法。</p>
<ol>
<li>盗视频、盗语音</li>
</ol>
<p>之前看过一些大佬的视频，特别是生活类，比如美食视频，就会被一些营销号盗走，只用视频然后后期配音介绍，随便起个标题“外国小伙在某国吃啥啥啥”，这种就挺恶心的。其实我目前的视频也出现过被搬运，为啥叫搬运呢，因为是教程类的视频，本来就是希望更多人看的，而且我也不知名，做的也不精细，搬运我视频真的是疯了。</p>
<p>之后，可能几年后万一我的视频也有一点播放量，也出现被盗的情况，所以我从最开始就会放个logo。</p>
<ol start="2">
<li>杠精行为</li>
</ol>
<p>以后可能是常有的，毕竟林子大了什么精都有。杠精多了说明我也有一定人气了，解决杠精行为我能想到的就是公开处刑，既然杠精提供了素材，咱也不好说啥，整合一下水期视频岂不美哉，我还挺期待。</p>
<ol start="3">
<li>对接广告的看法</li>
</ol>
<p>这个是大后期的事了，我姑且说一下我对接广告的看法，我个人不会选择强行植入，我会选择防不胜防的类型，有趣并达到广而告之的效果，并且视频标题以“【广告】”开头。</p>
<h1>最后</h1>
<p>好，第0期就到这儿。我是imba久期，溜了溜了。</p>
<hr>
<h1>视频后期的统一标准</h1>
<p>BGM：<a href="https://www.youtube.com/watch?v=6hCccojvkmw">https://www.youtube.com/watch?v=6hCccojvkmw</a></p>
<p>视频字幕参数：</p>
<ul>
<li>居中</li>
<li>X位置960</li>
<li>Y位置1000</li>
<li>字体大小80</li>
<li>白色</li>
<li>阴影 黑色 不透明度100% 角度135° 距离10 扩展30</li>
</ul>
<h1>对应视频</h1>
<p><a href="https://www.bilibili.com/video/av72044834">https://www.bilibili.com/video/av72044834</a></p>
<h1>弹幕评论删除记录</h1>
<p>暂无</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[wakeonlan! 树莓派远程开机]]></title>
            <link>https://imba97.com/posts/445</link>
            <guid isPermaLink="false">https://imba97.com/posts/445</guid>
            <pubDate>Fri, 20 Sep 2019 00:30:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>你需要对树莓派做一个内网映射，可以在外网访问树莓派以及整个局域网的设备。我用的是<code>frp</code>，但本教程并不包含内网映射</p>
<p>frp配置请查看<a href="/posts/506">《frp内网穿透 树莓派+阿里云服务器》</a></p>
<p>实现远程开机是通过一个软件，你可以用以下命令下载它</p>
<pre><code class="language-bash">apt-get install wakeonlan
</code></pre>
<!--more-->
<h2>使用</h2>
<pre><code class="language-bash"># wakeonlan -i [广播地址] [MAC地址] 下面是例子
wakeonlan -i 192.168.1.255 FF:FF:FF:FF:FF:FF
</code></pre>
<h2>广播地址</h2>
<p>一般局域网的广播地址是<code>192.168.X.255</code>，<strong>X</strong>位置可能会不同，根据你的内网地址的广播地址填写</p>
<h2>MAC地址</h2>
<p>在windows系统中，打开CMD，输入<code>ipconfig /all</code>，找到你连接内网网卡，复制MAC地址<br>
<strong>注意这个地址中间是以“-”分隔的，你需要手动改成“:”</strong></p>
<h2>目标电脑的设置</h2>
<p>你需要开启主板的一个配置<strong>允许PCI/PCIE设备唤醒电脑</strong>，不同主板设置界面不同，你可以根据你的主板品牌、型号去百度<strong>XX主板开启PCI设备唤醒</strong></p>
<p>主板设置好后再设置网卡一些属性，首先打开设备管理器或用其他方法找到网卡属性，高级里面把唤醒魔包的值设为开启</p>
<p><img src="https://imba97.com/uploads/2019/09/wakeonlan-1.png" alt=""></p>
<p>然后再点<strong>电源管理</strong>，都给他勾上</p>
<p><img src="/uploads/2019/09/wakeonlan-2.png" alt=""></p>
<p>2020-12-02 更新，之前重装过一次电脑，发现唤醒失效了</p>
<p>原因是我开启了“<strong>快速启动</strong>”，你需要关掉</p>
<p><code>Win+R</code>打开运行 -&gt; 输入<code>control</code>打开控制面板 -&gt; 硬件和声音 -&gt; 电源按钮功能</p>
<p>把快速启动的勾去掉</p>
<p><img src="/uploads/2019/09/wakeonlan-3.png" alt=""></p>
<h2>远程开机程序</h2>
<p>这里我用了一个PHP的网页，执行了<code>wakeonlan</code>这条命令<br>
需要用<code>exec</code>函数去执行，如果没成功，很可能是因为这个函数比较危险，在<code>php.ini</code>里是被禁用了<br>
解除禁用你得找到<code>php.ini</code>，搜索<code>disable_functions</code>，把<code>exec</code>或者你需要用的函数删掉，这样OK了</p>
<p>下面是部分PHP的源码，我也加了个密码保证只有我自己能开机，<code>exec</code>具体怎么用还请看<a href="https://www.php.net/manual/en/function.exec.php"><strong>官方文档</strong></a></p>
<pre><code class="language-php">if(!empty($_POST['pwd']) &amp;&amp; $_POST['pwd'] == '123') {
  $shell = 'wakeonlan -i 192.168.1.255 FF:FF:FF:FF:FF:FF';
  exec($shell, $result, $status);
  // 也可以直接
  // exec('wakeonlan -i 192.168.1.255 FF:FF:FF:FF:FF:FF');
}
</code></pre>
<p>全部代码</p>
<pre><code class="language-php">&lt;?php
  header(&quot;Content-type:text/html;charset=utf-8&quot;);

  if(isset($_POST['pwd'])) {

    if(!empty($_POST['pwd']) &amp;&amp; $_POST['pwd'] == '123') {
      $shell = 'wakeonlan -i 192.168.1.255 FF:FF:FF:FF:FF:FF';
      exec($shell, $result, $status);
      echo '&lt;hr&gt;';
      var_dump($a);echo '&lt;br&gt;';
      var_dump($result);echo '&lt;br&gt;';
      var_dump($status);echo '&lt;br&gt;';
      $shell = &quot;&lt;font color='red'&gt;etherwake&lt;/font&gt;&quot;;
        echo &quot;&lt;pre&gt;&quot;;
        if( $status ){
            echo &quot;shell命令{$shell}执行失败&lt;hr&gt;&quot;;
            print_r( $result );
        } else {
            echo &quot;shell命令{$shell}成功执行&lt;hr&gt;&quot;;
            print_r( $result );
        }
        echo &quot;&lt;/pre&gt;&quot;;
    }
    return 0;
  }
?&gt;

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
  &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
  &lt;title&gt;biu&lt;/title&gt;
  &lt;style&gt;
    .etherwake {
      margin-top: 50px;
      text-align: center;
    }
    .etherwake #pwd {
      padding-left: 5px;
      width: 200px;
      height: 30px;
      font-size: 26px;
      border: none;
      border-bottom: 1px #000 solid;
      outline:none;
    }

    #etherwake_btn {
      margin-left: 10px;
      width: 100px;
      height: 30px;
      font-size: 20px;
      background: none;
      border: 1px #000 solid;
      border-radius: 5px;
      outline:none;
    }
    #etherwake_btn:active {
      background-color: #CCC;
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;etherwake&quot;&gt;&lt;input id=&quot;pwd&quot; type=&quot;password&quot; &gt;&lt;input type=&quot;button&quot; id=&quot;etherwake_btn&quot; value=&quot;开机&quot;&gt;&lt;/div&gt;
&lt;div id=&quot;msg&quot;&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;script src=&quot;https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
  $(function() {
    $('#etherwake_btn').click(function() {
      $.ajax({
        url: 'index.php',
        type: 'POST',
        data: {
          'pwd': $('#pwd').val()
        },
        success: function(response) {
          $('#msg').html(response);
        }
      });
    });
  });
&lt;/script&gt;
&lt;/html&gt;
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[dateMaker.js]]></title>
            <link>https://imba97.com/posts/443</link>
            <guid isPermaLink="false">https://imba97.com/posts/443</guid>
            <pubDate>Mon, 16 Sep 2019 00:02:23 GMT</pubDate>
            <content:encoded><![CDATA[<p>这是<a href="/about">about</a>页面中用到的一个js程序，可以根据一个起始日期计算距离当前的差值，精确到毫秒。</p>
<p>程序本体：由一个<code>dateMaker</code>获取数据函数和一个<code>time_valide</code>日期格式化函数组成</p>
<!--more-->
<pre><code class="language-javascript">function dateMaker(id) {
  const dom = document.querySelector(`#${id}`)
  // 计时器间隔
  const interval_time = dom.getAttribute('interval-time') !== null ? Number.parseInt(dom.getAttribute('interval-time')) : 1000
  // 额外添加的时间，逗号隔开
  let plus_time = 0
  const plus_time_attr = dom.getAttribute('plus-time')
  const plus_time_arr = /^(?:\d+,)+\d+$/.test(plus_time_attr) ? plus_time_attr.split(',') : false
  if (plus_time_arr) {
    plus_time_arr.forEach((v, k) =&gt; {
      plus_time += Number.parseInt(v)
    })
  }
  else if (/^\d+$/.test(plus_time_attr)) {
    plus_time = Number.parseInt(plus_time_attr)
  }

  // 开始时间 字符串 比如 2019/09/11 16:42:00
  const sTime = new Date(dom.getAttribute('start-time')).getTime()

  const timer = setInterval(() =&gt; {
    const now = new Date()
    t = time_valide(sTime - plus_time, now.getTime())
    const years = t.y &gt; 0 ? `${t.y}年` : ''
    const months = t.m &gt; 0 ? `${t.m}个月` : t.y &gt; 0 ? '1个月' : ''
    const days = t.d &gt; 0 ? `${t.d}天` : t.y &gt; 0 ? '1天' : ''
    let hours = t.h
    if (hours &lt; 10)
      hours = `0${hours}`
    hours += '个小时'
    let mins = t.i
    if (mins &lt; 10)
      mins = `0${mins}`
    mins += '分钟'
    let secs = t.s
    if (secs &lt; 10)
      secs = `0${secs}`
    secs += '秒'
    let msecs = now.getMilliseconds()
    if (msecs &lt; 10) {
      msecs = `00${msecs}毫秒`
    }
    else if (msecs &gt;= 10 &amp;&amp; msecs &lt; 100) {
      msecs = `0${msecs}毫秒`
    }
    else {
      msecs += '毫秒'
    }
    dom.innerHTML = years + months + days + hours + mins + secs + msecs
  }, interval_time)
}

function time_valide(starttime, endtime) {
  const timediff = endtime / 1000 - starttime / 1000
  // 年
  const years = Number.parseInt(timediff / 31536000)
  let remain = timediff % 31536000
  // 月
  const months = Number.parseInt(remain / 2592000)
  remain %= 2592000
  // 日
  const days = Number.parseInt(remain / 86400)
  remain = remain % 86400
  // 时
  const hours = Number.parseInt(remain / 3600)
  remain = remain % 3600
  // 分
  const mins = Number.parseInt(remain / 60)
  remain = remain % 60
  // 秒
  const secs = Number.parseInt(remain % 60)

  return {
    y: years,
    m: months,
    d: days,
    h: hours,
    i: mins,
    s: secs
  }
}
</code></pre>
<p>使用时引入dateMaker.js，使用<code>dateMaker</code>并传入一个ID</p>
<pre><code class="language-html">&lt;span id=&quot;date&quot; plus-time=&quot;23241600000&quot; start-time=&quot;2018/10/16 07:00:00&quot; interval-time=&quot;50&quot;&gt;&lt;/span&gt;
</code></pre>
<p>支持几个属性</p>
<ul>
<li><strong>plus-time</strong>：额外增加的时间，单位毫秒，比如我统计工作时间，我是从2018年10月到现在在工作，并且2015年也工作了9个多月，所以我这里写“23241600000”，也就是额外增加的时间，可以添加多个，用英文逗号隔开</li>
<li><strong>start-time</strong>：这个不用多说，就是开始时间，年月日格式必须用斜杠“yyyy/mm/dd”，不能用减号“yyyy-mm-dd”，否则苹果手机的浏览器可能不兼容，包括苹果手机的QQ、微信内置浏览器</li>
<li><strong>interval-time</strong>：这是计时器的间隔时间，单位毫秒，每过多长时间运行一次，如果时间精确到秒的话写1000就OK，精确到毫秒的话得有那种飞快跑动的感觉，所以这里是50</li>
</ul>
<p>最后会在这个<code>span</code>标签内生成“<strong>X年X个月X天XX个小时XX分钟XX秒XXX毫秒</strong>”</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[被恶心了两次的PHP安装错误]]></title>
            <link>https://imba97.com/posts/441</link>
            <guid isPermaLink="false">https://imba97.com/posts/441</guid>
            <pubDate>Wed, 11 Sep 2019 15:17:05 GMT</pubDate>
            <content:encoded><![CDATA[<p>web开发中我是用的XAMPP集成环境，安装的PHP版本是<code>5.6</code>，我又单独下了个<code>7.3</code>版本，这样只需要单独写个配置文件，之后切换加载不同的配置文件就可以切换PHP版本了。</p>
<p>这次的问题是，昨天PHP-7.3的目录坏了，原因至今不详</p>
<p><img src="https://imba97.com/uploads/2019/09/php-7.3.png" alt=""></p>
<!--more-->
<p>打不开也删不掉，我决定这个先暂时放一放，直接去下载一个新的PHP</p>
<p>于是在官网下完后，解压、改名，接下来修改配置文件，新的PHP是没有<code>php.ini</code>这个文件的，但在文件夹下有两个文件<code>php.ini-development</code>和<code>php.ini-production</code>是<code>php.ini</code>的模板，分别对应着开发版和线上版，根据需要复制一份然后把名字改成<code>php.ini</code>就可以</p>
<p>这还没结束，你还需要开启扩展，比如<code>;extension=openssl</code>，把你需要的扩展前面的分号去掉</p>
<h2>这次的问题</h2>
<p>运行Apache、Mysql一切正常，打开最近用Laravel开发的网站发现，它报了个错！</p>
<pre><code class="language-bash">Call to undefined function Illuminate\Encryption\openssl_cipher_iv_length()
</code></pre>
<p>我一想，版本跟之前一样，配置跟之前一样，咋会报错呢，而且跟openssl有关。类似的问题之前有过，都是openssl，只不过报错不同。在网上搜这个错误得到的结果基本是你没开启openssl扩展，但我开了。</p>
<h2>解决</h2>
<p>也纯属巧合，本来打开<code>php.ini</code>搜了一下extension关键字想去再确认一下还有没有跟openssl有关的其他扩展，结果突然看到<code>;extension_dir = &quot;ext&quot;</code>是被注释的！</p>
<p>突然想起来，上次报错的问题也是这么解决的，时间长了就忘了，这次写在这备份！</p>
<p>那个意思就是扩展的目录，指定存放扩展的文件夹是哪个。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[魔兽争霸3连接PvPGN服务器]]></title>
            <link>https://imba97.com/posts/437</link>
            <guid isPermaLink="false">https://imba97.com/posts/437</guid>
            <pubDate>Sat, 07 Sep 2019 16:39:46 GMT</pubDate>
            <content:encoded><![CDATA[<h2>魔兽争霸III</h2>
<p>首先你的魔兽必须得是正版，输入CDKey激活的那种（大概吧），你可以在这里下载</p>
<p><a href="https://pan.baidu.com/s/15JaEG7tMocHnRE6YbftJbQ">war3.zip</a> <code>1.09G</code> 提取码 <code>goqr</code></p>
<!--more-->
<h2><a href="http://Battle.net">Battle.net</a> Gateways Editor</h2>
<p>这是战网网关编辑器，修改成PvPGN服务器的地址</p>
<p>下载：<a href="http://static.imba97.com/uploads/gwe.zip">gwe.zip</a> <code>208.0K</code></p>
<p><img src="https://imba97.com/uploads/2019/09/v1.png" alt=""></p>
<p>点击 Add Gateway 添加<br>
Name 随便<br>
Zone 没提供的话就0吧<br>
IP   PvPGN 服务器IP或域名</p>
<h2>WarCraft 3 PvPGN Loader</h2>
<p>这是运行所需要的文件，有它你才能连接PvPGN服务器</p>
<p>下载：<a href="http://static.imba97.com/uploads/w3l.zip">w3l.zip</a> <code>20.0K</code></p>
<p>把解压缩得到的 w3l.exe、w3h.dll、wl27.dll 放到魔兽争霸目录下，运行 <strong>w3l.exe</strong> 打开游戏</p>
<p>点击<code>Battle.net</code>，然后注册或者输入用户名和密码登陆，即可看到以下界面</p>
<p><img src="/uploads/2019/09/pvpgn-3.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[树莓派搭建PvPGN服务器]]></title>
            <link>https://imba97.com/posts/436</link>
            <guid isPermaLink="false">https://imba97.com/posts/436</guid>
            <pubDate>Sat, 07 Sep 2019 16:06:38 GMT</pubDate>
            <content:encoded><![CDATA[<h2>简介</h2>
<p>PvPGN 和 PvPGN PRO<br>
PvPGN 全称：Player versus Player Gaming Network，是一款免费的开源跨平台服务器软件，支持Battle.net和Westwood Online游戏客户端<br>
PvPGN在2011年停止维护，PvPGN PRO是PvPGN的一个分支版本，持续维护更新。</p>
<p><a href="https://github.com/pvpgn/pvpgn-server">详情查看Github项目</a></p>
<!--more-->
<h2>搭建</h2>
<p>参考<a href="https://pvpgn.pro/pvpgn_installation.html">官方教程</a></p>
<p>首先下载编译 PvPGN 源代码所需的软件、工具</p>
<p>这里用的存储方式是mysql，所以下载了<code>libmysql++-dev</code>，你可以不下载mysql数据库，根据自己的需求来，甚至可以用文件存储的方式保存玩家信息等</p>
<p>其他存储：pgsql、sqlite3、odbc</p>
<pre><code class="language-bash">sudo apt-get -y install build-essential clang libc++-dev git cmake zlib1g-dev liblua5.1-0-dev libmysql++-dev
</code></pre>
<p>这会下载一堆编译所需工具</p>
<p>然后下载 PvPGN 的源代码，找个目录执行命令下载并解压</p>
<pre><code class="language-bash">wget https://github.com/pvpgn/pvpgn-server/archive/master.tar.gz
tar xf master.tar.gz
</code></pre>
<p>接下来是进入解压出来的文件夹里，建立一个<code>build</code>目录，然后进入这个目录</p>
<pre><code class="language-bash">cd pvpgn-server-master
mkdir build
cd build
</code></pre>
<p>配置和编译</p>
<pre><code class="language-bash">cmake -D WITH_MYSQL=true -D WITH_LUA=true ../
make &amp;&amp; make install
</code></pre>
<p>运行</p>
<pre><code class="language-bash">/usr/local/sbin/bnetd
</code></pre>
<p>配置文件路径：<code>/usr/local/etc/pvpgn/</code></p>
<p>如果你是用文件储存数据，储存的文件在：<code>/usr/local/var/pvpgn/</code></p>
<h1>测试</h1>
<p>最后你可以在<code>Battle.net Gateways Editor</code>中测试连接是否正常</p>
<p><img src="https://imba97.com/uploads/2019/09/pvpgn-1.png" alt=""></p>
<p>如果你做了内网映射，你可配置一下端口，即可从外网访问到你树莓派PvPGN服务器</p>
<p><img src="/uploads/2019/09/pvpgn-2.png" alt=""></p>
<p>最后成功连上服务器，连接需要做一些操作，详情请看：<a href="/posts/437">《魔兽争霸3连接PvPGN服务器》</a></p>
<p><img src="/uploads/2019/09/pvpgn-3.png" alt=""></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Chrome插件跨域请求]]></title>
            <link>https://imba97.com/posts/434</link>
            <guid isPermaLink="false">https://imba97.com/posts/434</guid>
            <pubDate>Fri, 30 Aug 2019 12:58:05 GMT</pubDate>
            <content:encoded><![CDATA[<p>Chrome插件中，可以跨域的地方只有<code>background js</code>和<code>popup js</code>这两个地方，<code>popup js</code>是右上角那个弹出页，只有弹出的时候才有效，所以定义跨域监听大概不现实</p>
<!--more-->
<p>这样只能在<code>background js</code>，我们需要在<code>background js</code>中定义一个监听函数，来监听其他页面传过来的信息。根据传来的值，判断是否是API请求</p>
<p>下面是一个有<code>get</code>和<code>post</code>的简易解决方案</p>
<pre><code class="language-javascript">chrome.runtime.onMessage.addListener(
  (request, sender, sendResponse) =&gt; {
    switch (request.type) {
      case 'get':
        fetch(request.url)
          .then((response) =&gt; { return response.json() })
          .then((json) =&gt; { return sendResponse(json) })
          // eslint-disable-next-line node/handle-callback-err
          .catch((error) =&gt; { return sendResponse(null) })
        break
      case 'post':
        fetch(request.url, {
          method: 'POST',
          mode: 'cors',
          credentials: 'include',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: JSON.stringify(request.data)
        })
          .then((response) =&gt; { return response.json() })
          .then((json) =&gt; { return sendResponse(json) })
          // eslint-disable-next-line node/handle-callback-err
          .catch((error) =&gt; { return sendResponse(null) })
        break
        // 你可以定义任意内容，使用sendResponse()来返回它
      case 'test':
        sendResponse({ msg: 'test' })
        break
    }
  }
)
</code></pre>
<p>在其他页面你只需要这样调用</p>
<pre><code class="language-javascript">chrome.runtime.sendMessage(
  {
    // 里面的值应该可以自定义，用于判断哪个请求之类的
    type: 'get',
    url: 'https://api.xxxx.com/api?a=1&amp;b=2' // 需要请求的url
  },
  (json) =&gt; {
    console.log(json)
  }
)
</code></pre>
<p>进一步学习：<a href="https://developer.chrome.com/apps/xhr"><strong>官方文档</strong></a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 – 网络请求]]></title>
            <link>https://imba97.com/posts/421</link>
            <guid isPermaLink="false">https://imba97.com/posts/421</guid>
            <pubDate>Tue, 20 Aug 2019 15:54:58 GMT</pubDate>
            <content:encoded><![CDATA[<p>获取页面数据</p>
<!--more-->
<pre><code class="language-objective-c">- (void)viewDidLoad {
  [super viewDidLoad];
  // URL
  NSURL * url = [NSURL URLWithString:@&quot;https://m.baidu.com&quot;];
  // 请求
  NSURLRequest * request = [NSURLRequest requestWithURL:url];
  // 连接服务器
  [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
    // data是返回数据的16进制，需要转换
    NSString * html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  }];
}
</code></pre>
<h2>头信息</h2>
<pre><code class="language-objective-c">// URL
// 请求
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:url];
// 头信息
[request setValue:@&quot;iPhone&quot; forHTTPHeaderField:@&quot;User-Agent&quot;];
// 发送请求
</code></pre>
<h2>缓存</h2>
<pre><code class="language-objective-c">// URL
// 请求
NSURLRequest * request = [NSURLRequest requestWithURL:url cachePolicy:0 timeoutInterval:15.0]
// 发送请求
</code></pre>
<p><code>cachePolicy</code>定义缓存的类型，是个枚举类型</p>
<ul>
<li>NSURLRequestUseProtocolCachePolicy = 0，默认缓存策略</li>
<li>NSURLRequestReloadIgnoringLocalCacheData = 1，忽略本地缓存，新闻等APP常用</li>
<li>NSURLRequestReturnCacheDataElseLoad = 2，返回缓存数据，如果没有缓存从服务器加载</li>
<li>NSURLRequestReturnCacheDataDontLoad = 3，返回缓存数据，如果没有缓存，就空着</li>
</ul>
<h2>回调代码块</h2>
<pre><code class="language-objective-c">// 发送请求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
  NSLog(@&quot;喵喵喵&quot;);
}];
</code></pre>
<p>参数</p>
<ul>
<li>response(NSHTTPURLResponse) 服务器的响应</li>
<li>URL 服务器返回的URL，绝大部分和请求的URL一样，重定向时不一样</li>
<li>MIMEType 二进制数据文件类型</li>
<li>expectedContentLength 下载文件的长度</li>
<li>textEncodingName 文本编码名称</li>
<li>suggestedFilename 服务器建议的保存文件名称</li>
<li>statusCode 状态码</li>
<li>allHeaderFields 所有相应头的内容</li>
<li>data 返回数据</li>
<li>connectionError 错误</li>
</ul>
<h2>JSON反序列化</h2>
<pre><code class="language-objective-c">[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
  id result = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
}];
</code></pre>
<p>一个JSON对象应该具有以下属性</p>
<ul>
<li>顶级节点是一个NSArray 或 NSDictionary</li>
<li>所有对象必须是NSString、NSNumber、NSArray、NSDictionary、NSNull</li>
<li>所有字典的key必须是NSString</li>
<li>NSNumber 不能是无理数或空</li>
</ul>
<p><code>options</code>参数</p>
<ul>
<li>NSJSONReadingMutableContainers 容器节点是可变的</li>
<li>NSJSONReadingMutableLeaves 子节点是可变的</li>
<li>NSJSONReadingAllowFragments 允许顶级节点不是NSArray或NSDictionary</li>
</ul>
<h2>PList反序列化</h2>
<pre><code class="language-objective-c">[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
  id result = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
}];
</code></pre>
<p><code>options</code>参数</p>
<ul>
<li>NSPropertyListImmutable 不可变</li>
<li>NSPropertyListMutableContainers 容器可变</li>
<li>NSPropertyListMutableContainersAndLeaves 容器和子节点都可变</li>
</ul>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 – NSOperation]]></title>
            <link>https://imba97.com/posts/417</link>
            <guid isPermaLink="false">https://imba97.com/posts/417</guid>
            <pubDate>Mon, 19 Aug 2019 15:51:20 GMT</pubDate>
            <content:encoded><![CDATA[<p>本质上是GCD的并发队列异步执行，是对GCD的面向对象的封装，是苹果大力推荐的并发技术</p>
<!--more-->
<p>与GCD的对比：</p>
<ul>
<li>GCD：</li>
<li>iOS 4.0 推出</li>
<li>针对多核处理器做了优化的并发技术</li>
<li>是C语言</li>
<li>提供一次执行、延迟执行、调度组</li>
<li>将<strong>任务</strong>添加到队列（串行、并发、主队列、全局队列），并且指定执行任务的函数（同步、异步）</li>
<li>NSOperation：</li>
<li>iOS 2.0 推出</li>
<li>底层是GCD</li>
<li>面向对象</li>
<li>提供最大并发线程、队列暂停继续、取消所有操作、指定操作之间的依赖关系</li>
<li>将<strong>操作</strong>添加到队列（并发），然后立即异步执行</li>
</ul>
<p>NSOperation 是一个抽象类，继承NSObject</p>
<ul>
<li>特点：不能直接使用</li>
<li>目的：定义子类共有属性和方法</li>
<li>子类：</li>
<li>NSInvocationOperation</li>
<li>NSBlockOperation</li>
</ul>
<pre><code class="language-objective-c">- (void)demo1 {
  NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@&quot;invocation&quot;];
  [op start];
}
- (void)downloadImage:(id)objc {
  NSLog(@&quot;%@ %@&quot;, [NSThread currentThread, objc]);
}

- (void)viewDidLoad {
  [super viewDidLoad];
  [self demo1];
}
</code></pre>
<p>使用队列</p>
<pre><code class="language-objective-c">- (void)demo1 {
  NSInvocationOperation * op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImage:) object:@&quot;invocation&quot;];
  NSOperationQueue * q =[[NSOperationQueue alloc] init];
  // 添加到队列
  [q addOperation: op]
}
</code></pre>
<p>NSBlockOperation</p>
<pre><code class="language-objective-c">- (void)demo3 {
  NSOperationQueue * q =[[NSOperationQueue alloc] init];
  NSBlockOperation * op = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
  }]
}
</code></pre>
<p>更简单的写法</p>
<pre><code class="language-objective-c">- (void)demo4 {
  NSOperationQueue * q =[[NSOperationQueue alloc] init];
  [q addOperationWithBlock: ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
  }]
}
</code></pre>
<p>使用全局队列</p>
<pre><code class="language-objective-c">@interface ViewController()
// 先定义一个全局变量，然后用它指向一个队列
@property(nonacomic, strong) NSOperationQueue * opQueue;
@end

@implementation ViewController

// 懒加载
-(NSOperationQueue *)opQueue {
  if(!_opQueue) {
    _opQueue = [[NSOperationQueue alloc]init];
  }
  return _opQueue;
}

// 使用
- (void)demo5 {
  [self.opQueue addOperationWithBlock: ^{
    NSLog(@&quot;%@&quot;, [NSThread currentNSThread]);
  }];

  // block operation
  NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
  }];
  [self.opQueue addOperation: op1];

  // invocation operation
  NSInvocationOperation * op2 = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(downloadImage:) object:@&quot;invocation&quot;];
  [self.opQueue addOperation: op2];
}

@end
</code></pre>
<p>线程间的通讯，在子线程更新UI</p>
<pre><code class="language-objective-c">- (void)demo6 {
  [self.opQueue addOperationWithBlock: ^{
    NSLog(@&quot;耗时操作 %@&quot;, [NSThread currentThread]);
    // 主线程更新UI，首先拿到主线程
    [[NSOperationQueue mainQueue] AddOperationWithBlock: ^{
      NSLog(@&quot;UI操作 %@&quot;, [NSThread currentThread]);
    }];
  }];
}
</code></pre>
<p>最大并发数，iOS 8.0 开始无论使用GCD还是NSOperation都会开启很多子线程，iOS 7.0 以前，GCD通常只会开启5~6条线程</p>
<p>一般情况下，WIFI网络设置5、6，流量网络设置2、3</p>
<pre><code class="language-objective-c">- (void)demo7 {
  // 控制最大线程数量
  self.opQueue.maxConcurrentOperationCount = 2;
  for(int i = 0; i &lt; 20; i++) {
    [self.opQueue addOperationWithBlock: ^{
      NSLog(@&quot;%@&quot;, [NSThread currentThread]);
    }];
  }
}
</code></pre>
<p>暂停、继续</p>
<pre><code class="language-objective-c">-(IBAction) pause {
  // 判断队列是否挂起（暂停）
  if(self.opQueue.isSuspended) {
    NSLog(@&quot;继续&quot;);
    self.opQueue.suspended = NO;
  } else {
    NSLog(@&quot;暂停&quot;);
    self.opQueue.suspended = YES;
  }
}
</code></pre>
<p>取消所有操作</p>
<pre><code class="language-objective-c">- (IBAction) cancelAll {
  NSLog(@&quot;取消所有操作&quot;);
  [self.opQueue cancelAllOperations];
}
</code></pre>
<p>依赖关系</p>
<pre><code class="language-objective-c">- (void) dependecy {
  NSBlockOperation * op1 = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@&quot;动作 1 %@&quot;, [NSThread currentThread]);
  }];
  NSBlockOperation * op2 = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@&quot;动作 2 %@&quot;, [NSThread currentThread]);
  }];
  NSBlockOperation * op3 = [NSBlockOperation blockOperationWithBlock: ^{
    NSLog(@&quot;动作 3 %@&quot;, [NSThread currentThread]);
  }];

  // NSOperation 的依赖关系，op2依赖于op1，op3依赖于op2
  [op2 addDependency: op1];
  [op3 addDependency: op2];

  // 添加到队列，waitUntilFinished是“是否等待”，会卡住当前线程
  [self.opQueue addOperations:@[op1, op2, op3] waitUntilFinished:YES];

  // 更新UI让主线程执行操作
  [[NSOperationQueue mainQueue] addOperation:op3];

  NSLog(@&quot;运行至此&quot;);
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 - GCD]]></title>
            <link>https://imba97.com/posts/397</link>
            <guid isPermaLink="false">https://imba97.com/posts/397</guid>
            <pubDate>Wed, 14 Aug 2019 19:59:16 GMT</pubDate>
            <content:encoded><![CDATA[<p>全称是Grand Central Dispatch，🐮🍺的中枢调度器，纯C语言，提供非常多强大的函数。</p>
<!--more-->
<pre><code class="language-objective-c">- (void)viewDidLoad {
  [super viewDidLoad];
  [self gcdDemo1];
}

// 同步执行方法，这里不执行完就不会执行下一个任务
- (void)gcdDemo1 {
  // 创建队列
  dispatch_queue_t q = dispatch_get_global_queue(0, 0);

  // 定义任务 block
  void(^task)() = ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
  };
  // 添加任务到队列并执行
  dispatch_sync(q, task);
}
</code></pre>
<p>也可以直接写成</p>
<pre><code class="language-objective-c">dispatch_sync(q, ^{
  NSLog(@&quot;biu&quot;);
});
</code></pre>
<p>异步执行，不同等待，执行下一个任务具备开启线程的能力，异步通常是多线程的代名词</p>
<pre><code class="language-objective-c">- (void)gcdDemo2 {
  dispatch_queue_t q = dispatch_get_global_queue(0, 0);
  void(^task)() = ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
  };
  dispatch_async(q, task);
}
</code></pre>
<pre><code class="language-objective-c">- (void)gcdDemo3 {
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
      // 更新UI
      dispatch_async(dispatch_get_main_queue(0, 0), ^{
        NSLog(@&quot;更新UI%@&quot;, [NSThread currentThread]);
      });
  });
}
</code></pre>
<h2>串行并行、同步异步</h2>
<p><img src="https://imba97.com/uploads/2019/08/656540a5c2173889a1dc174369f389bc.png" alt=""></p>
<p>创建串行队列</p>
<pre><code class="language-objective-c">dispatch_queue_t q = dispatch_queue_create(&quot;name&quot;, DISPATCH_QUEUE_SERIAL);
// 因为 DISPATCH_QUEUE_SERIAL == NULL 所以可以直接设为NULL
dispatch_queue_t q = dispatch_queue_create(&quot;name&quot;, NULL);
</code></pre>
<p>创建并发队列</p>
<pre><code class="language-objective-c">dispatch_queue_t q = dispatch_queue_create(&quot;name&quot;, DISPATCH_QUEUE_CONCURRENT);
</code></pre>
<p>同步任务一旦开始执行，即使后面是异步任务，也不会执行异步任务，而是等同步任务执行完后再执行</p>
<p>异步任务中的同步任务，会在当前异步的线程中队列执行，而异步任务则还是会再开线程或被分配到闲置线程</p>
<pre><code class="language-objective-c">- (void)gcdDemo4 {
  dispatch_queue_t q = dispatch_queue_create(&quot;name&quot;, DISPATCH_QUEUE_CONCURRENT);
  void (^task)() = ^{
    // 假设这段代码输出 3 线程
    NSLog(@&quot;%@&quot;, [NSThread currentThread]);
    // 那么下面同步任务中会等待 3 线程执行完后，再执行这个任务中的代码，也会输出 3 线程
    dispatch_sync(q, ^{
      NSLog(@&quot;%@&quot;, [NSThread currentThread]);
    });
  };
  // 把task添加到q队列
  dispatch_async(q, task);
}
</code></pre>
<h2>全局队列</h2>
<p>本质上是并发队列，会开启线程，不会顺序执行。与并发队列不同的是有固定队列名，MRC下并发队列需要手动释放内存，全局队列不需要。但现在因为是ARC自动管理内存，所以都不需要手动释放内存。</p>
<pre><code class="language-objective-c">dispaych_queue_t g = dispatch_get_global_queue(0, 0);
</code></pre>
<ul>
<li>参数 1</li>
</ul>
<p>涉及到系统适配</p>
<pre><code class="language-objective-c">// iOS 8 服务质量
QOS_CLASS_USER_INTERACTIVE // 用户交互（希望线程快速被执行，不要用耗时操作）
QOS_CLASS_USER_INITIATED   // 用户需要（不要用耗时操作）
QOS_CLASS_DEFAULT          // 默认的（给系统重置队列）
QOS_CLASS_UTILITY          // 实用工具（做耗时操作）
QOS_CLASS_BACKGROUND       // 后台执行
QOS_CLASS_UNSPECIFIED      // 没有指定优先级

// iOS 7 调度的优先级
DISPATCH_QUEUE_PRIORITY_HIGH       // 高优先级
DISPATCH_QUEUE_PRIORITY_DEFAULT    // 默认优先级
DISPATCH_QUEUE_PRIORITY_LOW        // 低优先级
DISPATCH_QUEUE_PRIORITY_BACKGROUND // 后台优先级
</code></pre>
<ul>
<li>参数 2</li>
</ul>
<p>为未来使用的一个保留，现在始终是 0</p>
<h2>GCD延迟执行</h2>
<p>异步任务，经过一定的时间后，使用指定线程，执行代码块的任务</p>
<pre><code class="language-objective-c">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  NSLog(@&quot;%@&quot;, [NSThread currentThread]);
});
</code></pre>
<ul>
<li>参数 1</li>
</ul>
<p>等待时间，<code>DISPATCH_TIME_NOW</code>开始，等待 1.0 秒（后面参数是毫秒）</p>
<pre><code class="language-objective-c">dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
</code></pre>
<ul>
<li>参数 2</li>
</ul>
<p>指定线程</p>
<ul>
<li>参数 3</li>
</ul>
<p>代码块</p>
<h2>执行一次</h2>
<p>不仅能保证只执行一次，而且是线程安全</p>
<pre><code class="language-objective-c">static dispatch_once_t onceToken;
dispatch_once(&amp;onceToken, ^{
  NSLog(@&quot;%@&quot;, [NSThread currentThread]);
});
</code></pre>
<h2>调度组</h2>
<p>组队列可以把多个队列放进组，可以做到等待所有组内队列执行完毕后再执行一些动作</p>
<pre><code class="language-objective-c">// 队列
dispatch_queue_t q = dispatch_get_global_queue(0, 0);
// 调度组
dispatch_group_t g = dispatch_group_create();
// 添加任务
dispatch_group_async(g, q, ^{
  NSLog(@&quot;任务一完成&quot;);
});
dispatch_group_async(g, q, ^{
  NSLog(@&quot;任务二完成&quot;);
});
dispatch_group_async(g, q, ^{
  NSLog(@&quot;任务三完成&quot;);
});
// 所有任务完成后执行的动作 传入 组、使用的线程、代码块
dispatch_group_notify(g, dispatch_get_main_queue(), ^{
  NSLog(@&quot;所有任务执行完毕&quot;);
});
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 – 多线程]]></title>
            <link>https://imba97.com/posts/387</link>
            <guid isPermaLink="false">https://imba97.com/posts/387</guid>
            <pubDate>Tue, 13 Aug 2019 14:17:27 GMT</pubDate>
            <content:encoded><![CDATA[<h2>互斥锁</h2>
<pre><code class="language-objective-c">
@property(nonatomic, assign) init tickets;

- (void)touchesBegan:(NSSet&lt;UITouch *&gt; *)touches withEvent:(UIEvent *)event {
  self.tickets = 20;

  NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
  [t1 start];

  NSThread * t2 = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];
  [t2 start];
}

- (void)saleTickets {
  while(YES) {
    [NSThread sleepForTimeInterval: 1.0];
    // 互斥锁可以让锁内代码同一时间只被一条线程运行
    @synchronized(self) {
      if(self.tickets &gt; 0) {
        self.tickets--;
        NSLog(@&quot;剩下%d张票&quot;, self.tickets);
      } else {
        NSLog(@&quot;卖完了&quot;);
        break;
      }
    }
  }
}
</code></pre>
<!--more-->
<h2>自旋锁</h2>
<p>原子属性内部的锁</p>
<pre><code class="language-objective-c">@property(atomic, assign) init tickets;
</code></pre>
<h2>相同点与不同的</h2>
<p>相同点</p>
<ul>
<li>消耗性能、效率不高</li>
<li>能够保证线程安全</li>
</ul>
<p>不同点</p>
<ul>
<li><strong>自旋锁</strong>读取时不会锁</li>
<li>线程被<strong>互斥锁</strong>锁在外面，线程会进入休眠状态，等待锁打开后被唤醒</li>
<li>线程被<strong>自旋锁</strong>锁在外面，线程会进入死循环状态等待开锁</li>
</ul>
<h2>RunLoop</h2>
<p>主线程默认开启RunLoop，子线程默认关闭，需要手动开启</p>
<pre><code class="language-objective-c">@interface ViewController()
@property(assign, nonatomic, getter=isFinished)BOOL finished
@end

- (void)viewDidLoad {
  [super viewDidLoad];

  // 先执行demo
  NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
  [t1 start];

  self.finished = NO;

  // 然后执行otherMethod
  // 如果不设置t1的RunLoop，执行完demo是不会执行otherMethod的
  [self performSelector:@selector(otherMethod) onThread:t1 withObject:nil waitUntilDone:NO];
}

- (void)demo {
  NSLog(@&quot;before&quot;);
  while(!self.isFinished) {
    // 开启RunLoop查询当前线程有没有事件
    [[NsRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
  NSLog(@&quot;after&quot;);
}

}

- (void)otherMethod {
  NSLog(@&quot;otherMethod function&quot;);
  // 执行完第二个方法后设置为YES
  self.finished = YES;
}

// 另一种方式 一般不用 很难停下来
[[NSRunLoop currentRunLoop] run];
</code></pre>
<h2>RunLoopMode</h2>
<ul>
<li><code>NSDefaultRunLoop</code> 时钟、网络事件模式，操作UI界面会停止执行，执行会非常耗时</li>
<li><code>NSRunLoopCommonModes</code> 用户交互模式</li>
</ul>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[阿里云服务器内网穿透的坑]]></title>
            <link>https://imba97.com/posts/379</link>
            <guid isPermaLink="false">https://imba97.com/posts/379</guid>
            <pubDate>Mon, 12 Aug 2019 17:35:35 GMT</pubDate>
            <content:encoded><![CDATA[<p>这篇文章主要不是说内网穿透的，而是说一下用阿里云服务器做内网穿透的注意事项</p>
<!--more-->
<p>首先是需要配置这个“安全组配置”，把需要用到的端口都配置一下，这个网上不少教程</p>
<p><img src="https://imba97.com/uploads/2019/08/0b4584c3292271b9c120b5fe5bb628e6.png" alt=""></p>
<p>但如果没成功，你还需要把防火墙端口打开，比如把<code>23333</code>端口打开</p>
<pre><code class="language-bash"># 开启端口
firewall-cmd --zone=public --add-port=23333/tcp --permanent
# 重启防火墙
systemctl restart firewalld
</code></pre>
<p>本来我以为安全组配置是跟防火墙同步的，后来发现并不是，所以得手动开一下</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[树莓派折腾笔记]]></title>
            <link>https://imba97.com/posts/371</link>
            <guid isPermaLink="false">https://imba97.com/posts/371</guid>
            <pubDate>Sat, 10 Aug 2019 13:45:58 GMT</pubDate>
            <content:encoded><![CDATA[<h2>解锁root</h2>
<pre><code class="language-bash">sudo passwd root
sudo passwd --unlock root
# 可能会提示 password expiry information changed
# 需要配置ssh，允许root用户登录
sudo nano /etc/ssh/sshd_config
# Ctrl + W 搜索 PermitRootLogin without-password 取消注释并改为
PermitRootLogin yes
</code></pre>
<!--more-->
<p>重启后就可以用root登录了，之后的操作我都是在root下，如果你没用root，自行加上<code>sudo</code></p>
<h2>切换国内软件源</h2>
<p>参考 [<a href="https://www.cnblogs.com/mq0036/p/8893733.html">1</a>]</p>
<p>版本对应表 [<a href="https://www.debian.org/releases/">2</a>]</p>
<p>版本号 | 对应代号</p>
<ul>
<li>
<pre><code>|     -
</code></pre>
</li>
</ul>
<p>6.0   |   squeeze<br>
7.0   |   wheezy<br>
8.0   |   jessie<br>
9.0   |   stretch<br>
10.0  |   buster<br>
11.0？|   bullseye</p>
<pre><code class="language-bash"># 首先查看版本
cat /etc/debian_version

# 备份配置
cp /etc/apt/sources.list /etc/apt/sources.list.bak

# 修改 sources.list
nano /etc/apt/sources.list
</code></pre>
<p>删除原有内容，配置国内的，这里以中国科学技术大学的软件源为例<br>
版本号不同，配置也不同，我的版本号是 10.0 所以是 buster</p>
<pre><code class="language-bash">deb http://mirrors.ustc.edu.cn/raspbian/raspbian/ buster main contrib non-free
deb-src http://mirrors.ustc.edu.cn/raspbian/raspbian/ buster main contrib non-free
</code></pre>
<p>保存退出，然后更新一下列表和软件</p>
<pre><code class="language-bash">apt-get update
apt-get upgrade
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 – 03]]></title>
            <link>https://imba97.com/posts/311</link>
            <guid isPermaLink="false">https://imba97.com/posts/311</guid>
            <pubDate>Thu, 08 Aug 2019 11:44:43 GMT</pubDate>
            <content:encoded><![CDATA[<h2>类</h2>
<!--more-->
<pre><code class="language-objective-c">// Person.h 声明
#import &lt;Foundation/Foundation.h&gt;
@interface Person : NSObject
- (void) funcname
+ (void) funcname
@end

// Person.m 实现
#import &quot;Person.h&quot;
@implementation Person
- (void) funcname {
  NGLog(@&quot;Hello OC&quot;);
}
+ (void) funcname {
  NGLog(@&quot;Hello OC&quot;);
}
@end
</code></pre>
<h2>减号方法</h2>
<p>也叫对象方法</p>
<ul>
<li>声明</li>
</ul>
<pre><code class="language-objective-c">- (void) funcname
</code></pre>
<ul>
<li>调用</li>
</ul>
<pre><code class="language-objective-c">// 实例化类
Person *p1 = [[Person allot] init];
// 然后再调用
[p1 funcname];
</code></pre>
<h2>加号方法</h2>
<p>也叫类方法、静态方法</p>
<ul>
<li>声明</li>
</ul>
<pre><code class="language-objective-c">+ (void) funcname
</code></pre>
<ul>
<li>调用</li>
</ul>
<pre><code class="language-objective-c">直接调用
[Person funcname];
</code></pre>
<h2>方法传参</h2>
<pre><code class="language-objective-c">// 一个参数
- (void) funcname:(int)a

// 多个参数
- (void) funcname:(int)a and:(int)b
</code></pre>
<p>方法名包括冒号，甚至可以声明下面的方法</p>
<pre><code class="language-objective-c">- (void):(int)a :(int)b
</code></pre>
<h2>propterty</h2>
<p>生成get、set方法、属性变量</p>
<pre><code class="language-objective-c">// Person.h
#import &lt;Foundation/Foundation.h&gt;
@interface Person : NSObject
{
  // 可以不写
  int _age;
  NSString *_name;
}

@property(nonatomic, assign) int age;
@property(nonatomic, assign) NSString *name;

- (void) say();

@end

// Person.m
#import &quot;Person.h&quot;
@implementation Person

// 可以不写
@synthesize age = _age;
@synthesize name = _name;

- (void) say() {
  NGLog(@&quot;my name is %s&quot;, _name);
  NGLog(@&quot;my name is %s&quot;, self.name); // self.name等于[self name]
}

@end
</code></pre>
<ul>
<li>
<p>参数1</p>
</li>
<li>
<p><code>natomic</code> 原子属性（线程安全），需要消耗大量资源</p>
</li>
<li>
<p><code>nonatomic</code> 非原子属性（非线程安全），适合内存小的设备</p>
</li>
<li>
<p>参数2</p>
</li>
<li>
<p><code>assign</code> 直接赋值</p>
</li>
<li>
<p><code>strong</code> 强引用，销毁后指向的内存数据也会被销毁</p>
</li>
<li>
<p><code>weak</code> 弱引用</p>
</li>
<li>
<p>参数3</p>
</li>
<li>
<p><code>readonly</code>只能读取（只生成get方法）</p>
</li>
</ul>
<p>set方法是<code>setAge</code>，变量名第一个字母大写<br>
get方法是<code>age</code>，变量名<br>
直接可以调用</p>
<pre><code class="language-objective-c">Person *p = [[Person alloc] init];
[p age]
</code></pre>
<p>使用<code>@propterty</code>后也可以定义<code>setOne</code>方法来进行重写set、get方法</p>
<p>注意自动生成的get方法可能会造成泄漏，可以改为以下写法</p>
<pre><code class="language-objective-c">- (void)setName:(char*)nameValue {
  // 自动生成的是 name = nameValue;
  if(nameValue) {
    if(name != nameValue) {
      // 释放空间
      free(name);
      // 置空（好像可以不写）
      name = NULL;

      // 获取nameValue的长度
      int len = StrLength(nameValue) + 1;
      // 设置长度
      name = (char*) malloc(len);
      // 赋值
      strcpy(name, nameValue);
    }
  }
}
</code></pre>
<h2>synthesize</h2>
<p>合成指令，在 Xcode 4.5 之前非常常见</p>
<h2>点表达式</h2>
<pre><code class="language-objective-c">p.age = 20;
// 等于 [p setAge:20]

NGLog(@&quot;age = %d&quot;, p.age);
// 等于 [p age];
</code></pre>
<p>点表达式如果做左值，就是调用set方法，如果做右值则是调用get方法</p>
<h2>static 关键字</h2>
<pre><code class="language-objective-c">void func() {
  static int a = 0;
  int b = 0;
  a++;
  b++;
}

// 如果多次调用
func(); // a = 1 ,b = 1
func(); // a = 2, b = 1

// static int a = 0; 只会被执行一次，把a放入静态存储区
</code></pre>
<h2>self 关键字</h2>
<p>类似this这种</p>
<h2>前置声明</h2>
<p>也叫不完全声明</p>
<pre><code class="language-objective-c">// .h
@class ClassName

// .m
import &quot;ClassName.h&quot;
</code></pre>
<p>告诉编译器，这是个类，但类中有什么方法不会加载进来<br>
可以直接在<code>.h</code>文件中导入<code>ClassName.h</code>，但运行效率低，不推荐</p>
<h2>dealloc</h2>
<p>析构函数，用来释放内存空间<br>
需要在编译设置<code>Build Phases &gt; Compile Sources &gt; 相应的文件</code>加上<code>-fno-objc-arc</code>表示手动维护内存</p>
<pre><code class="language-objective-c">// Person.m
- (void) dealloc {
  [super dealloc]
  NGLog(@&quot;dealloc function called&quot;);
}

// main.m
Person *p = [[Person alloc] init];
// 手动释放
[p release];
// 释放后dealloc会被自动调用
</code></pre>
<p>现在有arc机制，自动维护内存</p>
<h2>初始化传参</h2>
<p>首先需要自定义一个初始化函数，函数名必须以<code>init</code>开头，并遵循以下规则</p>
<ol>
<li>判断参数有效性</li>
<li>初始化父类</li>
<li>初始化实例变量</li>
</ol>
<pre><code class="language-objective-c">// Person.h
- (id)initWithName:(NSString*)nameValue age:(int)ageValue;

// Person.m
- (id)initWithName:(NSString*)nameValue age:(int)ageValue {
  // 1
  if(!nameValue || ageValue &gt; 120) {
    return nil;
  }

  // 2
  self = [super init];
  if(!self) {
    return nil;
  }

  // 3
  self.name = [[NSString alloc]initWithString:nameValue];
  if(!self.name) {
    self = nil;
    return nil;
  }
  self.age = ageValue;
}
</code></pre>
<h2>协议和代理</h2>
<p>协议有约束作用，只声明不实现，类似抽象类</p>
<!--more-->
<pre><code class="language-objective-c">// nameDelegate.h
#import &lt;Foundation/Foundation.h&gt;

// 声明
@protocol nameDelegate&lt;NSObject&gt;

// 必须实现
@required

- (void) say();

// 可选
@optional

// ...

@end
</code></pre>
<p>定义两个类文件</p>
<pre><code class="language-objective-c">// Animal.h
#import &lt;Foundation/Foundation.h&gt;
#import &quot;nameDelegate.h&quot;
@interface Person : NSObject&lt;nameDelegate&gt;

@property(nonatomic, assign) id&lt;nameDelegate&gt; delegate;

- (void)sayNyan {
  NGLog(@&quot;喵喵喵&quot;);
  // 通知人类我喵喵喵了
  [self.delegate say];
}

@end

// Person.h
#import &lt;Foundation/Foundation.h&gt;
#import &quot;nameDelegate.h&quot;

@class Animal

@interface Person : NSObject&lt;nameDelegate&gt;

@property(nonatomic, strong) Animal *nyanko

@end

// Person.m
import &quot;Person.h&quot;

@implementation Person

- (id)init {
  self = [super init];
  if(!self) {
    return nil;
  }

  self.nyanko = [[Animal alloc] init];
  if(!self.nyako) {
    self = nil;
    return nil;
  }

  // 代理 ？？？
  self.nyanko.delegate = self;
  return self;
}

// 实现协议
- (void)say {
  NGLog(@&quot;它喵喵喵了&quot;);
}

@end
</code></pre>
<p>使用</p>
<pre><code class="language-objective-c">// main.m
#import &lt;Foundation/Foundation.h&gt;
#import &quot;Person.h&quot;
#import &quot;Animal.h&quot;

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // 造猫
    Person * cat = [[Person alloc] init];
    // 让她叫
    [p.nyanko sayNyan];
  }
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 – 02]]></title>
            <link>https://imba97.com/posts/310</link>
            <guid isPermaLink="false">https://imba97.com/posts/310</guid>
            <pubDate>Wed, 07 Aug 2019 18:43:24 GMT</pubDate>
            <content:encoded><![CDATA[<h2>函数</h2>
<pre><code class="language-objective-c">// main.m
#import &lt;Foundation/Foundation.h&gt;

// C风格函数
int sumFunc(int a, int b) {
  return a + b;
}

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    int sum = sumFunc(1, 2);
    NGLog(@&quot;sum = %d&quot;, sum);
  }
  return 0;
}
</code></pre>
<!--more-->
<h2>指针 和 block</h2>
<p>常用于多线程、异步任务、集合遍历、集合排序、动画转场</p>
<pre><code class="language-objective-c">// main.m

void test() {
  printf(&quot;喵喵喵&quot;);
}

int sum(a, b) {
  return a + b;
}

// 定义指针
void (*p)();

// 定义block
typedef int (^b)(int, int);

int main(int argc, const char * argv[]) {
  @autoreleasepool {
    // 指向test
    p = test;
    // 调用
    (*p)(); // = test(); = p();

    // block也可以调用函数
    b = sum;
    b(1, 2);

    // 直接定义函数
    void(^myBlock)() = ^{
      NSLog(@&quot;这是个block&quot;);
    }

    // 带参数
    void(^myBlock2)() = ^(int a, int b) {
      return a + b;
    }
  }
}
</code></pre>
<h2>结构体 和 指针</h2>
<pre><code class="language-objective-c">// main.m
@autoreleasepool {
  // 定义结构体
  struct point {
    int x;
    int y;

    // 指针
    struct point * nextP;
  }
  // 创建
  struct point p;
  // 赋值
  p.x = 10;
  p.y = 10;

  // 第二个结构体
  struct circle {
    // 可以包含其他结构体
    struct point pp;
    // 定义本结构体的变量
    int radius;
  }

  struct circle c;

  c.pp = p;
  c.radius = 50;

  // 定义结构体也可以使用
  typedef struct _student {
    char name;
    int age;
  } student;
  // 调用
  student a;
  a.name = &quot;喵喵喵&quot;;
  a.age = 20;

  // 使用指针
  struct point * tmpPoint;
  // 指向p的地址
  tmpPoint = &amp;p;

  tmpPoint-&gt;x = 10;
  tmpPoint-&gt;y = 10;

  // 使用内部定义的指针指向pointB的地址
  struct point pointB;
  pointB.x = 233;
  pointB.y = 233;
  p.nextP = &amp;pointB;
}
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Objective-C 笔记 - 基本类型]]></title>
            <link>https://imba97.com/posts/299</link>
            <guid isPermaLink="false">https://imba97.com/posts/299</guid>
            <pubDate>Tue, 06 Aug 2019 17:03:44 GMT</pubDate>
            <content:encoded><![CDATA[<h2>数据类型</h2>
<ul>
<li>数组</li>
</ul>
<p>存储指向对象的指针</p>
<p>定义一维数组</p>
<pre><code class="language-objective-c">int core[10];
for(int i = 0; i &lt; 100; i++) {
  NSLog(@&quot;core[%d] = %d&quot;, i, core[i]);
}
// 没有初始化之前 里面是垃圾值
</code></pre>
<!--more-->
<p>定义二维数组</p>
<pre><code class="language-objective-c">int core2[3][5];
for(int i = 0; i &lt; 3; i++) {
  for(int j = 0; j &lt; 5; j++) {
    core2[i][j] = i + j;
    NSLog(@&quot;core2[%d][%d] = %d&quot;, i, j, core[i][j]);
  }
}
</code></pre>
<h2>枚举</h2>
<p>C语言定义枚举类型，枚举的数据类型不确定，会默认使用<code>int</code><br>
iOS 6.0 之后推出两个宏</p>
<ul>
<li>NS_ENUM 可以指定数据类型</li>
<li>NS_OPTION 可以使用位移，用位或来设置数值</li>
</ul>
<h2>#define</h2>
<p>宏定义（全局变量）<br>
一般用于定义全局、值不变的内容</p>
<pre><code class="language-objective-c">#define COLOR 0xffffff
#define IMAGE_PATH @&quot;image.png&quot;
#define iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO)
</code></pre>
<h2>#import</h2>
<p>包含预警功能，已经包含过的文件不会再次包含。</p>
<pre><code class="language-objective-c">// 尖括号是从系统指定目录下查找
#import &lt;Foundation/Foundation.h&gt;
// 引号是从用户目录下查找
#import &quot;Person.h&quot;
</code></pre>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[Git服务器自动同步部署到项目]]></title>
            <link>https://imba97.com/posts/280</link>
            <guid isPermaLink="false">https://imba97.com/posts/280</guid>
            <pubDate>Tue, 06 Aug 2019 11:37:54 GMT</pubDate>
            <content:encoded><![CDATA[<p>上次简单说了<a href="/posts/281">搭建Git服务器</a>，这次来聊聊自动同步部署到项目。这种方式会很方便，比如用在网站上，只需要点一下push，线上的网站就会同步。这个教程的前提是网站放在Git服务器上。</p>
<!--more-->
<h1>前言</h1>
<p>发现之前的文章有几处错误，又经过一大堆测试，直接登录git用户模拟了一遍操作等，终于找到比较完善的方法了。</p>
<p><img src="https://imba97.com/uploads/2019/08/ef06969c39226befcb45e6b110ee3e08.png" alt=""></p>
<h1>解决疑难杂症</h1>
<ol>
<li>查看项目目录下的<code>.git</code>，所属是否是<code>git</code>用户的，如果不是需要修改</li>
</ol>
<pre><code class="language-bash">chown -R git:git .git
</code></pre>
<ol start="2">
<li>git报错</li>
</ol>
<pre><code class="language-bash">[git用户 g]$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 2), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From /git/g
   528b0d2..9b2e4c3  master     -&gt; origin/master
Updating 528b0d2..9b2e4c3
error: unable to unlink old 'index.php' (Permission denied)
</code></pre>
<p>没有权限更新，解决这个问题可以使用<code>sudo</code>命令，但这样又会引出一个新问题。</p>
<ol start="3">
<li>sudo问题</li>
</ol>
<p>你需要先在<code>/etc/sudoers</code>文件中把执行sudo命令的用户加上，否则会报错</p>
<pre><code class="language-bash">vim /etc/sudoers
</code></pre>
<p>把git用户加上，并且不需要密码</p>
<pre><code class="language-bash">git ALL=(ALL)NOPASSWD:ALL
</code></pre>
<p>但sudo还有一个问题</p>
<pre><code class="language-bash">[git@用户 g]$ sudo git pull
Updating 528b0d2..9b2e4c3
Fast-forward
 index.php | 1 +
 1 file changed, 1 insertion(+)

[git@用户 g]$ ll
-rw-r--r-- 1 root root  5105 Aug 14 19:03 index.php
</code></pre>
<p>就是用户和用户组变成了<code>root</code>，这样网页会打不开显示拒绝访问，就不得不使用<code>chown</code>来更换，所以Git钩子文件就得这样写。</p>
<h1>Git钩子</h1>
<p>在相应的Git项目目录中，进入<code>hooks</code>目录，新建或更改<code>post-receive</code>文件，用自己装的编辑器打开，使用命令。</p>
<pre><code class="language-bash">vim post-receive
</code></pre>
<p>输入以下信息</p>
<pre><code class="language-bash">#!/bin/sh
cd /www/wwwroot/myWebSite || exit
unset GIT_DIR
sudo git reset --hard
sudo git pull origin master
sudo chown -R www:www *
</code></pre>
<p><code>sudo git reset --hard</code>是解决报错<code>Your local changes would be overwritten by merge. Commit, stash or revert them to proceed</code>，这是因为拉取代码时发现本地代码有过修改，所以提示你提交一下项目目录代码，但我们并不要提交，于是使用<code>sudo git reset --hard</code>可以放弃项目目录修改（其实不是修改，只是跟git上的不同），让拉取的代码覆盖项目目录的代码，实现硬更新。</p>
<p><code>sudo chown -R www:www *</code>是把当前目录所有文件的所有者改为<code>www</code>用户和<code>www</code>用户组，这个用户是文件原本的用户，如果你的是其他的请自行更改，<code>.git</code>是隐藏目录所以不会被修改，但如果被修改需要你再改回来，在下面添加命令<code>sudo chown -R git:git .git</code>就可以。</p>
<p>这样就完成了，下次push项目后就会被触发，自动在<code>myWebSite</code>目录下拉取相应分支的更新、更改用户和用户组。</p>
<h1>后言</h1>
<p>像这种更改<code>git</code>用户获取权限不用密码的方式，我不确定安全性怎么样，还请大佬指教。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[搭建自己的Git服务器]]></title>
            <link>https://imba97.com/posts/281</link>
            <guid isPermaLink="false">https://imba97.com/posts/281</guid>
            <pubDate>Thu, 01 Aug 2019 14:17:39 GMT</pubDate>
            <content:encoded><![CDATA[<h1>服务器信息</h1>
<p>CentOS 7.0 x86_64 (64bit)<br>
这里用CentOS举例</p>
<h1>安装Git</h1>
<pre><code class="language-bash"># CentOS
yum install git-core
# Ubuntu
apt-get install git
</code></pre>
<p>不多赘述，安装教程一搜一大把<br>
<a href="https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git">官网安装教程</a></p>
<!--more-->
<h1>创建用户</h1>
<p>如果整个服务器只有你自己用，那无所谓，用root都行（大概吧）</p>
<pre><code class="language-bash"># 一般来说会创建一个名叫git的用户
# 命令：adduser username
adduser git
</code></pre>
<p>设置密码</p>
<pre><code class="language-bash"># 命令：passwd username
passwd git
# 然后输入密码
</code></pre>
<h1>创建仓库</h1>
<p>移动到需要创建仓库的目录中执行</p>
<pre><code class="language-bash"># 比如我在 `/home/git/test/`下创建
cd /home/git/test
# 你可以切换到git用户来创建
su git
# 初始化仓库
git init --bare xxx.git
# 修改仓库所有者（不是git用户创建的情况下）
sudo chown -R git:git xxx.git
</code></pre>
<h1>创建SSH</h1>
<h3>服务端</h3>
<p>这步是为了不用每次提交、同步等操作都要输入密码<br>
在用户目录下，也就是<code>/home/username/</code>中，有个隐藏目录<code>.ssh</code><br>
如果没有需要先创建，用户名叫git所以就在<code>/home/git/</code>下</p>
<pre><code class="language-bash"># 移动至用户目录，如果用git登录的可以直接 cd ~
cd /home/git/
# 查看是否有.ssh
ls -a
# 如果没有，创建文件夹 并 设置文件夹所属 并 设置权限
# 命令：mkdir .ssh &amp;&amp; chown -R username:groupname .ssh &amp;&amp; chmod 700 .ssh
mkdir .ssh &amp;&amp; chown -R git:git .ssh &amp;&amp; chmod 700 .ssh
# 如果有 直接跳到下一步创建 authorized_keys
</code></pre>
<p>接着在<code>.ssh</code>目录下新建一个储存公钥的文件</p>
<pre><code class="language-bash"># 移动到.ssh
cd .ssh
# 创建文件 并 设置文件所属 并 设置权限
# 命令：touch authorized_keys &amp;&amp; chown -R username:usergroup authorized_keys &amp;&amp; chmod 600 authorized_keys
touch authorized_keys &amp;&amp; chown -R git:git authorized_keys &amp;&amp; chmod 600 authorized_keys
</code></pre>
<h3>本地</h3>
<p>生成公钥，如果你本地没有公钥，则需要生成一个，打开终端输入</p>
<pre><code class="language-bash">ssh-keygen -t rsa
</code></pre>
<p>我本地是Windows<br>
生成的公钥在<code>C:\Users\username\.ssh\id_rsa.pub</code>，<code>username</code>是你的用户名<br>
最后只需要把<code>id_rsa.pub</code>的内容粘贴到刚才在服务器创建的<code>authorized_keys</code>里就可以<br>
多个公钥每行一个</p>
<h1>克隆到本地</h1>
<pre><code class="language-bash"># git clone username@ip或域名:根目录开始的路径
git clone git@255.255.255.255:/home/git/test/xxx.git
</code></pre>
<h1>后言</h1>
<p>其实我在搭Git服务器的时候，主要问题在SSH，看了很多教程都是简单带过。最后慢慢试终于成功了，所以分享一下。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[在SVG中使用Javascript]]></title>
            <link>https://imba97.com/posts/249</link>
            <guid isPermaLink="false">https://imba97.com/posts/249</guid>
            <pubDate>Tue, 30 Jul 2019 12:44:41 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近研究SVG图，发现可以使用<code>javascript</code>。但SVG是XML，直接在里面写<code>javascript</code>会报错。</p>
<p>你需要加一个，不让XML解析的标识 <code>&lt;![CDATA[]]&gt;</code>。</p>
<!--more-->
<pre><code class="language-xml">&lt;svg&gt;
  ...
  &lt;script&gt;
    &lt;![CDATA[
      alert(233); // 这里面写javascript代码
    ]]&gt;
  &lt;/script&gt;
&lt;/svg&gt;
</code></pre>
<p>以上类似的教程网上有不少，我主要想说：</p>
<p>有一点需要注意，这种js无法在<code>&lt;img&gt;</code>或<code>background-image</code>引用的时候运行，也就是如果SVG是个时钟，js控制着时分秒针的运动，那么这张SVG图被当做图片引用会变成一张静态图片。</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[共产风人设]]></title>
            <link>https://imba97.com/posts/135</link>
            <guid isPermaLink="false">https://imba97.com/posts/135</guid>
            <pubDate>Tue, 07 Aug 2018 03:37:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>人设图</p>
<!--more-->
<p><img src="https://imba97.com/uploads/2018/08/20180414233331-768x1212.png" alt=""></p>
<p>作者：<a href="https://space.bilibili.com/561845"><font style="color: #d62b2b;"><strong>@Illeindliass</strong></font></a></p>
<p>感谢大佬的画，可以说是非常的强了</p>
<p>同时感谢第一张人设的画师：<a href="https://www.weibo.com/u/5390007285"><font style="color: #d62b2b;"><strong>@阿布</strong></font></a></p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[switch关灯工具]]></title>
            <link>https://imba97.com/posts/139</link>
            <guid isPermaLink="false">https://imba97.com/posts/139</guid>
            <pubDate>Tue, 07 Aug 2018 03:34:00 GMT</pubDate>
            <content:encoded><![CDATA[<pre><code>程序名：switch关灯工具
开发语言：还是JavaScript
开发框架：还是jQuery
开发总用时：≈12小时
</code></pre>
<!--more-->
<p><img src="https://imba97.com/uploads/2018/08/switch_1.png" alt=""></p>
<p>用途：看视频非全屏时用的，将播放器周围变暗，变得更容易观看（理论上，不过讲真我觉得无所谓，因为我微博背景是个动图，我刷微博都没被背景分过心。</p>
<p>曾经在朋友圈还是空间来着看到过有人做过这个东西，当时感觉贼jer厉害，现在懂了原理之后，想尝试着做做，最开始凉了，放置了一段时间之后，换了另一种思路，姑且就做出来了这个switch关灯工具。</p>
<p>原来的尝试是想获取网页中的video或object等视频相关的标签进行判断，再把父级元素的z-index（网页中层次排序）设置高，然后加入一个稍微低于前面父级元素但高于网页中其他元素的黑色背景。最后做了个只支持B站直播页面的（是用这个页面进行了各种尝试）。</p>
<p>但因为每个网页结构不同，video标签的父级标签不一定是啥，就算知道了父级还需要看父级的父级还有没有父级父亲节快乐，必须在最外层加z-index才有用（应该是这样，再说网页代码可能还会随着网页的更新而改变，不太好弄。所以就这样凉了。</p>
<p>之后的方法是这个：</p>
<p><img src="/uploads/2018/08/switch_2.jpg" alt=""></p>
<p>把域名和这个网站对应元素的id或class放上</p>
<p>这样可以根据不同的域名找到不同的元素来进行操作</p>
<p>（现在B站直播页面不能用了，这个就是网页更新会出现的问题，刚才看了一下并不是id变了，而是元素的排序问题让z-index失效了。</p>
<p>接下来介绍一下程序的安装页面（也算一个小开发）</p>
<p>支持中日双语：</p>
<p><img src="/uploads/2018/08/switch_3.gif" alt=""></p>
<p>然后这个页面需要先“开灯”，算是呼应主题</p>
<p><img src="/uploads/2018/08/switch_4.gif" alt=""></p>
<p>emmm...</p>
<p>至于桌子中间那个，为什么要用这么一个图，首先说一下这里面所有东西都是自己随便画的（鼠绘）。符合本人个性，毕竟我做的页面、程序之类的基本都是以搞怪为主题的（并不是老司机。</p>
<p><img src="/uploads/2018/08/switch_5.jpg" alt=""></p>
<p>这是所有的图。可以很清楚的看到无码版的是个棕色皮肤人种的“蒜头鼻”。</p>
<p>有人就要问了，那为什么是棕色皮肤人种的“蒜头鼻”呀？这个原因很简单，因为棕色皮肤人种介于白色皮肤和黑色皮肤之间，而关灯程序恰恰就是在白色的网页跟黑色的网页之间转换（大部分网页是白底，关灯后就变黑了。</p>
<p>“蒜头鼻”是因鼻子头部皮下脂肪和纤维组织厚，鼻翼软骨增生等原因造成的，而关灯程序的原理是把一些代码增加到原有的网页中，就仿佛网页的皮下脂肪和纤维组织变厚、软骨增生一样，所以得此“蒜--头--鼻”。（滑稽</p>
<p>这个解释真的太完美了，我自己都信了<img src="https://s20.postimg.cc/dyhymox71/640_4.jpg" width="60" /></p>
<p>有理有据，让人信服</p>
<img src="https://s20.postimg.cc/jmo9dl999/640_5.jpg" width="200" />
<p>页面里的提示：</p>
<p><img src="/uploads/2018/08/switch_6.png" alt=""></p>
<p>以及，日语版</p>
<p><img src="/uploads/2018/08/switch_7.png" alt=""></p>
<p>设置及设置的提示：</p>
<p><img src="/uploads/2018/08/switch_8.jpg" alt=""></p>
<p>还有蒜头鼻：</p>
<p><img src="/uploads/2018/08/switch_9.png" alt=""></p>
<p>嗯。非常人性化的提示。（至于写的是啥视频里貌似能看到 2333</p>
<p><img src="/uploads/2018/08/switch_10.gif" alt=""></p>
<p>使用方法：</p>
<p>将这个打了码的棕色人种的蒜头鼻拖进收藏夹</p>
<p><img src="/uploads/2018/08/switch_11.gif" alt=""></p>
<p>设置：</p>
<p>设置可以通过两种方式</p>
<p>手动点击桌子上的设置按钮，这样的话是个空的设置，留空的话为默认原始设置（应该，我忘了</p>
<p>还可以通过直接点击你收藏夹的按钮，像这样：</p>
<p><img src="/uploads/2018/08/switch_12.gif" alt=""></p>
<p>会读取你之前的设置，如果版本不同会提示基于哪个版本，还会显示当前最新版本</p>
<p>下面是重新设置：</p>
<p><img src="/uploads/2018/08/switch_13.gif" alt=""></p>
<p>改了个名字和快捷键，设置好后再次把“蒜头鼻”拖进收藏夹就可以了。点击不同的按钮会获取按钮相应的设置</p>
<p>没什么用的：</p>
<p>可以说是程序reset，也可以叫彩弹</p>
<p><img src="/uploads/2018/08/switch_14.gif" alt=""></p>
<p>（不过公布出来就不叫彩弹了吧，就是点10下上面的灯，会掉下来，然后进入初始界面。</p>
<p>实际演示：</p>
<p><img src="/uploads/2018/08/switch_15.gif" alt=""></p>
<p>目前理论还支持B站、A站、优酷土豆、爱奇艺、微博、乐视、N站、油管（因为没试所以理论上支持。</p>
<p><a href="https://www.bilibili.com/video/av18696545"><strong>开发视频</strong></a>了解一下</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
        <item>
            <title><![CDATA[人型自走输入法]]></title>
            <link>https://imba97.com/posts/119</link>
            <guid isPermaLink="false">https://imba97.com/posts/119</guid>
            <pubDate>Sat, 04 Aug 2018 15:42:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>今儿来介绍一个网页程序。</p>
<pre><code>程序名：人型自走输入法
开发语言：JavaScript
开发框架：jQuery
开发总用时：≈30小时
</code></pre>
<!--more-->
<p><img src="https://imba97.com/uploads/2018/08/jpinput_1.jpg" alt=""></p>
<p>我做的程序一般都是自己需要的（这个不算，只是一时兴起，最初做这个的契机是因为沪江小D（一个多语言翻译、查词等功能的网站），沪江小D的日语查询页面是自带一个点选输入日语的输入法：</p>
<p><img src="/uploads/2018/08/jpinput_2.jpg" alt=""></p>
<p>（网站改版了，之前也差不多是这样的</p>
<p>于是根据这个做出来第一版：</p>
<p><img src="/uploads/2018/08/jpinput_3.gif" alt=""></p>
<p>支持点选输入、平假名片假名切换的功能，其实还是比较简陋的。好处就是可以在除沪江小D之外的其他网页中使用（也不是所有的</p>
<p>后来想这种点选的还是不太人性化，用户体验不是很好，于是重新构思，就这样用键盘输入的雏形就出来了。</p>
<p>最后出现了这个可以用键盘输入的：</p>
<p><img src="/uploads/2018/08/jpinput_4.gif" alt=""></p>
<p>（这个是2.1版本，有汉字的判断，但数据库还是只有5个へんたい，23333</p>
<p>姑且先说一下这个汉字吧，输入法程序本体，从汉字的查询到显示已经基本OK（流程：最后一次输入后等待1秒向服务器发送当前输入的假名→检索数据库→返回结果→服务器端分页→返回数据到输入法程序→输入法程序本体再次进行分页→显示），但是在服务器端添加汉字的问题，和动词、形容词等词的查询问题一直没解决（其实是好久没维护了，基本弃坑了），之前想过几种方案，像完全匹配法，输入いきます，就根据数据库存的いきます返回相应的带汉字的“行きます”。</p>
<p>就像目前数据库里的へんたい们一样：</p>
<p><img src="/uploads/2018/08/jpinput_5.png" alt=""></p>
<p>但这种方法会在数据库存入大量相同的词，否决。毕竟能用程序判断的还是用程序判断解决比较好。</p>
<p>还一个方案是过滤掉像する、します、できる等等这种“送り仮名”，然后只把前面的部分进行数据库匹配。送り仮名是汉字词尾的假名（する这种不太确定是不是送り仮名，反正就是“汉字词尾的假名”这个意思），一般很多一样的。比如送り这个词，り就叫做送り仮名。</p>
<p>但之后又被我否决了。因为每次输入后都进行一遍匹配可能比较吃力。</p>
<p>这个方案在“<a href="http://data.imba97.com/Jpinput"><strong>data.imba97.com/Jpinput</strong></a>”进行了演示</p>
<p>这个问题一直没解决因为懒 233。不过应该可以用模糊查询解决的，得到一串假名后先从第一个假名判断返回汉字，再判断前两个有没有汉字，再判断前三个依此类推这样应该可以，以后试。（先弃坑算了 2333。（将来如果可以想直接用API解决</p>
<p>至于服务器端分页跟输入法程序本体分页，最开始是只有输入法程序本体分页的，流程是：输入后等待1秒向服务器发送假名→向数据库查询并返回5条数据→显示并储存到输入法程序本体的数组内。储存在本地的好处不用多说，因为避免再次请求服务器（返回上一页的时候。</p>
<p>之后改为服务器分页和输入法程序本体分页两种分页模式，每次向服务器请求，一次性查询并返回25条数据，然后储存在输入法程序本体的数组内，再通过输入法本体的分页把它分为5个词/页（上面演示图里是3个词/页。如果输入法本体的翻页翻到最后一页，再次向服务器发送请求，返回服务器分页第二页的数据，依此类推。这样又减少了请求次数，而且还加快了翻页流畅度，毕竟不用每翻一页都请求一次。</p>
<p>其他的一些功能。</p>
<p>版本检测功能：</p>
<p><img src="/uploads/2018/08/jpinput_6.gif" alt=""></p>
<p>确认的话直接跳转到安装程序的网页（写作安装读作拖进收藏夹</p>
<p>取消的话也可以正常使用之前版本的</p>
<p>自动加载jQuery功能：</p>
<p><img src="/uploads/2018/08/jpinput_7.gif" alt=""></p>
<p>程序定位及优点：</p>
<ol>
<li>安装、使用方法简单，程序轻巧。</li>
<li>给电脑没有日语输入法和不想切来切去的人使用。</li>
<li>能满足普通的输入和假名、单词的查询（在百度等网站使用。</li>
</ol>
<p>缺点也很明显：</p>
<ol>
<li>只能在PC端的浏览器中使用。</li>
<li>版本更新和汉字输入涉及到跨域连接服务器，暂不支持https协议的网站（但可以进行假名的输入，输入法服务器网站如果也是https应该可以解决这一问题。（应该</li>
<li>不支持屏蔽加载外部JavaScript的网站。</li>
</ol>
<p><img src="/uploads/2018/08/jpinput_8.png" alt=""></p>
<p><a href="https://www.bilibili.com/video/av8521956"><strong>开发视频</strong></a>了解一下，流程还是很全的<br>
有兴趣的可以研究一下（虽然不是高端的东西</p>
]]></content:encoded>
            <author>mail@imba97.cn (imba97)</author>
        </item>
    </channel>
</rss>