1333 lines
138 KiB
HTML
1333 lines
138 KiB
HTML
<!DOCTYPE html><html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。"><link rel="icon" href="/images/Bi.ico"><script defer src="https://umami.biss.click/script.js" data-website-id="3d97b310-b241-4fc6-bc9c-68e317fc7d42"></script><script>window.SITE_CONFIG = {"site":{"url":"https://blog.biss.click","name":"Bi's Blog","description":"Personal blog by biss.","language":"zh-CN","domain":"blog.biss.click","slogan":"好奇心转化为代码的空间。","startTime":"2025-01-01T00:00:00+08:00","copyrightStartYear":2025,"license":"内容采用 CC BY-NC-SA 4.0 协议"},"author":{"name":"biss","email":"bishsh2006@gmail.com","github":"https://github.com/bishshi","avatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"comments":{"envId":"https://comment.biss.click","region":"","script":"https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"},"analytics":{"umami":{"script":"https://umami.biss.click/script.js","websiteId":"3d97b310-b241-4fc6-bc9c-68e317fc7d42","statsEndpoint":"https://blogumami.biss.click"}},"talks":{"apiUrl":"https://mm.biss.click/api/echo/page","pageSize":30,"homeLimit":6,"cacheKey":"talksCache","cacheTimeKey":"talksCacheTime","cacheDurationMs":1800000,"fallbackAvatar":"https://free.picui.cn/free/2025/08/10/689845496a283.png"},"search":{"apiKey":"2uxQzk7eD2GBCljdSmIiuU3ause7UK9n","server":{"host":"typesense.biss.click","port":"443","protocol":"https"},"indexName":"blogs","searchParams":{"query_by":"title,content","highlight_full_fields":"title,content","per_page":8,"num_typos":1,"typo_tokens_threshold":1,"prefix":true},"ui":{"maxRetries":10,"retryDelay":100,"animationDuration":300,"maxInitRetries":30,"placeholder":"输入关键词寻找故事..."}},"externalLinks":{"waitSeconds":10,"whitelist":["blog.biss.click","biss.click","github.com","localhost","127.0.0.1"]}};</script><script>
|
||
(() => {
|
||
const storageKey = 'site-theme';
|
||
const savedTheme = localStorage.getItem(storageKey);
|
||
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||
document.documentElement.dataset.theme = savedTheme || systemTheme;
|
||
})();
|
||
</script><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/instantsearch.css/themes/reset-min.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css"><link rel="stylesheet" href="https://cdn.jsdmirror.com/npm/@fortawesome/fontawesome-free@6.7.2/css/all.min.css"><title>添加typesense搜索 | Bi's Blog</title><link rel="stylesheet" href="/_astro/BaseLayout.D7kFxpxP.css"></head> <body> <header class="site-header"> <nav class="nav"> <a class="brand" href="/">Bi's Blog</a> <button class="nav-menu-toggle" type="button" aria-label="打开菜单" aria-expanded="false" data-nav-toggle> <i class="fa-solid fa-bars nav-menu-open" aria-hidden="true"></i> <i class="fa-solid fa-xmark nav-menu-close" aria-hidden="true"></i> </button> <div class="nav-menu" data-nav-menu> <div class="nav-links nav-center"> <a href="/archives/"><i class="fa-solid fa-box-archive nav-icon" aria-hidden="true"></i><span>归档</span></a><a href="/categories/"><i class="fa-solid fa-folder-open nav-icon" aria-hidden="true"></i><span>分类</span></a><a href="/tags/"><i class="fa-solid fa-tags nav-icon" aria-hidden="true"></i><span>标签</span></a><a href="/shuoshuo/"><i class="fa-regular fa-comments nav-icon" aria-hidden="true"></i><span>说说</span></a><a href="/about/"><i class="fa-solid fa-heart nav-icon" aria-hidden="true"></i><span>关于</span></a><a href="/link/"><i class="fa-solid fa-link nav-icon" aria-hidden="true"></i><span>友链</span></a> </div> <div class="nav-links nav-actions"> <button class="nav-random" type="button" onclick="randomPost()" aria-label="前往随机文章" title="前往随机文章"> <i class="fa-solid fa-shuffle nav-icon" aria-hidden="true"></i> <span>随机文章</span> </button> <button class="nav-search search-typesense-trigger" type="button" aria-label="搜索" title="搜索"> <i class="fa-solid fa-magnifying-glass nav-icon" aria-hidden="true"></i> <span>搜索</span> </button> <button class="theme-toggle" type="button" aria-label="切换夜间模式" title="切换夜间模式" data-theme-toggle> <i class="fa-solid fa-sun theme-toggle-sun nav-icon" aria-hidden="true"></i> <i class="fa-solid fa-moon theme-toggle-moon nav-icon" aria-hidden="true"></i> </button> </div> </div> </nav> </header> <main> <section class="post-layout"> <article class="article post-article"> <header class="article-header"> <div class="article-header-content"> <time datetime="2026-02-05T13:14:16.000Z">2026/02/05</time> <h1>添加typesense搜索</h1> <div class="meta-list"> <a href="/categories/tech/">技术</a><a href="/categories/site-notes/">建站手札</a> </div> </div> </header> <section class="article-summary-card" aria-label="AI 文章摘要"> <div class="article-summary-dots" aria-hidden="true"> <span></span> <span></span> <span></span> </div> <p class="article-summary-content"> <span class="article-summary-text" data-summary-text="这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。">这篇文章介绍了如何在博客中集成Typesense搜索引擎,包括安装Typesense、添加数据集以及同步数据的详细步骤。首先,通过命令行安装Typesense并配置相关参数,如API密钥和数据目录。接着,安装必要的npm包,并在Hexo博客中配置搜索功能,指定存储搜索数据的JSON文件路径。然后,编写了一个同步脚本,用于读取XML文件、解析数据、检查或创建集合以及导入数据到Typesense。最后,在博客底部添加了InstantSearch和Typesense的JavaScript库,并根据实际情况修改了配置。</span> <span class="article-summary-caret" aria-hidden="true"></span> </p> <div class="article-summary-footer"> <span class="article-summary-brand"> <span aria-hidden="true">✣</span>
|
||
AI摘要
|
||
</span> <span class="article-summary-model">QWEN</span> </div> </section> <div class="prose" data-toc="[{"depth":1,"text":"安装typesense","id":"安装typesense"},{"depth":1,"text":"添加数据集","id":"添加数据集"},{"depth":1,"text":"创建只读key","id":"创建只读key"},{"depth":1,"text":"博客添加搜索","id":"博客添加搜索"},{"depth":1,"text":"注意","id":"注意"}]"> <p>最近在构建班级博客,用<code>ghost cms</code>,在构建搜索时发现了typesense,所以把他移植到这个博客上。</p>
|
||
<h1 id="安装typesense">安装typesense</h1>
|
||
<p>直接用<code>docker-compose</code>:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="yaml"><code><span class="line"></span>
|
||
<span class="line"><span style="color:#85E89D">services</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#85E89D"> typesense</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#85E89D"> image</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">typesense/typesense:30.1</span></span>
|
||
<span class="line"><span style="color:#85E89D"> restart</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">always</span></span>
|
||
<span class="line"><span style="color:#85E89D"> ports</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">"8108:8108"</span></span>
|
||
<span class="line"><span style="color:#85E89D"> volumes</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF">./typesense-data:/data</span></span>
|
||
<span class="line"><span style="color:#85E89D"> command</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'--data-dir /data --api-key=填写key --enable-cors'</span></span>
|
||
<span class="line"></span></code></pre>
|
||
<p>然后就是反向代理之类的,不过多写了。</p>
|
||
<h1 id="添加数据集">添加数据集</h1>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#6A737D"># 先安装库</span></span>
|
||
<span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> hexo-generator-search</span></span>
|
||
<span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> typesense</span><span style="color:#9ECBFF"> xml2js</span></span></code></pre>
|
||
<p>然后在<code>config.yml</code>配置(就是把文章生成json):</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="yml"><code><span class="line"><span style="color:#85E89D">search</span><span style="color:#E1E4E8">:</span></span>
|
||
<span class="line"><span style="color:#85E89D"> path</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">search.json</span></span>
|
||
<span class="line"><span style="color:#85E89D"> field</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">post</span></span>
|
||
<span class="line"><span style="color:#85E89D"> content</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">true</span></span></code></pre>
|
||
<p>创建一个数据同步脚本<code>sync_typesense.js</code>:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> Typesense</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'typesense'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> fs</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'fs'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> xml2js</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'xml2js'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D">// --- 配置区域 ---</span></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> CONFIG</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> apiKey: </span><span style="color:#9ECBFF">'你的Admin-API-Key'</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 必须是 Admin Key</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> host: </span><span style="color:#9ECBFF">'你的Typesense主机地址'</span><span style="color:#E1E4E8">, </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> port: </span><span style="color:#79B8FF">443</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> protocol: </span><span style="color:#9ECBFF">'https'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> collectionName: </span><span style="color:#9ECBFF">'blogs'</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">};</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> client</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> Typesense.</span><span style="color:#B392F0">Client</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'nodes'</span><span style="color:#E1E4E8">: [{ </span><span style="color:#9ECBFF">'host'</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.host, </span><span style="color:#9ECBFF">'port'</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.port, </span><span style="color:#9ECBFF">'protocol'</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.protocol }],</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'apiKey'</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.apiKey,</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'connectionTimeoutSeconds'</span><span style="color:#E1E4E8">: </span><span style="color:#79B8FF">5</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">});</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">async</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> sync</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> try</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 1. 读取并解析 XML</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> xml</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> fs.</span><span style="color:#B392F0">readFileSync</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'./public/search.xml'</span><span style="color:#E1E4E8">, </span><span style="color:#9ECBFF">'utf8'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> parser</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#E1E4E8"> xml2js.</span><span style="color:#B392F0">Parser</span><span style="color:#E1E4E8">({ explicitArray: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> result</span><span style="color:#F97583"> =</span><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> parser.</span><span style="color:#B392F0">parseStringPromise</span><span style="color:#E1E4E8">(xml);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 提取文章列表 (处理单篇文章和多篇文章的情况)</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> entries </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> result.search.entry;</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">Array.</span><span style="color:#B392F0">isArray</span><span style="color:#E1E4E8">(entries)) entries </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> [entries];</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // 格式化数据以适配 Typesense</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> documents</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> entries.</span><span style="color:#B392F0">map</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">post</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> ({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> title: post.title,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> url: post.url,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> content: post.content,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> categories: post.categories </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> (Array.</span><span style="color:#B392F0">isArray</span><span style="color:#E1E4E8">(post.categories.category) </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> post.categories.category </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> [post.categories.category]) </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> [],</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> tags: post.tags </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> (Array.</span><span style="color:#B392F0">isArray</span><span style="color:#E1E4E8">(post.tags.tag) </span><span style="color:#F97583">?</span><span style="color:#E1E4E8"> post.tags.tag </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> [post.tags.tag]) </span><span style="color:#F97583">:</span><span style="color:#E1E4E8"> [],</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }));</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // 2. 检查或创建 Collection (Schema)</span></span>
|
||
<span class="line"><span style="color:#F97583"> try</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> client.</span><span style="color:#B392F0">collections</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.collectionName).</span><span style="color:#B392F0">retrieve</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (err) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> schema</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> name: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.collectionName,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> fields: [</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> { name: </span><span style="color:#9ECBFF">'title'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'string'</span><span style="color:#E1E4E8">, locale: </span><span style="color:#9ECBFF">'zh'</span><span style="color:#E1E4E8">},</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> { name: </span><span style="color:#9ECBFF">'content'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'string'</span><span style="color:#E1E4E8">, locale: </span><span style="color:#9ECBFF">'zh'</span><span style="color:#E1E4E8">},</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> { name: </span><span style="color:#9ECBFF">'url'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'string'</span><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> { name: </span><span style="color:#9ECBFF">'categories'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'string[]'</span><span style="color:#E1E4E8">, facet: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> { name: </span><span style="color:#9ECBFF">'tags'</span><span style="color:#E1E4E8">, type: </span><span style="color:#9ECBFF">'string[]'</span><span style="color:#E1E4E8">, facet: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> ]</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> };</span></span>
|
||
<span class="line"><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> client.</span><span style="color:#B392F0">collections</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">create</span><span style="color:#E1E4E8">(schema);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'Collection created!'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // 3. 导入数据 (使用 upsert 模式:存在则更新,不存在则创建)</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">`Syncing ${</span><span style="color:#E1E4E8">documents</span><span style="color:#9ECBFF">.</span><span style="color:#79B8FF">length</span><span style="color:#9ECBFF">} posts to Typesense...`</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> await</span><span style="color:#E1E4E8"> client.</span><span style="color:#B392F0">collections</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.collectionName).</span><span style="color:#B392F0">documents</span><span style="color:#E1E4E8">().</span><span style="color:#B392F0">import</span><span style="color:#E1E4E8">(documents, { action: </span><span style="color:#9ECBFF">'upsert'</span><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'Sync complete!'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (error) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'Sync failed:'</span><span style="color:#E1E4E8">, error);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">}</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#B392F0">sync</span><span style="color:#E1E4E8">();</span></span></code></pre>
|
||
<p>然后运行<code>node sync_typesense.js</code></p>
|
||
<h1 id="创建只读key">创建只读key</h1>
|
||
<p>把下面代码存储成js,node运行就行。</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> http</span><span style="color:#F97583"> =</span><span style="color:#B392F0"> require</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'https'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> data</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> JSON</span><span style="color:#E1E4E8">.</span><span style="color:#B392F0">stringify</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> "description"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"Public search only key"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> "actions"</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"documents:search"</span><span style="color:#E1E4E8">],</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> "collections"</span><span style="color:#E1E4E8">: [</span><span style="color:#9ECBFF">"blogs"</span><span style="color:#E1E4E8">]</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">});</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> hostname: </span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// 不要带 https://</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> port: </span><span style="color:#79B8FF">443</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> path: </span><span style="color:#9ECBFF">'/keys'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> method: </span><span style="color:#9ECBFF">'POST'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> headers: {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'X-TYPESENSE-API-KEY'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'你的admin key'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'Content-Type'</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">'application/json'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'Content-Length'</span><span style="color:#E1E4E8">: data.</span><span style="color:#79B8FF">length</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">};</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583">const</span><span style="color:#79B8FF"> req</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> http.</span><span style="color:#B392F0">request</span><span style="color:#E1E4E8">(options, </span><span style="color:#FFAB70">res</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> res.</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'data'</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">d</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> { process.stdout.</span><span style="color:#B392F0">write</span><span style="color:#E1E4E8">(d); });</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">});</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8">req.</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'error'</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">error</span><span style="color:#F97583"> =></span><span style="color:#E1E4E8"> { console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(error); });</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">req.</span><span style="color:#B392F0">write</span><span style="color:#E1E4E8">(data);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8">req.</span><span style="color:#B392F0">end</span><span style="color:#E1E4E8">();</span></span></code></pre>
|
||
<h1 id="博客添加搜索">博客添加搜索</h1>
|
||
<p>在<code>config.yaml</code>inject bottom添加:</p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="yaml"><code><span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF"><script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script></span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> - </span><span style="color:#9ECBFF"><script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script></span></span></code></pre>
|
||
<p>为了方便,我直接修改了<code>\themes\butterfly\source\js\search\local_search.js</code></p>
|
||
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;" tabindex="0" data-language="js"><code><span class="line"><span style="color:#E1E4E8">(</span><span style="color:#F97583">function</span><span style="color:#E1E4E8"> () {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> 'use strict'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 配置区域 - 请根据实际情况修改</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> CONFIG</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> apiKey: </span><span style="color:#9ECBFF">""</span><span style="color:#E1E4E8">, </span><span style="color:#6A737D">// ⚠️ 建议使用 Search-Only API Key</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> server: {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> host: </span><span style="color:#9ECBFF">"host"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> port: </span><span style="color:#9ECBFF">"443"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> protocol: </span><span style="color:#9ECBFF">"https"</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> indexName: </span><span style="color:#9ECBFF">"blogs"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchParams: {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> query_by: </span><span style="color:#9ECBFF">"title,content"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> highlight_full_fields: </span><span style="color:#9ECBFF">"title,content"</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> per_page: </span><span style="color:#79B8FF">8</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> num_typos: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> typo_tokens_threshold: </span><span style="color:#79B8FF">1</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> prefix: </span><span style="color:#79B8FF">true</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> ui: {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> maxRetries: </span><span style="color:#79B8FF">10</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> retryDelay: </span><span style="color:#79B8FF">100</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> animationDuration: </span><span style="color:#79B8FF">300</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> };</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 状态管理</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> searchInstance </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> null</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> isInitialized </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> false</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> isSearchOpen </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> false</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> let</span><span style="color:#E1E4E8"> initRetryCount </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> MAX_INIT_RETRIES</span><span style="color:#F97583"> =</span><span style="color:#79B8FF"> 30</span><span style="color:#E1E4E8">; </span><span style="color:#6A737D">// 最多重试30次 (3秒)</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 错误提示函数</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> showErrorMessage</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> hitsContainer</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'hits'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">hitsContainer) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> hitsContainer.innerHTML </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div class="ts-empty">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<p>依赖库未能正确加载,请检查以下配置:</p>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<ol style="text-align: left; max-width: 500px; margin: 15px auto;">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<li>检查 JS 文件加载顺序(先 instantsearch.js,再 adapter)</li>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<li>尝试更换 CDN 或使用本地文件</li>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<li>打开浏览器控制台查看详细错误信息</li>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</ol>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div style="margin-top: 20px;">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</div>'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 1. 动态插入 HTML 结构</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> searchHTML</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> `</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="typesense-search-mask" class="ts-mask" style="display:none;"></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="typesense-search-container" class="ts-container"></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div class="ts-header"></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <span class="ts-title"></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <i class="fas fa-search"></i> 本站搜索</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </span></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <span id="close-typesense" class="ts-close" aria-label="关闭搜索">&times;</span></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="searchbox"></div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="stats" class="ts-stats"></div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="hits" class="ts-hits"></div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div id="pagination" class="ts-pagination"></div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <div class="ts-footer"></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <small>Search powered by <strong>Typesense</strong></small></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </div></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </div></span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#9ECBFF"> <style></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-mask {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> position: fixed;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> top: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> left: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> width: 100%;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> height: 100%;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: rgba(0, 0, 0, 0.7);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> z-index: 10000;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> backdrop-filter: blur(5px);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> -webkit-backdrop-filter: blur(5px);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: opacity 300ms ease;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-mask.active { opacity: 1; }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-container {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin: 5% auto;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> width: 90%;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> max-width: 650px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--search-bg, var(--card-bg, #fff));</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 25px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 12px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> position: relative;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> z-index: 10001;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transform: translateY(-50px);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: all 300ms ease;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-mask.active .ts-container {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transform: translateY(0);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 1;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-header {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: flex;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> justify-content: space-between;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> align-items: center;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-bottom: 20px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-bottom: 2px solid var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding-bottom: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-title {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 1.2rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-weight: bold;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-close {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> cursor: pointer;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 28px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #333);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> line-height: 1;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: color 0.2s, transform 0.2s;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-close:hover {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transform: scale(1.1);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-SearchBox-input {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> position: relative;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> z-index: 10002;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> cursor: text;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 12px 40px 12px 15px !important;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 8px !important;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border: 2px solid #eee !important;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> width: 100%;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> outline: none;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: border-color 0.3s, box-shadow 0.3s;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--card-bg, #fff);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #333);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 1rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-SearchBox-input:focus {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-color: var(--text-highlight-color, #49b1f5) !important;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-stats {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin: 10px 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 0.85rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #666);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 0.8;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-hits {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> max-height: 55vh;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> overflow-y: auto;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-top: 15px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding-right: 5px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-hits::-webkit-scrollbar { width: 6px; }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-hits::-webkit-scrollbar-track {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--card-bg, #f1f1f1);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-hits::-webkit-scrollbar-thumb {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-empty {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> text-align: center;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 40px 20px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #999);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-empty i {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 3rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-bottom: 15px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 0.3;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-empty code {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: #f5f5f5;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 2px 6px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 3px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-family: monospace;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: #e91e63;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-empty ol {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding-left: 20px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-empty li {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin: 8px 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-result-item {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 8px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: all 0.2s ease;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-bottom: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 15px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border: 1px solid transparent;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> text-decoration: none;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: block;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--card-bg, #fff);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-result-item:hover {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-color: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transform: translateX(5px);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-result-title {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-weight: bold;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 1.1rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-bottom: 8px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: block;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> line-height: 1.4;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-result-content {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-size: 0.9rem;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #666);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> line-height: 1.6;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> opacity: 0.85;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: -webkit-box;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> -webkit-line-clamp: 2;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> -webkit-box-orient: vertical;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> overflow: hidden;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-result-item mark {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: #ffeb3b;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: #000;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 2px 4px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 3px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> font-weight: 500;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-pagination {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-top: 20px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: flex;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> justify-content: center;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> gap: 5px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-Pagination-list {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: flex;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> list-style: none;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin: 0;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> gap: 5px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-Pagination-link {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> display: block;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 8px 12px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border: 1px solid #ddd;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-radius: 5px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: var(--font-color, #333);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> text-decoration: none;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> transition: all 0.2s;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--card-bg, #fff);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-Pagination-link:hover {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: #fff;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ais-Pagination-item--selected .ais-Pagination-link {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: var(--text-highlight-color, #49b1f5);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> color: #fff;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-footer {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> text-align: right;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin-top: 15px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> border-top: 1px solid var(--border-color, #eee);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding-top: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> @media (max-width: 768px) {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-container {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> margin: 10px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> width: calc(100% - 20px);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> padding: 20px 15px;</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .ts-hits { max-height: 50vh; }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> [data-theme="dark"] .ts-mask,</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> .dark-mode .ts-mask {</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> background: rgba(0, 0, 0, 0.85);</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> }</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> </style></span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> `</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> document.body.</span><span style="color:#B392F0">insertAdjacentHTML</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'beforeend'</span><span style="color:#E1E4E8">, searchHTML);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> mask</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'typesense-search-mask'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> closeBtn</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'close-typesense'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> container</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">getElementById</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'typesense-search-container'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 搜索控制</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> openSearch</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (isSearchOpen) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> isSearchOpen </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> mask.style.display </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> 'block'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> void</span><span style="color:#E1E4E8"> mask.offsetWidth;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> mask.classList.</span><span style="color:#B392F0">add</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'active'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> document.body.style.overflow </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> 'hidden'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">isInitialized) {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> initTypesense</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#B392F0"> focusSearchInput</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> closeSearch</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">isSearchOpen) </span><span style="color:#F97583">return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> isSearchOpen </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> false</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> mask.classList.</span><span style="color:#B392F0">remove</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'active'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> mask.style.display </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> 'none'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> document.body.style.overflow </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }, </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.ui.animationDuration);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> focusSearchInput</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">retryCount</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> retryCount </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> retryCount </span><span style="color:#F97583">||</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> input</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ais-SearchBox-input'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (input) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> input.</span><span style="color:#B392F0">focus</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> input.</span><span style="color:#B392F0">select</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (retryCount </span><span style="color:#F97583"><</span><span style="color:#79B8FF"> CONFIG</span><span style="color:#E1E4E8">.ui.maxRetries) {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(</span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> focusSearchInput</span><span style="color:#E1E4E8">(retryCount </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }, </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.ui.retryDelay);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 事件监听</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'click'</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (e.target.</span><span style="color:#B392F0">closest</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.search-typesense-trigger'</span><span style="color:#E1E4E8">)) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> e.</span><span style="color:#B392F0">preventDefault</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#B392F0"> openSearch</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> closeBtn.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'click'</span><span style="color:#E1E4E8">, closeSearch);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> mask.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'click'</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (e.target </span><span style="color:#F97583">===</span><span style="color:#E1E4E8"> mask) </span><span style="color:#B392F0">closeSearch</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> container.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'click'</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> e.</span><span style="color:#B392F0">stopPropagation</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> window.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'keydown'</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">e</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (e.key </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'Escape'</span><span style="color:#F97583"> &&</span><span style="color:#E1E4E8"> isSearchOpen) {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> closeSearch</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> ((e.ctrlKey </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> e.metaKey) </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> e.key </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'k'</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> e.</span><span style="color:#B392F0">preventDefault</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#B392F0"> openSearch</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // Typesense 初始化(带重试限制)</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> initTypesense</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (isInitialized </span><span style="color:#F97583">||</span><span style="color:#E1E4E8"> searchInstance) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">warn</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'Typesense 搜索已初始化'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> instantsearchLoaded </span><span style="color:#F97583">=</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> instantsearch </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> 'undefined'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> adapterLoaded </span><span style="color:#F97583">=</span><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> TypesenseInstantSearchAdapter </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> 'undefined'</span><span style="color:#F97583"> ||</span><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#F97583"> typeof</span><span style="color:#E1E4E8"> window.TypesenseInstantSearchAdapter </span><span style="color:#F97583">!==</span><span style="color:#9ECBFF"> 'undefined'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'📦 依赖库检查 ('</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> (initRetryCount </span><span style="color:#F97583">+</span><span style="color:#79B8FF"> 1</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '/'</span><span style="color:#F97583"> +</span><span style="color:#79B8FF"> MAX_INIT_RETRIES</span><span style="color:#F97583"> +</span><span style="color:#9ECBFF"> '):'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' instantsearch.js:'</span><span style="color:#E1E4E8">, instantsearchLoaded </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> '✅ 已加载'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> '❌ 未加载'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' TypesenseAdapter:'</span><span style="color:#E1E4E8">, adapterLoaded </span><span style="color:#F97583">?</span><span style="color:#9ECBFF"> '✅ 已加载'</span><span style="color:#F97583"> :</span><span style="color:#9ECBFF"> '❌ 未加载'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">instantsearchLoaded </span><span style="color:#F97583">||</span><span style="color:#F97583"> !</span><span style="color:#E1E4E8">adapterLoaded) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> initRetryCount</span><span style="color:#F97583">++</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (initRetryCount </span><span style="color:#F97583">>=</span><span style="color:#79B8FF"> MAX_INIT_RETRIES</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'❌ Typesense 依赖库加载失败!'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'🔧 请检查 _config.butterfly.yml 配置:'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'inject:'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' bottom: # ⚠️ 使用 bottom 而不是 head'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' - <script src="/js/typesense-search-fixed.js"></script>'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'💡 或在控制台手动检查:'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' typeof instantsearch'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">' typeof TypesenseInstantSearchAdapter'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#B392F0"> showErrorMessage</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">warn</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'⏳ 100ms 后重试...'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#B392F0"> setTimeout</span><span style="color:#E1E4E8">(initTypesense, </span><span style="color:#79B8FF">100</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> initRetryCount </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> 0</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'🚀 开始初始化 Typesense...'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> try</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> typesenseAdapter</span><span style="color:#F97583"> =</span><span style="color:#F97583"> new</span><span style="color:#B392F0"> TypesenseInstantSearchAdapter</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> server: {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> apiKey: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.apiKey,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> nodes: [{</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> host: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.server.host,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> port: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.server.port,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> protocol: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.server.protocol</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }],</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> cacheSearchResultsForSeconds: </span><span style="color:#79B8FF">120</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> additionalSearchParameters: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.searchParams</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchInstance </span><span style="color:#F97583">=</span><span style="color:#B392F0"> instantsearch</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchClient: typesenseAdapter.searchClient,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> indexName: </span><span style="color:#79B8FF">CONFIG</span><span style="color:#E1E4E8">.indexName,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> routing: </span><span style="color:#79B8FF">false</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchInstance.</span><span style="color:#B392F0">addWidgets</span><span style="color:#E1E4E8">([</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> instantsearch.widgets.</span><span style="color:#B392F0">searchBox</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> container: </span><span style="color:#9ECBFF">'#searchbox'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> placeholder: </span><span style="color:#9ECBFF">'输入关键词寻找故事...'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> autofocus: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> showReset: </span><span style="color:#79B8FF">true</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> showSubmit: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> showLoadingIndicator: </span><span style="color:#79B8FF">true</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }),</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> instantsearch.widgets.</span><span style="color:#B392F0">stats</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> container: </span><span style="color:#9ECBFF">'#stats'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> templates: {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> text</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">data</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (</span><span style="color:#F97583">!</span><span style="color:#E1E4E8">data.query) </span><span style="color:#F97583">return</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#9ECBFF"> '找到 <strong>'</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> data.nbHits </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</strong> 条结果 ('</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> data.processingTimeMS </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> 'ms)'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }),</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> instantsearch.widgets.</span><span style="color:#B392F0">hits</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> container: </span><span style="color:#9ECBFF">'#hits'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> templates: {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> empty</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">results</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#9ECBFF"> '<div class="ts-empty">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div><i class="fas fa-search"></i></div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div>找不到与 "<strong>'</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> results.query </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</strong>" 相关的内容</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</div>'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> },</span></span>
|
||
<span class="line"><span style="color:#B392F0"> item</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">hit</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 使用 _highlightResult 获取高亮文本</span></span>
|
||
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> titleHighlight </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> hit._highlightResult </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> hit._highlightResult.title </span></span>
|
||
<span class="line"><span style="color:#F97583"> ?</span><span style="color:#E1E4E8"> hit._highlightResult.title.value </span></span>
|
||
<span class="line"><span style="color:#F97583"> :</span><span style="color:#E1E4E8"> (hit.title </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#F97583"> var</span><span style="color:#E1E4E8"> contentHighlight </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> hit._highlightResult </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> hit._highlightResult.content </span></span>
|
||
<span class="line"><span style="color:#F97583"> ?</span><span style="color:#E1E4E8"> hit._highlightResult.content.value </span></span>
|
||
<span class="line"><span style="color:#F97583"> :</span><span style="color:#E1E4E8"> (hit.content </span><span style="color:#F97583">||</span><span style="color:#9ECBFF"> ''</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 截取内容长度</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (contentHighlight.</span><span style="color:#79B8FF">length</span><span style="color:#F97583"> ></span><span style="color:#79B8FF"> 200</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> contentHighlight </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> contentHighlight.</span><span style="color:#B392F0">substring</span><span style="color:#E1E4E8">(</span><span style="color:#79B8FF">0</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">200</span><span style="color:#E1E4E8">) </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '...'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#F97583"> return</span><span style="color:#9ECBFF"> '<a href="'</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> hit.url </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '" class="ts-result-item">'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div class="ts-result-title">'</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> titleHighlight </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '<div class="ts-result-content">'</span><span style="color:#F97583"> +</span><span style="color:#E1E4E8"> contentHighlight </span><span style="color:#F97583">+</span><span style="color:#9ECBFF"> '</div>'</span><span style="color:#F97583"> +</span></span>
|
||
<span class="line"><span style="color:#9ECBFF"> '</a>'</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }),</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> instantsearch.widgets.</span><span style="color:#B392F0">pagination</span><span style="color:#E1E4E8">({</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> container: </span><span style="color:#9ECBFF">'#pagination'</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> padding: </span><span style="color:#79B8FF">2</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> showFirst: </span><span style="color:#79B8FF">false</span><span style="color:#E1E4E8">,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> showLast: </span><span style="color:#79B8FF">false</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> })</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> ]);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchInstance.</span><span style="color:#B392F0">start</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> isInitialized </span><span style="color:#F97583">=</span><span style="color:#79B8FF"> true</span><span style="color:#E1E4E8">;</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> </span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'✅ Typesense 初始化成功!'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> searchInstance.</span><span style="color:#B392F0">on</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'render'</span><span style="color:#E1E4E8">, </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (isSearchOpen) {</span></span>
|
||
<span class="line"><span style="color:#F97583"> const</span><span style="color:#79B8FF"> input</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">querySelector</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'.ais-SearchBox-input'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (input </span><span style="color:#F97583">&&</span><span style="color:#E1E4E8"> document.activeElement </span><span style="color:#F97583">!==</span><span style="color:#E1E4E8"> input) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> input.</span><span style="color:#B392F0">focus</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> });</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">catch</span><span style="color:#E1E4E8"> (error) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">error</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'❌ 初始化失败:'</span><span style="color:#E1E4E8">, error);</span></span>
|
||
<span class="line"><span style="color:#B392F0"> showErrorMessage</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 初始化</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#F97583"> function</span><span style="color:#B392F0"> init</span><span style="color:#E1E4E8">() {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'🔍 Typesense 搜索已准备就绪'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'💡 快捷键: Ctrl/Cmd + K'</span><span style="color:#E1E4E8">);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#F97583"> if</span><span style="color:#E1E4E8"> (document.readyState </span><span style="color:#F97583">===</span><span style="color:#9ECBFF"> 'loading'</span><span style="color:#E1E4E8">) {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> document.</span><span style="color:#B392F0">addEventListener</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'DOMContentLoaded'</span><span style="color:#E1E4E8">, init);</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> } </span><span style="color:#F97583">else</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#B392F0"> init</span><span style="color:#E1E4E8">();</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> }</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // 全局接口</span></span>
|
||
<span class="line"><span style="color:#6A737D"> // ============================================================================</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> window.TypesenseSearch </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> {</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> open: openSearch,</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> close: closeSearch,</span></span>
|
||
<span class="line"><span style="color:#B392F0"> isOpen</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> isSearchOpen; },</span></span>
|
||
<span class="line"><span style="color:#B392F0"> getInstance</span><span style="color:#E1E4E8">: </span><span style="color:#F97583">function</span><span style="color:#E1E4E8">() { </span><span style="color:#F97583">return</span><span style="color:#E1E4E8"> searchInstance; }</span></span>
|
||
<span class="line"><span style="color:#E1E4E8"> };</span></span>
|
||
<span class="line"></span>
|
||
<span class="line"><span style="color:#E1E4E8">})();</span></span></code></pre>
|
||
<h1 id="注意">注意</h1>
|
||
<div class="butterfly-note butterfly-note--red butterfly-note--simple"><i class="fa-solid fa-circle-info" aria-hidden="true"></i><div class="butterfly-note__content"><p>indexName: “blogs” 和 collectionName: ‘posts’ 要一致!!!</p></div></div> </div> <section class="post-copyright-card" aria-label="文章版权信息"> <div class="post-copyright-mark" aria-hidden="true"> <span>CC</span> </div> <div class="post-copyright-main"> <h2>添加typesense搜索</h2> <a class="post-copyright-link" href="/posts/f287c563/">/posts/f287c563/</a> <dl class="post-copyright-meta"> <div> <dt>作者</dt> <dd>biss</dd> </div> <div> <dt>发布于</dt> <dd><time datetime="2026-02-05T13:14:16.000Z">2026/02/05</time></dd> </div> <div> <dt>许可协议</dt> <dd><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/" target="_blank" rel="noreferrer">CC BY-NC-SA 4.0</a></dd> </div> </dl> </div> <div class="post-copyright-footer"> <div class="post-copyright-actions" aria-label="文章操作"> <button class="post-copyright-action is-primary post-share-button" type="button" data-share-title="添加typesense搜索" data-share-url="https://blog.biss.click/posts/f287c563/" title="分享文章"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"></path> <path d="M12 16V4"></path> <path d="m7 9 5-5 5 5"></path> </svg> <span>分享</span> </button> <a class="post-copyright-action" href="/rss.xml" title="订阅 RSS"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M5 5a14 14 0 0 1 14 14"></path> <path d="M5 11a8 8 0 0 1 8 8"></path> <path d="M6 18h.01"></path> </svg> <span>订阅</span> </a> <a class="post-copyright-action" href="/about/" title="打赏作者"> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M7 8h10"></path> <path d="M8 8V5h8v3"></path> <path d="M6 8h12l-1.5 11h-9L6 8Z"></path> <path d="M10 12h4"></path> </svg> <span>打赏</span> </a> <a class="post-copyright-icon" href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fblog.biss.click%2Fposts%2Ff287c563%2F&text=%E6%B7%BB%E5%8A%A0typesense%E6%90%9C%E7%B4%A2" target="_blank" rel="noreferrer" title="分享到 X"> <span aria-hidden="true">x</span> </a> <a class="post-copyright-icon" href="https://service.weibo.com/share/share.php?url=https%3A%2F%2Fblog.biss.click%2Fposts%2Ff287c563%2F&title=%E6%B7%BB%E5%8A%A0typesense%E6%90%9C%E7%B4%A2" target="_blank" rel="noreferrer" title="分享到微博"> <span aria-hidden="true">微</span> </a> </div> </div> </section> <script>
|
||
(() => {
|
||
document.querySelectorAll('.post-share-button').forEach((button) => {
|
||
button.addEventListener('click', async () => {
|
||
const title = button.dataset.shareTitle || document.title;
|
||
const url = button.dataset.shareUrl || window.location.href;
|
||
const label = button.querySelector('span');
|
||
const originalText = label?.textContent || '分享';
|
||
|
||
try {
|
||
if (navigator.share) {
|
||
await navigator.share({ title, url });
|
||
return;
|
||
}
|
||
|
||
await navigator.clipboard.writeText(url);
|
||
if (label) label.textContent = '已复制';
|
||
} catch {
|
||
if (label) label.textContent = '复制失败';
|
||
} finally {
|
||
window.setTimeout(() => {
|
||
if (label) label.textContent = originalText;
|
||
}, 1800);
|
||
}
|
||
});
|
||
});
|
||
})();
|
||
</script> <section id="post-comment" class="twikoo-comments" aria-label="Comments" data-env-id="https://comment.biss.click" data-region data-path="/posts/f287c563/" data-script="https://cdn.jsdelivr.net/npm/twikoo@1.7.7/dist/twikoo.min.js"> <div class="twikoo-comments-head"> <span class="twikoo-comments-icon" aria-hidden="true"> <svg viewBox="0 0 24 24"> <path d="M21 12a8 8 0 0 1-8 8H7l-4 3v-5a8 8 0 1 1 18-6Z"></path> </svg> </span> <div> <h2>Comments</h2> <p>添加typesense搜索</p> </div> </div> <div id="twikoo-wrap" class="twikoo-wrap"></div> </section> <script>
|
||
(() => {
|
||
const root = document.getElementById('post-comment');
|
||
const wrap = document.getElementById('twikoo-wrap');
|
||
if (!root || !wrap || root.dataset.twikooLoaded === 'true') return;
|
||
|
||
const envId = root.dataset.envId;
|
||
const path = root.dataset.path || window.location.pathname;
|
||
const region = root.dataset.region || undefined;
|
||
if (!envId) return;
|
||
|
||
root.dataset.twikooLoaded = 'true';
|
||
|
||
const initTwikoo = () => {
|
||
if (!window.twikoo || typeof window.twikoo.init !== 'function') return;
|
||
|
||
window.twikoo.init({
|
||
el: wrap,
|
||
envId,
|
||
region,
|
||
path,
|
||
});
|
||
};
|
||
|
||
if (window.twikoo) {
|
||
initTwikoo();
|
||
return;
|
||
}
|
||
|
||
const script = document.createElement('script');
|
||
script.src = root.dataset.script;
|
||
script.defer = true;
|
||
script.onload = initTwikoo;
|
||
script.onerror = () => {
|
||
root.dataset.twikooLoaded = 'false';
|
||
};
|
||
document.head.appendChild(script);
|
||
})();
|
||
</script> </article> <aside class="article-sidebar" aria-label="文章侧边栏"> <section class="sidebar-card article-author-card"> <img class="profile-avatar" src="https://free.picui.cn/free/2025/08/10/689845496a283.png" alt="biss" loading="lazy"> <div class="profile-name">biss</div> <div class="article-author-meta"> <span>2026/02/05</span> <a href="/categories/tech/">技术</a> </div> </section> <div class="article-sticky-sidebar"> <section class="sidebar-card toc-card"> <h2> <span aria-hidden="true">☰</span>
|
||
文章目录
|
||
<em>5</em> </h2> <nav class="toc-list toc-tree" aria-label="文章目录" data-toc-tree> <div class="toc-group is-open" data-toc-group data-heading-id="安装typesense"> <a class="toc-link toc-link-main" href="#安装typesense" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>安装typesense</span> </a> </div><div class="toc-group" data-toc-group data-heading-id="添加数据集"> <a class="toc-link toc-link-main" href="#添加数据集" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>添加数据集</span> </a> </div><div class="toc-group" data-toc-group data-heading-id="创建只读key"> <a class="toc-link toc-link-main" href="#创建只读key" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>创建只读key</span> </a> </div><div class="toc-group" data-toc-group data-heading-id="博客添加搜索"> <a class="toc-link toc-link-main" href="#博客添加搜索" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>博客添加搜索</span> </a> </div><div class="toc-group" data-toc-group data-heading-id="注意"> <a class="toc-link toc-link-main" href="#注意" data-toc-link data-toc-top> <span class="toc-marker" aria-hidden="true"></span> <span>注意</span> </a> </div> </nav> </section> <section class="sidebar-card same-category-card"> <h2> <span aria-hidden="true">▣</span>
|
||
同类文章
|
||
<em>8</em> </h2> <div class="same-category-list"> <a href="/posts/4c91a8f2/"> <strong>用 Umami 分享链接给网站信息卡提供统计数据</strong> <time datetime="2026-06-19T08:58:00.000Z">2026/06/19</time> </a><a href="/posts/3f8de22/"> <strong>GPT Image2 初试</strong> <time datetime="2026-04-25T15:00:00.000Z">2026/04/25</time> </a><a href="/posts/8712baaa/"> <strong>电子价签玩法</strong> <time datetime="2026-04-19T13:46:25.000Z">2026/04/19</time> </a><a href="/posts/9b2ef00a/"> <strong>OpenGL- Cohen-Sutherland 编码裁剪算法</strong> <time datetime="2026-04-15T19:34:23.000Z">2026/04/16</time> </a><a href="/posts/7207243b/"> <strong>OpenGL-直线的扫描转换</strong> <time datetime="2026-04-11T19:01:02.000Z">2026/04/12</time> </a><a href="/posts/437a5198/"> <strong>OpenGL-基础程序</strong> <time datetime="2026-04-11T18:20:34.000Z">2026/04/12</time> </a><a href="/posts/c4477b0c/"> <strong>配置OpenGL环境</strong> <time datetime="2026-04-02T04:52:16.000Z">2026/04/02</time> </a><a href="/posts/b559997d/"> <strong>opencv应用-算术运算</strong> <time datetime="2026-03-17T13:13:08.000Z">2026/03/17</time> </a> </div> </section> <section class="sidebar-card site-info-card" data-site-info-card data-stats-endpoint="https://blogumami.biss.click"> <h2><i class="fa-solid fa-chart-line" aria-hidden="true"></i> 网站信息</h2> <dl class="site-info-list"> <div> <dt>文章数目</dt> <dd>43</dd> </div><div> <dt>本站总字数</dt> <dd>32,569</dd> </div><div> <dt>本站访客数</dt> <dd data-stat-key="visitors">--</dd> </div><div> <dt>本站访问量</dt> <dd data-stat-key="pageviews">--</dd> </div><div> <dt>最后更新时间</dt> <dd>2026/06/19</dd> </div> </dl> </section> <script>
|
||
(() => {
|
||
const cacheDuration = 10 * 60 * 1000;
|
||
const formatter = new Intl.NumberFormat('en-US');
|
||
const cards = [...document.querySelectorAll('[data-site-info-card]')];
|
||
const endpoint = cards.find((card) => card.dataset.statsEndpoint)?.dataset.statsEndpoint;
|
||
|
||
if (!endpoint) return;
|
||
|
||
const cacheKey = `site-info-stats:${endpoint}`;
|
||
|
||
const updateCards = (stats) => {
|
||
const visitors = Number(stats?.visitors);
|
||
const pageviews = Number(stats?.pageviews);
|
||
|
||
cards.forEach((card) => {
|
||
const visitorsEl = card.querySelector('[data-stat-key="visitors"]');
|
||
const pageviewsEl = card.querySelector('[data-stat-key="pageviews"]');
|
||
if (visitorsEl && Number.isFinite(visitors)) visitorsEl.textContent = formatter.format(visitors);
|
||
if (pageviewsEl && Number.isFinite(pageviews)) pageviewsEl.textContent = formatter.format(pageviews);
|
||
});
|
||
};
|
||
|
||
try {
|
||
const cached = JSON.parse(localStorage.getItem(cacheKey) || 'null');
|
||
if (cached?.time && Date.now() - cached.time < cacheDuration) {
|
||
updateCards(cached.data);
|
||
return;
|
||
}
|
||
} catch {
|
||
localStorage.removeItem(cacheKey);
|
||
}
|
||
|
||
fetch(endpoint, { headers: { Accept: 'application/json' } })
|
||
.then((response) => {
|
||
if (!response.ok) throw new Error(`Stats request failed: ${response.status}`);
|
||
return response.json();
|
||
})
|
||
.then((data) => {
|
||
localStorage.setItem(cacheKey, JSON.stringify({ time: Date.now(), data }));
|
||
updateCards(data);
|
||
})
|
||
.catch(() => {
|
||
});
|
||
})();
|
||
</script> </div> </aside> </section> </main> <footer class="site-footer" aria-label="站点页脚"> <div class="footer-inner"> <section class="footer-top" aria-label="站点信息"> <a class="footer-brand" href="/" aria-label="Bi's Blog 首页"> <img src="/images/Bi.ico" alt="" loading="lazy"> <span>Bi's Blog</span> </a> <p class="footer-slogan">好奇心转化为代码的空间。</p> </section> <section class="footer-contact" aria-label="联系方式"> <p>联系方式</p> <div class="footer-socials"> <a href="mailto:bishsh2006@gmail.com" aria-label="发送邮件" title="发送邮件"> <i class="fa-solid fa-envelope" aria-hidden="true"></i> </a><a href="/rss.xml" aria-label="订阅 RSS" title="订阅 RSS"> <i class="fa-solid fa-rss" aria-hidden="true"></i> </a><a href="https://github.com/bishshi" target="_blank" rel="noreferrer" aria-label="GitHub" title="GitHub"> <i class="fa-brands fa-github" aria-hidden="true"></i> </a> </div> <nav class="footer-contact-links" aria-label="页脚链接"> <a href="/archives/">发展历程</a> <span aria-hidden="true">|</span><a href="/privacy/">隐私政策</a> </nav> </section> <section class="footer-bottom" aria-label="版权信息"> <div> <p>© 2025 - 2026 Bi's Blog</p> <p>blog.biss.click | 内容采用 CC BY-NC-SA 4.0 协议</p> </div> <div class="footer-meta"> <p>本站已运行:<span id="site-runtime">计算中...</span></p> <p> <a href="https://fastly.com/" target="_blank" rel="noreferrer">Fastly</a> <span aria-hidden="true">|</span><a href="https://astro.build/" target="_blank" rel="noreferrer">ASTRO</a> </p> </div> </section> </div> </footer> <aside class="hitokoto-card" aria-label="一言" data-hitokoto-card data-astro-cid-4cgfuthx> <div class="hitokoto-card__head" data-astro-cid-4cgfuthx> <button class="hitokoto-card__toggle" type="button" aria-label="展开一言" aria-expanded="true" title="一言" data-hitokoto-toggle data-astro-cid-4cgfuthx> <i class="fa-solid fa-quote-left" aria-hidden="true" data-astro-cid-4cgfuthx></i> <span data-astro-cid-4cgfuthx>一言</span> </button> <div class="hitokoto-card__actions" data-astro-cid-4cgfuthx> <button class="hitokoto-card__refresh" type="button" aria-label="换一句" title="换一句" data-hitokoto-refresh data-astro-cid-4cgfuthx> <i class="fa-solid fa-rotate" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> <button class="hitokoto-card__collapse" type="button" aria-label="收起一言" title="收起" data-hitokoto-collapse data-astro-cid-4cgfuthx> <i class="fa-solid fa-chevron-down" aria-hidden="true" data-astro-cid-4cgfuthx></i> </button> </div> </div> <a class="hitokoto-card__text" href="https://hitokoto.cn/" target="_blank" rel="noreferrer" data-hitokoto-text data-astro-cid-4cgfuthx>
|
||
获取中...
|
||
</a> <p class="hitokoto-card__from" data-hitokoto-from data-astro-cid-4cgfuthx>来自 Hitokoto</p> </aside> <script type="module">(()=>{const o=document.querySelector("[data-hitokoto-card]"),s=o?.querySelector("[data-hitokoto-toggle]"),r=o?.querySelector("[data-hitokoto-text]"),l=o?.querySelector("[data-hitokoto-from]"),k=o?.querySelector("[data-hitokoto-refresh]"),m=o?.querySelector("[data-hitokoto-collapse]");if(!(o instanceof HTMLElement)||!(s instanceof HTMLButtonElement)||!(r instanceof HTMLAnchorElement)||!(l instanceof HTMLElement))return;const g="https://yiyan.biss.click/api?encode=json",u="hitokoto-card-cache",y=360*60*1e3,d={sentence:"生活明朗,万物可爱。",source:"本地缓存",url:"https://hitokoto.cn/"},L=["/","/index.html"].includes(window.location.pathname);function c(t){o.classList.toggle("is-collapsed",!t),s.setAttribute("aria-expanded",String(t)),s.setAttribute("aria-label",t?"收起一言":"展开一言")}function f(){try{const t=localStorage.getItem(u);if(!t)return null;const e=JSON.parse(t);return!e||typeof e.expiresAt!="number"||e.expiresAt<=Date.now()?null:e.data}catch{return null}}function x(t){try{localStorage.setItem(u,JSON.stringify({data:t,expiresAt:Date.now()+y}))}catch{}}function i(t){r.textContent=t.sentence,r.href=t.url||"https://hitokoto.cn/",l.textContent=t.source?`出自 ${t.source}`:"来自 Hitokoto"}async function h({force:t=!1}={}){if(!t){const e=f();if(e){i(e);return}}o.classList.add("is-loading"),r.textContent="获取中...";try{const e=await fetch(g);if(!e.ok)throw new Error(`Hitokoto request failed: ${e.status}`);const n=await e.json(),a=n.hitokoto||n.content||n.sentence||n.text,S=typeof a=="string"&&a.trim()?a.trim():d.sentence,E=typeof n.from=="string"&&n.from.trim()?n.from.trim():"Hitokoto",w=n.uuid?`https://hitokoto.cn/?uuid=${encodeURIComponent(n.uuid)}`:"https://hitokoto.cn/",p={sentence:S,source:E,url:w};x(p),i(p)}catch(e){console.error(e),i(f()||d)}finally{o.classList.remove("is-loading")}}c(L),s.addEventListener("click",()=>{c(o.classList.contains("is-collapsed"))}),k?.addEventListener("click",t=>{t.stopPropagation(),c(!0),h({force:!0})}),m?.addEventListener("click",t=>{t.stopPropagation(),c(!1)}),o.addEventListener("click",t=>{o.classList.contains("is-collapsed")&&(t.target instanceof Element&&t.target.closest("a, button")||c(!0))}),h()})();</script> <div class="custom-context-menu" data-context-menu role="menu" aria-hidden="true"> <div class="context-menu-nav" aria-label="页面导航"> <button type="button" data-context-action="back" aria-label="后退上一页" title="后退上一页"> <i class="fa-solid fa-arrow-left" aria-hidden="true"></i> </button> <button type="button" data-context-action="forward" aria-label="前进下一页" title="前进下一页"> <i class="fa-solid fa-arrow-right" aria-hidden="true"></i> </button> <button type="button" data-context-action="reload" aria-label="刷新页面" title="刷新页面"> <i class="fa-solid fa-rotate-right" aria-hidden="true"></i> </button> <a href="/" aria-label="返回首页" title="返回首页"> <i class="fa-solid fa-house" aria-hidden="true"></i> </a> </div> <div class="context-menu-group" data-selection-group hidden> <button type="button" data-context-action="copy-selection" data-selection-item> <i class="fa-regular fa-copy" aria-hidden="true"></i> <span>复制选中文字</span> </button> <button type="button" data-context-action="search-selection" data-selection-item> <i class="fa-brands fa-microsoft" aria-hidden="true"></i> <span>用 Bing 搜索</span> </button> <button type="button" data-context-action="quote-selection" data-selection-item data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>引用到评论区</span> </button> </div> <div class="context-menu-group" data-image-group hidden> <button type="button" data-context-action="copy-image"> <i class="fa-regular fa-image" aria-hidden="true"></i> <span>复制图片</span> </button> <button type="button" data-context-action="open-image"> <i class="fa-solid fa-up-right-from-square" aria-hidden="true"></i> <span>新页面打开图片</span> </button> <button type="button" data-context-action="copy-image-url"> <i class="fa-solid fa-link" aria-hidden="true"></i> <span>复制图片地址</span> </button> </div> <div class="context-menu-group"> <a href="/archives/"> <i class="fa-solid fa-box-archive" aria-hidden="true"></i> <span>时间线</span> </a> <a href="/categories/"> <i class="fa-solid fa-folder" aria-hidden="true"></i> <span>分类</span> </a> <a href="/tags/"> <i class="fa-solid fa-hashtag" aria-hidden="true"></i> <span>标签</span> </a> <button type="button" data-context-action="comments" data-comments-item> <i class="fa-regular fa-comment-dots" aria-hidden="true"></i> <span>跳转评论区</span> </button> <button type="button" data-context-action="print"> <i class="fa-solid fa-print" aria-hidden="true"></i> <span>打印页面</span> </button> <a href="/cc/"> <i class="fa-solid fa-circle-info" aria-hidden="true"></i> <span>网站版权</span> </a> </div> </div> <script>
|
||
(() => {
|
||
const menu = document.querySelector('[data-context-menu]');
|
||
if (!menu) return;
|
||
|
||
const selectionItems = [...menu.querySelectorAll('[data-selection-item]')];
|
||
const selectionGroup = menu.querySelector('[data-selection-group]');
|
||
const commentsItems = [...menu.querySelectorAll('[data-comments-item]')];
|
||
const imageGroup = menu.querySelector('[data-image-group]');
|
||
const focusableSelector = 'a[href], button:not([disabled])';
|
||
let lastSelectionText = '';
|
||
let lastImage = null;
|
||
|
||
const isNativeContextTarget = (target) =>
|
||
target instanceof Element &&
|
||
Boolean(target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'));
|
||
|
||
const getCommentsTarget = () =>
|
||
document.querySelector('.twikoo-comments, #twikoo-wrap, #twikoo, .tk-comments-container');
|
||
|
||
const getSelectionText = () => String(window.getSelection?.() || '').trim();
|
||
|
||
const getImageTarget = (target) => {
|
||
if (!(target instanceof Element)) return null;
|
||
const image = target.closest('img');
|
||
return image instanceof HTMLImageElement ? image : null;
|
||
};
|
||
|
||
const getImageUrl = () => {
|
||
if (!lastImage) return '';
|
||
return lastImage.currentSrc || lastImage.src || lastImage.getAttribute('src') || '';
|
||
};
|
||
|
||
const copyText = async (text) => {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.setAttribute('readonly', '');
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.inset = '0 auto auto -9999px';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
textarea.remove();
|
||
};
|
||
|
||
const setMenuState = () => {
|
||
const hasSelection = lastSelectionText.length > 0;
|
||
const hasComments = Boolean(getCommentsTarget());
|
||
const hasImage = Boolean(getImageUrl());
|
||
|
||
selectionItems.forEach((item) => {
|
||
item.hidden = !hasSelection;
|
||
});
|
||
if (selectionGroup) selectionGroup.hidden = !hasSelection;
|
||
|
||
commentsItems.forEach((item) => {
|
||
item.hidden = !hasComments || (item.hasAttribute('data-selection-item') && !hasSelection);
|
||
});
|
||
|
||
if (imageGroup) imageGroup.hidden = !hasImage;
|
||
};
|
||
|
||
const closeMenu = () => {
|
||
menu.classList.remove('is-open');
|
||
menu.setAttribute('aria-hidden', 'true');
|
||
};
|
||
|
||
const openMenu = (x, y) => {
|
||
setMenuState();
|
||
menu.classList.add('is-open');
|
||
menu.setAttribute('aria-hidden', 'false');
|
||
|
||
const rect = menu.getBoundingClientRect();
|
||
const gap = 10;
|
||
const left = Math.min(Math.max(gap, x), window.innerWidth - rect.width - gap);
|
||
const top = Math.min(Math.max(gap, y), window.innerHeight - rect.height - gap);
|
||
|
||
menu.style.left = `${left}px`;
|
||
menu.style.top = `${top}px`;
|
||
};
|
||
|
||
const jumpToComments = () => {
|
||
const target = getCommentsTarget();
|
||
target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
};
|
||
|
||
const quoteToComments = () => {
|
||
jumpToComments();
|
||
window.setTimeout(() => {
|
||
const textarea = document.querySelector('#twikoo textarea, .twikoo-comments textarea, .tk-input textarea');
|
||
if (!(textarea instanceof HTMLTextAreaElement) || !lastSelectionText) return;
|
||
|
||
const quote = lastSelectionText
|
||
.split(/\r?\n/)
|
||
.map((line) => `> ${line}`)
|
||
.join('\n');
|
||
const current = textarea.value.trim();
|
||
textarea.value = current ? `${current}\n\n${quote}\n` : `${quote}\n`;
|
||
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
textarea.focus();
|
||
}, 500);
|
||
};
|
||
|
||
const searchSelection = () => {
|
||
if (!lastSelectionText) return;
|
||
const url = `https://www.bing.com/search?q=${encodeURIComponent(lastSelectionText)}`;
|
||
window.open(url, '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
const copyImage = async () => {
|
||
const imageUrl = getImageUrl();
|
||
if (!imageUrl) return;
|
||
|
||
try {
|
||
if (!navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {
|
||
throw new Error('Image clipboard is not available.');
|
||
}
|
||
|
||
const response = await fetch(imageUrl, { mode: 'cors' });
|
||
const blob = await response.blob();
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({ [blob.type || 'image/png']: blob }),
|
||
]);
|
||
} catch {
|
||
await copyText(new URL(imageUrl, window.location.href).toString());
|
||
}
|
||
};
|
||
|
||
const openImage = () => {
|
||
const imageUrl = getImageUrl();
|
||
if (!imageUrl) return;
|
||
window.open(new URL(imageUrl, window.location.href).toString(), '_blank', 'noopener,noreferrer');
|
||
};
|
||
|
||
document.addEventListener('contextmenu', (event) => {
|
||
if (isNativeContextTarget(event.target)) return;
|
||
|
||
lastSelectionText = getSelectionText();
|
||
lastImage = getImageTarget(event.target);
|
||
event.preventDefault();
|
||
openMenu(event.clientX, event.clientY);
|
||
});
|
||
|
||
document.addEventListener('click', (event) => {
|
||
const actionTarget = event.target instanceof Element ? event.target.closest('[data-context-action]') : null;
|
||
if (!actionTarget || !menu.contains(actionTarget)) {
|
||
closeMenu();
|
||
return;
|
||
}
|
||
|
||
const action = actionTarget.dataset.contextAction;
|
||
closeMenu();
|
||
|
||
if (action === 'back') history.back();
|
||
if (action === 'forward') history.forward();
|
||
if (action === 'reload') location.reload();
|
||
if (action === 'copy-selection' && lastSelectionText) copyText(lastSelectionText);
|
||
if (action === 'search-selection') searchSelection();
|
||
if (action === 'quote-selection') quoteToComments();
|
||
if (action === 'copy-image') copyImage();
|
||
if (action === 'open-image') openImage();
|
||
if (action === 'copy-image-url') copyText(new URL(getImageUrl(), window.location.href).toString());
|
||
if (action === 'comments') jumpToComments();
|
||
if (action === 'print') window.print();
|
||
});
|
||
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') closeMenu();
|
||
if (event.key === 'Tab' && menu.classList.contains('is-open')) {
|
||
const items = [...menu.querySelectorAll(focusableSelector)].filter((item) => !item.closest('[hidden]'));
|
||
if (items.length === 0) return;
|
||
const first = items[0];
|
||
const last = items[items.length - 1];
|
||
|
||
if (event.shiftKey && document.activeElement === first) {
|
||
event.preventDefault();
|
||
last.focus();
|
||
} else if (!event.shiftKey && document.activeElement === last) {
|
||
event.preventDefault();
|
||
first.focus();
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener('scroll', closeMenu, { passive: true });
|
||
window.addEventListener('resize', closeMenu);
|
||
})();
|
||
</script> <script src="https://cdn.jsdmirror.com/npm/instantsearch.js@4.56.0"></script> <script src="https://cdn.jsdmirror.com/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script> <script src="https://cdn.jsdmirror.com/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script> <script src="/js/random.js"></script> <script src="/js/search/typesense-search.js"></script> <script src="/js/external-links.js"></script> <script>
|
||
(() => {
|
||
const nav = document.querySelector('.nav');
|
||
const toggle = document.querySelector('[data-nav-toggle]');
|
||
const menu = document.querySelector('[data-nav-menu]');
|
||
if (!nav || !toggle || !menu) return;
|
||
|
||
function setMenu(open) {
|
||
nav.classList.toggle('is-open', open);
|
||
toggle.setAttribute('aria-expanded', String(open));
|
||
toggle.setAttribute('aria-label', open ? '关闭菜单' : '打开菜单');
|
||
}
|
||
|
||
toggle.addEventListener('click', () => {
|
||
setMenu(!nav.classList.contains('is-open'));
|
||
});
|
||
|
||
menu.addEventListener('click', (event) => {
|
||
if (event.target instanceof Element && event.target.closest('a')) setMenu(false);
|
||
});
|
||
|
||
window.addEventListener('resize', () => {
|
||
if (window.innerWidth > 720) setMenu(false);
|
||
});
|
||
})();
|
||
|
||
(() => {
|
||
const storageKey = 'site-theme';
|
||
const root = document.documentElement;
|
||
const button = document.querySelector('[data-theme-toggle]');
|
||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||
const getSystemTheme = () => (media.matches ? 'dark' : 'light');
|
||
const getTheme = () => localStorage.getItem(storageKey) || getSystemTheme();
|
||
|
||
function applyTheme(theme) {
|
||
root.dataset.theme = theme;
|
||
button?.setAttribute('aria-label', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
|
||
button?.setAttribute('title', theme === 'dark' ? '切换到日间模式' : '切换到夜间模式');
|
||
}
|
||
|
||
applyTheme(getTheme());
|
||
|
||
button?.addEventListener('click', () => {
|
||
const nextTheme = root.dataset.theme === 'dark' ? 'light' : 'dark';
|
||
localStorage.setItem(storageKey, nextTheme);
|
||
applyTheme(nextTheme);
|
||
});
|
||
|
||
media.addEventListener('change', () => {
|
||
if (!localStorage.getItem(storageKey)) applyTheme(getSystemTheme());
|
||
});
|
||
})();
|
||
|
||
(() => {
|
||
const runtime = document.getElementById('site-runtime');
|
||
if (!runtime) return;
|
||
|
||
const start = new Date(window.SITE_CONFIG.site.startTime).getTime();
|
||
const pad = (value) => String(value).padStart(2, '0');
|
||
|
||
function updateRuntime() {
|
||
const diff = Math.max(0, Date.now() - start);
|
||
const days = Math.floor(diff / 86400000);
|
||
const hours = Math.floor((diff / 3600000) % 24);
|
||
const minutes = Math.floor((diff / 60000) % 60);
|
||
const seconds = Math.floor((diff / 1000) % 60);
|
||
runtime.textContent = `${days} 天 ${pad(hours)} 时 ${pad(minutes)} 分 ${pad(seconds)} 秒`;
|
||
}
|
||
|
||
updateRuntime();
|
||
window.setInterval(updateRuntime, 1000);
|
||
})();
|
||
</script> </body> </html> <script>
|
||
(() => {
|
||
const prose = document.querySelector('.prose[data-toc]');
|
||
if (!prose) return;
|
||
|
||
let toc = [];
|
||
try {
|
||
toc = JSON.parse(prose.dataset.toc || '[]');
|
||
} catch {
|
||
toc = [];
|
||
}
|
||
|
||
prose.querySelectorAll('h1, h2, h3').forEach((heading, index) => {
|
||
const item = toc[index];
|
||
if (item?.id && !heading.id) heading.id = item.id;
|
||
});
|
||
|
||
const tocTree = document.querySelector('[data-toc-tree]');
|
||
if (tocTree && toc.length > 0) {
|
||
const links = [...tocTree.querySelectorAll('[data-toc-link]')];
|
||
const groups = [...tocTree.querySelectorAll('[data-toc-group]')];
|
||
const headings = toc
|
||
.map((item) => document.getElementById(item.id))
|
||
.filter(Boolean);
|
||
|
||
const setActiveToc = (id) => {
|
||
links.forEach((link) => {
|
||
const isActive = link.getAttribute('href') === `#${id}`;
|
||
link.classList.toggle('is-active', isActive);
|
||
if (isActive) link.setAttribute('aria-current', 'location');
|
||
else link.removeAttribute('aria-current');
|
||
});
|
||
|
||
const activeLink = tocTree.querySelector(`[data-toc-link][href="#${CSS.escape(id)}"]`);
|
||
const activeGroup = activeLink?.closest('[data-toc-group]');
|
||
groups.forEach((group) => group.classList.toggle('is-open', group === activeGroup));
|
||
};
|
||
|
||
const updateToc = () => {
|
||
const anchorOffset = 132;
|
||
let activeId = headings[0]?.id;
|
||
|
||
headings.forEach((heading) => {
|
||
if (heading.getBoundingClientRect().top <= anchorOffset) activeId = heading.id;
|
||
});
|
||
|
||
if (activeId) setActiveToc(activeId);
|
||
};
|
||
|
||
tocTree.addEventListener('click', (event) => {
|
||
const link = event.target.closest?.('[data-toc-link]');
|
||
if (!link) return;
|
||
|
||
const group = link.closest('[data-toc-group]');
|
||
groups.forEach((item) => item.classList.toggle('is-open', item === group));
|
||
});
|
||
|
||
updateToc();
|
||
window.addEventListener('scroll', updateToc, { passive: true });
|
||
window.addEventListener('resize', updateToc);
|
||
}
|
||
|
||
const summaryText = document.querySelector('.article-summary-text');
|
||
if (!summaryText) return;
|
||
|
||
const fullText = summaryText.dataset.summaryText || summaryText.textContent || '';
|
||
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
if (reduceMotion) {
|
||
summaryText.textContent = fullText;
|
||
return;
|
||
}
|
||
|
||
summaryText.textContent = '';
|
||
let index = 0;
|
||
|
||
function typeSummary() {
|
||
index += 1;
|
||
summaryText.textContent = fullText.slice(0, index);
|
||
if (index < fullText.length) {
|
||
window.setTimeout(typeSummary, 34);
|
||
}
|
||
}
|
||
|
||
window.setTimeout(typeSummary, 280);
|
||
})();
|
||
</script> <script>
|
||
(() => {
|
||
const prose = document.querySelector('.post-article .prose');
|
||
if (!prose) return;
|
||
|
||
const collapsedLineCount = 8;
|
||
const copyLabels = {
|
||
idle: '复制',
|
||
done: '已复制',
|
||
error: '复制失败',
|
||
};
|
||
|
||
const languageNames = {
|
||
bash: 'Bash',
|
||
c: 'C',
|
||
cpp: 'C++',
|
||
cs: 'C#',
|
||
css: 'CSS',
|
||
html: 'HTML',
|
||
js: 'JavaScript',
|
||
json: 'JSON',
|
||
jsx: 'JSX',
|
||
md: 'Markdown',
|
||
powershell: 'PowerShell',
|
||
py: 'Python',
|
||
shell: 'Shell',
|
||
sh: 'Shell',
|
||
ts: 'TypeScript',
|
||
tsx: 'TSX',
|
||
txt: 'Text',
|
||
yaml: 'YAML',
|
||
yml: 'YAML',
|
||
};
|
||
|
||
function getCodeLanguage(pre, code) {
|
||
const rawLanguage =
|
||
pre.dataset.language ||
|
||
code.dataset.language ||
|
||
[...pre.classList, ...code.classList]
|
||
.find((className) => className.startsWith('language-'))
|
||
?.replace(/^language-/, '') ||
|
||
'';
|
||
|
||
const normalized = rawLanguage.trim().toLowerCase();
|
||
return languageNames[normalized] || normalized.toUpperCase() || 'Code';
|
||
}
|
||
|
||
function getCodeText(code, lines) {
|
||
if (lines.length > 0) return lines.map((line) => line.textContent || '').join('\n');
|
||
return code.textContent || '';
|
||
}
|
||
|
||
prose.querySelectorAll('img').forEach((image) => {
|
||
if (image.closest('a, .article-header-cover')) return;
|
||
|
||
const src = image.currentSrc || image.getAttribute('src');
|
||
if (!src) return;
|
||
|
||
const link = document.createElement('a');
|
||
link.className = 'fancybox';
|
||
link.href = src;
|
||
link.dataset.fancybox = 'article-gallery';
|
||
link.dataset.caption = image.getAttribute('alt') || '';
|
||
image.parentNode?.insertBefore(link, image);
|
||
link.appendChild(image);
|
||
});
|
||
|
||
prose.querySelectorAll('table').forEach((table) => {
|
||
if (table.closest('.table-scroll')) return;
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'table-scroll';
|
||
table.parentNode?.insertBefore(wrapper, table);
|
||
wrapper.appendChild(table);
|
||
});
|
||
|
||
prose.querySelectorAll('.butterfly-hide-inline').forEach((item) => {
|
||
const button = item.querySelector('.butterfly-hide-toggle');
|
||
if (!button) return;
|
||
|
||
button.addEventListener('click', () => {
|
||
item.classList.toggle('is-open');
|
||
});
|
||
});
|
||
|
||
if (window.Fancybox) {
|
||
window.Fancybox.bind('[data-fancybox="article-gallery"]', {
|
||
compact: false,
|
||
dragToClose: true,
|
||
Images: {
|
||
zoom: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
async function copyText(text) {
|
||
if (navigator.clipboard?.writeText) {
|
||
await navigator.clipboard.writeText(text);
|
||
return;
|
||
}
|
||
|
||
const textarea = document.createElement('textarea');
|
||
textarea.value = text;
|
||
textarea.setAttribute('readonly', '');
|
||
textarea.style.position = 'fixed';
|
||
textarea.style.top = '-9999px';
|
||
textarea.style.opacity = '0';
|
||
document.body.appendChild(textarea);
|
||
textarea.select();
|
||
document.execCommand('copy');
|
||
textarea.remove();
|
||
}
|
||
|
||
prose.querySelectorAll('pre').forEach((pre) => {
|
||
if (pre.closest('.code-block')) return;
|
||
|
||
const code = pre.querySelector('code');
|
||
if (!code) return;
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'code-block';
|
||
pre.parentNode?.insertBefore(wrapper, pre);
|
||
wrapper.appendChild(pre);
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'code-block-header';
|
||
|
||
const language = document.createElement('span');
|
||
language.className = 'code-block-language';
|
||
language.textContent = getCodeLanguage(pre, code);
|
||
|
||
const toolbar = document.createElement('div');
|
||
toolbar.className = 'code-block-toolbar';
|
||
|
||
const copyButton = document.createElement('button');
|
||
copyButton.className = 'code-block-copy';
|
||
copyButton.type = 'button';
|
||
copyButton.textContent = copyLabels.idle;
|
||
copyButton.setAttribute('aria-label', '复制代码');
|
||
|
||
const toggleButton = document.createElement('button');
|
||
toggleButton.className = 'code-block-toggle';
|
||
toggleButton.type = 'button';
|
||
toggleButton.textContent = '展开';
|
||
toggleButton.setAttribute('aria-expanded', 'false');
|
||
|
||
toolbar.append(copyButton, toggleButton);
|
||
header.append(language, toolbar);
|
||
wrapper.appendChild(header);
|
||
|
||
const codeLines = [...code.querySelectorAll(':scope > .line')];
|
||
if (codeLines.length > 0) {
|
||
wrapper.classList.add('has-line-numbers');
|
||
wrapper.style.setProperty('--line-number-width', `${String(codeLines.length).length + 1}ch`);
|
||
codeLines.forEach((line, index) => {
|
||
line.dataset.line = String(index + 1);
|
||
});
|
||
}
|
||
|
||
const lineCount = codeLines.length || (code.textContent || '').split(/\r?\n/).length;
|
||
if (lineCount > collapsedLineCount) wrapper.classList.add('is-collapsible');
|
||
|
||
copyButton.addEventListener('click', async () => {
|
||
try {
|
||
await copyText(getCodeText(code, codeLines));
|
||
copyButton.textContent = copyLabels.done;
|
||
window.setTimeout(() => {
|
||
copyButton.textContent = copyLabels.idle;
|
||
}, 1400);
|
||
} catch {
|
||
copyButton.textContent = copyLabels.error;
|
||
window.setTimeout(() => {
|
||
copyButton.textContent = copyLabels.idle;
|
||
}, 1600);
|
||
}
|
||
});
|
||
|
||
toggleButton.addEventListener('click', () => {
|
||
const isExpanded = wrapper.classList.toggle('is-expanded');
|
||
toggleButton.textContent = isExpanded ? '收起' : '展开';
|
||
toggleButton.setAttribute('aria-expanded', String(isExpanded));
|
||
});
|
||
});
|
||
})();
|
||
</script> |