_1、服务端配置

_1.1、部署

官方对于部署的介绍非常详细,各种方案都提供了,我这里选择使用 docker 来进行部署。

添加服务启动脚本start.sh/tmp/scraper目录

      docker run -itd --name meilisearch -p 7700:7700 --restart=always \
  -e MEILI_ENV="production" -e MEILI_NO_ANALYTICS=true \
  -e MEILI_MASTER_KEY="自定义一个不少于16字节的秘钥" \
  -v $(pwd)/meili_data:/meili_data \
  getmeili/meilisearch
    

自建的时候,需要将环境变量声明为生产,并且必须指定 master-key,否则将会提示无法使用。

然后运行该脚本,服务启动,通过监听日志,查看服务状态是否正常。

也可以请求服务的健康接口进行验证:

      $ curl -s http://localhost:7700/health | jq
{
  "status": "available"
}
    

注意,生产模式下,只有这一个接口是不需要秘钥认证即可访问的,其他接口访问的时候都需要带上秘钥。

_1.2、创建搜索的key

上边有了一个 master-key 用于爬虫抓取使用,还需要创建一个只有搜索权限的 key,可通过如下命令进行创建search.sh/tmp/scraper目录

      curl \
  -X POST 'http://localhost:7700/keys' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer 你自定义的秘钥' \
  --data-binary '{
    "description": "vp.xiaoying.org.cn key",
    "actions": ["search"],
    "indexes": ["blog"],  // 第四步建立索引抓取配置中的index_uid的值需与该值保持一致
    "expiresAt": "2099-01-01T00:00:00Z"
  }'
    

创建完成之后,能看到返回内容中有一个 key 的字段,就是这个只有搜索权限的 key 了。

_1.3、添加域名

这个根据自己的实际情况,我这里给 Nginx 添加配置文件,配置域名:

      server {
    listen 443 ssl;
    server_name vp.xiaoying.org.cn;

    ssl_certificate /etc/ssl/certs/vp.xiaoying.org.cn_bundle.crt;
    ssl_certificate_key /etc/ssl/certs/vp.xiaoying.org.cn.key;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    location ^~ /multi-search/ {
        proxy_set_header Host $host;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_pass http://127.0.0.1:7700;
    }

}
    

这样就完成了与meilisearch一样的服务端配置信息:

_1.4、建立索引

官方提供了爬虫工具,我们只需要进行简单的配置,即可将数据索引建立起来。

关于这段配置流程,官方文档同样给了详细的说明:抓取你的内容 (opens new window)

新建config.json如下

      {
    "index_uid": "teek",
    "sitemap_urls": ["https://vp.xiaoying.org.cn/sitemap.xml"],
    "start_urls": ["https://vp.xiaoying.org.cn/"],
    "stop_urls": [],
    "selectors": {
        "lvl0": {
            "selector": "section.has-active div h2",
            "defaultValue": "Documentation"
        },
        "lvl1": ".content h1", 
        "lvl2": ".content h2", 
        "lvl3": ".content h3", 
        "lvl4": ".content h4", 
        "lvl5": ".content h5", 
        "content": ".content p, .content li"    
    },
    "strip_chars": " .,;:#",
    "scrap_start_urls": true,
    "custom_settings": {
        "searchableAttributes": [
            "hierarchy_lvl2",
            "hierarchy_lvl3",
            "hierarchy_lvl4",
            "hierarchy_lvl5",
            "content"
        ],
        "displayedAttributes": [
            "hierarchy_lvl1",
            "hierarchy_lvl2",
            "hierarchy_lvl3",
            "hierarchy_lvl4",
            "hierarchy_lvl5",
            "content",
            "hierarchy_lvl0",
            "url",
            "anchor"
        ],
        "filterableAttributes": [
            "hierarchy_lvl2",
            "hierarchy_lvl3",
            "hierarchy_lvl4",
            "hierarchy_lvl5"
        ]
    },
}
    

index_uid :为索引名称,如果服务端没有,则会自动创建,需与第二步的indexes保持一致。

新建teek.sh如下对内容进行抓取:

      docker run -t --rm \
  --network=host \
  -e MEILISEARCH_HOST_URL='http://localhost:7700' \
  -e MEILISEARCH_API_KEY='第一步自定义的Master Key' \
  -v /tmp/scraper/config.json:/docs-scraper/config.json \
  getmeili/docs-scraper pipenv run ./docs_scraper config.json
    

config.jsonteek.sh放到/tmp/scraper目录下,然后通过如下命令运行爬虫对内容进行抓取:

      sh teek.sh
    

_2、代码配置

_2.1、编写Meilisearch.vue

      <template>
    <div>
        <!-- 搜索触发按钮 -->
        <button
            aria-label="Search"
            class="DocSearch DocSearch-Button"
            type="button"
            @click="toggleSearch">
            <span class="DocSearch-Button-Container">
                <span class="vp-icon DocSearch-Search-Icon"></span>
                <span class="DocSearch-Button-Placeholder">Search</span>
            </span>
            <span class="DocSearch-Button-Keys">
                <kbd class="DocSearch-Button-Key"></kbd>
                <kbd class="DocSearch-Button-Key">K</kbd>
            </span>
        </button>

        <!-- 搜索弹窗 -->
        <div
            v-if="isSearchOpen"
            aria-labelledby="docsearch-label"
            aria-modal="true"
            class="DocSearch DocSearch-Container"
            role="dialog"
            @click="closeSearch">
            <div class="DocSearch-Modal" @click.stop>
                <header class="DocSearch-SearchBar">
                    <form class="DocSearch-Form" @submit.prevent>
                        <label
                            id="docsearch-label"
                            class="DocSearch-MagnifierLabel"
                            for="docsearch-input">
                            <span
                                class="DocSearch-VisuallyHiddenForAccessibility"
                                >Search</span
                            >
                        </label>
                        <input
                            id="docsearch-input"
                            ref="searchInput"
                            v-model="searchQuery"
                            :placeholder="meiliConfig.placeholder || '搜索文档...'"
                            autocapitalize="off"
                            autocomplete="off"
                            autocorrect="off"
                            autofocus
                            class="DocSearch-Input"
                            spellcheck="false"
                            type="search"
                            @input="handleInput"
                            @keydown.down.prevent="moveDown"
                            @keydown.up.prevent="moveUp"
                            @keydown.esc.prevent="closeSearch"
                            @keydown.enter.prevent="goToHit" />
                        <button
                            v-if="searchQuery"
                            class="DocSearch-Reset"
                            type="reset"
                            @click="clearSearch">
                            ✕
                        </button>
                    </form>
                    <button
                        class="DocSearch-Cancel"
                        type="button"
                        @click="closeSearch">
                        Cancel
                    </button>
                </header>

                <!-- 搜索结果区域 -->
                <div class="DocSearch-Dropdown">
                    <div
                        ref="hitsContainer"
                        class="DocSearch-Dropdown-Container">
                        <!-- 分组渲染搜索结果 -->
                        <section
                            v-for="(group, groupIdx) in groupedHits"
                            v-if="searchQuery"
                            :key="groupIdx"
                            class="DocSearch-Hits">
                            <div class="DocSearch-Hit-source">
                                {{ group.title }}
                            </div>
                            <ul
                                :id="'docsearch-hits' + groupIdx + '-list'"
                                role="listbox">
                                <!-- 列表项:添加active类模拟默认hover,使用highlightKeyword处理关键词高亮 -->
                                <li
                                    v-for="(hit, itemIdx) in group.items"
                                    :key="itemIdx"
                                    :class="{ 'active': currentGroupIndex === groupIdx && currentItemIndex === itemIdx }"
                                    class="DocSearch-Hit"
                                    role="option"
                                    @mouseenter="handleMouseEnter(groupIdx, itemIdx)">
                                    <a
                                        :href="hit.url"
                                        @click="goToSpecificHit(groupIdx, itemIdx, $event)">
                                        <div class="DocSearch-Hit-Container">
                                            <div
                                                class="DocSearch-Hit-content-wrapper">
                                                <!-- 主标题:关键词高亮 -->
                                                <span
                                                    class="DocSearch-Hit-title"
                                                    v-html="highlightKeyword(hit.hierarchy_lvl2, searchQuery)"></span>
                                                <!-- 副标题:仅匹配关键词时展示 + 高亮 -->
                                                <span
                                                    v-if="hit.hierarchy_lvl3 && isMatch(hit.hierarchy_lvl3, searchQuery)"
                                                    class="DocSearch-Hit-path"
                                                    v-html="highlightKeyword(hit.hierarchy_lvl3, searchQuery)"></span>
                                                <!-- 内容:仅匹配关键词时展示 + 高亮 -->
                                                <span
                                                    v-if="hit.content && isMatch(hit.content, searchQuery)"
                                                    class="DocSearch-Hit-path"
                                                    v-html="highlightKeyword(hit.content, searchQuery)"></span>
                                            </div>
                                            <div class="DocSearch-Hit-action">
                                                <svg
                                                    height="20"
                                                    viewBox="0 0 20 20"
                                                    width="20">
                                                    <g
                                                        fill="none"
                                                        fill-rule="evenodd"
                                                        stroke="currentColor"
                                                        stroke-linecap="round"
                                                        stroke-linejoin="round">
                                                        <path
                                                            d="M18 3v4c0 2-2 4-4 4H2"></path>
                                                        <path
                                                            d="M8 17l-6-6 6-6"></path>
                                                    </g>
                                                </svg>
                                            </div>
                                        </div>
                                    </a>
                                </li>
                            </ul>
                        </section>
                        <!-- 无结果提示 -->
                        <div
                            v-if="searchQuery && hits.length === 0"
                            class="DocSearch-NoResults">
                            No results for "{{ searchQuery }}"
                        </div>
                    </div>
                </div>

                <!-- Footer 布局 -->
                <footer class="DocSearch-Footer">
                    <div class="DocSearch-Logo">
                        <span class="DocSearch-Label">Search by</span>
                        <a
                            href="https://vp.xiaoying.org.cn/pages/09b133"
                            rel="noopener noreferrer"
                            target="_blank"
                            style="background-color: #734894;">
                            <img
                                alt="Meilisearch's logo"
                                data-nimg="1"
                                decoding="async"
                                fetchpriority="high"
                                height="25"
                                src="https://www.meilisearch.com/_next/static/media/logo.cd874c57.svg"
                                width="162" />
                        </a>
                    </div>
                    <ul class="DocSearch-Commands">
                        <li>
                            <kbd class="DocSearch-Commands-Key">
                                <svg
                                    aria-label="Enter key"
                                    height="15"
                                    role="img"
                                    width="15">
                                    <g
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        stroke-width="1.2">
                                        <path
                                            d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3"></path>
                                    </g></svg></kbd
                            ><span class="DocSearch-Label">选择</span>
                        </li>
                        <li>
                            <kbd class="DocSearch-Commands-Key">
                                <svg
                                    aria-label="Arrow down"
                                    height="15"
                                    role="img"
                                    width="15">
                                    <g
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        stroke-width="1.2">
                                        <path
                                            d="M7.5 3.5v8M10.5 8.5l-3 3-3-3"></path>
                                    </g>
                                </svg> </kbd
                            ><kbd class="DocSearch-Commands-Key"
                                ><svg
                                    aria-label="Arrow up"
                                    height="15"
                                    role="img"
                                    width="15">
                                    <g
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        stroke-width="1.2">
                                        <path
                                            d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3"></path>
                                    </g></svg></kbd
                            ><span class="DocSearch-Label">切换</span>
                        </li>
                        <li>
                            <kbd class="DocSearch-Commands-Key"
                                ><svg
                                    aria-label="Escape key"
                                    height="15"
                                    role="img"
                                    width="15">
                                    <g
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        stroke-width="1.2">
                                        <path
                                            d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956"></path>
                                    </g></svg></kbd
                            ><span class="DocSearch-Label">关闭</span>
                        </li>
                    </ul>
                </footer>
            </div>
        </div>
    </div>
</template>

<script setup>
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch';
import instantsearch from 'instantsearch.js';
import { hits as hitsWidget } from 'instantsearch.js/es/widgets';
import { useData } from 'vitepress';
import { computed, nextTick, onUnmounted, ref } from 'vue';

const { theme } = useData();
const meiliConfig = computed(() => theme.value.meilisearch || {});

const isSearchOpen = ref(false);
const searchQuery = ref('');
const hits = ref([]);
const searchInput = ref(null);
const hitsContainer = ref(null);
let searchInstance = null;
let searchClient = null;
let selectedIndex = ref(-1);
const currentGroupIndex = ref(-1);
const currentItemIndex = ref(-1);

// 按 hierarchy_lvl1 分组展示搜索结果
const groupedHits = computed(() => {
    const groupMap = new Map();
    hits.value.forEach(hit => {
        const groupKey = hit.hierarchy_lvl1 || 'Documentation';
        if (!groupMap.has(groupKey)) {
            groupMap.set(groupKey, { title: groupKey, items: [] });
        }
        groupMap.get(groupKey).items.push(hit);
    });
    return Array.from(groupMap.values()).sort((a, b) => a.title.localeCompare(b.title));
});

// 同步选中状态(currentGroupIndex/currentItemIndex → selectedIndex)
const updateSelectedIndex = () => {
    const groups = groupedHits.value;

    // 检查 groups 是否为空
    if (!groups || groups.length === 0) {
        selectedIndex.value = -1;
        return;
    }

    // 检查 group 和 item 索引是否有效
    if (
        currentGroupIndex.value === -1 ||
        currentItemIndex.value === -1 ||
        currentGroupIndex.value >= groups.length
    ) {
        selectedIndex.value = -1;
        return;
    }

    const currentGroup = groups[currentGroupIndex.value];

    // 检查当前分组是否有内容
    if (!currentGroup || !currentGroup.items || currentItemIndex.value >= currentGroup.items.length) {
        selectedIndex.value = -1;
        return;
    }

    let prevItemsCount = 0;
    for (let i = 0; i < currentGroupIndex.value; i++) {
        prevItemsCount += groups[i].items.length;
    }

    selectedIndex.value = prevItemsCount + currentItemIndex.value;
};

// 切换搜索弹窗显示
const toggleSearch = async () => {
    isSearchOpen.value = !isSearchOpen.value;
    if (isSearchOpen.value) {
        await nextTick();
        searchInput.value?.focus();
        initMeiliSearch();
    } else {
        clearSearch();
    }
};

// 初始化 MeiliSearch 搜索实例
const initMeiliSearch = () => {
    if (searchInstance || !meiliConfig.value.host || !meiliConfig.value.apiKey || !meiliConfig.value.indexName) return;
    searchClient = instantMeiliSearch(meiliConfig.value.host, meiliConfig.value.apiKey).searchClient;

    searchInstance = instantsearch({
        indexName: meiliConfig.value.indexName,
        searchClient,
        routing: false
    });

    searchInstance.addWidgets([
        hitsWidget({
            container: hitsContainer.value,
            transformItems: items => {
                // 确保每个搜索结果项都有正确的url,规范化URL格式
                const validItems = items.map(item => {
                    // 如果有anchor但没有url,构造一个url
                    if (!item.url && item.anchor) {
                        item.url = new URL(`#${item.anchor}`, window.location.href).href;
                    }

                    // 如果既没有url也没有anchor,使用当前页面URL
                    if (!item.url) {
                        item.url = window.location.href;
                    }

                    return item;
                }).filter(item => item.url); // 只保留有url的项目

                hits.value = validItems;
                // 默认选中第一个项(模拟hover)
                if (validItems.length > 0) {
                    currentGroupIndex.value = 0; // 第一个分组
                    currentItemIndex.value = 0;  // 分组第一个项
                    // 等待DOM渲染后执行滚动(确保元素存在)
                    nextTick(() => scrollToActiveItem());
                } else {
                    // 无结果时重置状态
                    currentGroupIndex.value = -1;
                    currentItemIndex.value = -1;
                    selectedIndex.value = -1;
                }
                return validItems;
            },
            templates: {
                item: () => ''
            }
        })
    ]);

    searchInstance.start();
};

// 处理输入:为空时清空结果,不为空时执行搜索
const handleInput = () => {
    if (searchInstance?.helper) {
        if (searchQuery.value) {
            searchInstance.helper.setQuery(searchQuery.value).search();
        } else {
            clearSearch();
        }
    }
};

// 清空搜索(输入、结果、选中状态)
const clearSearch = () => {
    searchQuery.value = '';
    hits.value = [];
    selectedIndex.value = -1;
    currentGroupIndex.value = -1;
    currentItemIndex.value = -1;
};

// 关闭搜索弹窗
const closeSearch = () => {
    isSearchOpen.value = false;
    clearSearch();
};

const handleMouseEnter = (groupIdx, itemIdx) => {
    currentGroupIndex.value = groupIdx; // 更新分组索引
    currentItemIndex.value = itemIdx;   // 更新项索引
    updateSelectedIndex();              // 同步全局索引
};

// 跳转到特定搜索结果项
const goToSpecificHit = (groupIdx, itemIdx, event) => {
    // 阻止默认跳转行为
    event.preventDefault();

    // 更新选中索引
    currentGroupIndex.value = groupIdx;
    currentItemIndex.value = itemIdx;

    // 执行跳转
    goToHit();
};

// 跳转到选中的搜索结果
const goToHit = () => {
    // 直接使用当前分组和项索引获取正确的搜索结果项
    if (currentGroupIndex.value >= 0 && currentItemIndex.value >= 0 &&
        groupedHits.value[currentGroupIndex.value] &&
        groupedHits.value[currentGroupIndex.value].items[currentItemIndex.value]) {

        const hit = groupedHits.value[currentGroupIndex.value].items[currentItemIndex.value];
        window.location.href = hit.url;
        closeSearch();
    }
};

// 向下切换逻辑
const moveDown = () => {
    const groups = groupedHits.value;
    if (groups.length === 0) return;

    if (currentGroupIndex.value === -1) {
        for (let i = 0; i < groups.length; i++) {
            if (groups[i].items.length > 0) {
                currentGroupIndex.value = i;
                currentItemIndex.value = 0;
                scrollToActiveItem();
                return;
            }
        }
        return;
    }

    const currentGroup = groups[currentGroupIndex.value];
    if (currentItemIndex.value < currentGroup.items.length - 1) {
        currentItemIndex.value++;
        scrollToActiveItem();
        return;
    }

    let nextGroupIndex = currentGroupIndex.value + 1;
    while (nextGroupIndex < groups.length) {
        if (groups[nextGroupIndex].items.length > 0) {
            currentGroupIndex.value = nextGroupIndex;
            currentItemIndex.value = 0;
            scrollToActiveItem();
            return;
        }
        nextGroupIndex++;
    }

    for (let i = 0; i < groups.length; i++) {
        if (groups[i].items.length > 0) {
            currentGroupIndex.value = i;
            currentItemIndex.value = 0;
            scrollToActiveItem();
            return;
        }
    }
};

// 向上切换逻辑
const moveUp = () => {
    const groups = groupedHits.value;
    if (groups.length === 0) return;

    if (currentGroupIndex.value === -1) {
        for (let i = groups.length - 1; i >= 0; i--) {
            if (groups[i].items.length > 0) {
                currentGroupIndex.value = i;
                currentItemIndex.value = groups[i].items.length - 1;
                scrollToActiveItem();
                return;
            }
        }
        return;
    }

    if (currentItemIndex.value > 0) {
        currentItemIndex.value--;
        scrollToActiveItem();
        return;
    }

    let prevGroupIndex = currentGroupIndex.value - 1;
    while (prevGroupIndex >= 0) {
        if (groups[prevGroupIndex].items.length > 0) {
            currentGroupIndex.value = prevGroupIndex;
            currentItemIndex.value = groups[prevGroupIndex].items.length - 1;
            scrollToActiveItem();
            return;
        }
        prevGroupIndex--;
    }

    for (let i = groups.length - 1; i >= 0; i--) {
        if (groups[i].items.length > 0) {
            currentGroupIndex.value = i;
            currentItemIndex.value = groups[i].items.length - 1;
            scrollToActiveItem();
            return;
        }
    }
};

// 滚动到当前选中项
const scrollToActiveItem = () => {
    const groups = groupedHits.value;
    if (currentGroupIndex.value === -1 || currentItemIndex.value === -1 || !groups.length) return;

    const groupUl = document.getElementById(`docsearch-hits${currentGroupIndex.value}-list`);
    if (!groupUl) return;
    const activeLi = groupUl.children[currentItemIndex.value];
    if (activeLi) {
        activeLi.scrollIntoView({
            behavior: 'smooth',
            block: 'nearest',
            inline: 'nearest'
        });
    }
};

// 判断文本是否包含关键词(大小写不敏感)
const isMatch = (text, keyword) => {
    if (!text || !keyword) return false;
    // 转义正则特殊字符,避免报错(如. * + ?等)
    const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    // 大小写不敏感匹配
    return new RegExp(escapedKeyword, 'i').test(text);
};

// 高亮文本中的关键词
const highlightKeyword = (text, keyword) => {
    if (!text || !keyword) return text; // 无关键词时直接返回原文本
    const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    // 用<span>包裹匹配的关键词,添加高亮样式
    return text.replace(
        new RegExp(`(${ escapedKeyword })`, 'gi'), // g=全局匹配,i=大小写不敏感
        '<span class="docsearch-highlight">$1</span>'
    );
};

// 注册 Ctrl+K 快捷键
if (typeof window !== 'undefined'){
    document.addEventListener('keydown', e => {
        if (e.ctrlKey && e.key === 'k') {
            e.preventDefault();
            toggleSearch();
        }
    });
}


// 组件卸载时销毁搜索实例
onUnmounted(() => {
    if (searchInstance) {
        searchInstance.removeWidgets();
        searchInstance.destroy();
        searchInstance = null;
    }
});
</script>

<style scoped>
.DocSearch-Footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px;
}

.DocSearch-Logo {
    display: flex;
    align-items: center;
    gap: 4px;
}

.DocSearch-Button {
    background-color: transparent;
    margin-left: 1rem;
}

/* hover样式(与active类保持一致,模拟默认hover) */
.DocSearch-Hit a:hover,
.DocSearch-Hit.active a {
    background-color: var(--docsearch-primary-color);
}

/* hover/active时子元素颜色同步 */
.DocSearch-Hit a:hover *,
.DocSearch-Hit.active a * {
    color: var(--docsearch-hit-active-color);
}

.DocSearch-Hit-action {
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s ease, visibility 0.2s ease;
}

.DocSearch-Hit a:hover .DocSearch-Hit-action,
.DocSearch-Hit.active .DocSearch-Hit-action {
    opacity: 1;
    visibility: visible;
}

.DocSearch-Logo img {
    display: block;
    height: 44px;
}

/* 关键词高亮样式(可根据主题调整颜色) */
:deep(.docsearch-highlight) {
    background-color: rgba(255, 235, 59, 0.8);
    color: var(--docsearch-text-color) !important;
    /* 避免被父级hover样式覆盖 */
    padding: 0 2px;
    border-radius: 2px;
    box-shadow: 0 0 2px rgba(255, 235, 59, 0.5);
    /* 强化视觉,确保可见 */
}

/* 移动端全屏样式 */
@media (max-width: 768px) {
    .DocSearch-Container {
        position: fixed;
        top: 0;
        left: 0;
        width: 100vw;
        height: 100vh;
    }

    .DocSearch-Modal {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: calc(100% - 50px);
        /* 为页脚留出高度,假设页脚高50px,可根据实际调整 */
        max-width: 100%;
        max-height: calc(100% - 50px);
        border-radius: 0;
        margin: 0;
        display: flex;
        flex-direction: column;
    }

    .DocSearch-SearchBar {
        flex: 0 0 auto;
    }

    .DocSearch-Dropdown {
        flex: 1;
        overflow-y: auto;
        max-height: calc(100vh - 170px);
        /* 为搜索框和页脚等留出空间,数值根据实际调整 */
    }

    .DocSearch-Footer {
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 44px;
        background: var(--docsearch-footer-background);
        padding: 8px !important;
        display: flex;
        justify-content: space-between;
        align-items: center;
    }

    .DocSearch-Commands {
        display: flex;
        width: 60%;
        gap: 5px;
        margin: 0;
        padding: 0;
        list-style: none;
    }

    .DocSearch-Commands li {
        display: flex;
        align-items: center;
        gap: 4px;
        margin: 0;
        padding: 0;
    }

    .DocSearch-Label {
        font-size: 12px;
        color: var(--docsearch-muted-color);
    }

    .DocSearch-Commands-Key {
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .DocSearch-Logo {
        display: flex;
        align-items: center;
        gap: 4px;
        padding-left: 10px;
        vertical-align: middle;
        /* 确保与 img 对齐 */
        flex-wrap: nowrap;
        /* 防止内容换行 */
    }
}
</style>
    

_2.2、config.ts配置

去掉vp自带的搜索themeConfig.search,进行如下配置

      export default defineConfig({ 
  ...
  themeConfig: ({
    ...
      //@ts-ignore
        meilisearch: {
            host: 'http://localhost:5173/', // 服务地址(自建或云服务)
            apiKey: secureInfo.searchKey, // 搜索密钥(非管理员密钥)
            indexName: 'teek', // 索引名称
            placeholder: '搜索文档...' // 搜索框提示文字
        },
       
    ...
   })
  ...
})
    

_2.3、注册组件

插入nav-bar-content-before插槽

      <template>
  <template #nav-bar-content-before>
  	<Meilisearch />
  </template>
</template>

<script setup>
  import Meilisearch from "./components/Meilisearch.vue"; //修改为你的文件路径
</script>
    

注册好后启动项目,开始使用

_3. 索引自动化

当我们有新的文章发布时,应该重新运行抓取文章建立索引的命令,如果你的博客是通过 Github Action 进行发布的,那么官方还提供了通过 Action 自动抓取的方案。

首先在项目根目录下新建.github/workflows/crawler.yml文件,内容如下:

      name: Auto Crawler

on:
  push:
    branches: [ main ]  # 当推送到main分支时触发

jobs:
  crawler:
    runs-on: ubuntu-latest
    steps:
      - name: 运行meilisearch
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.TENCENT_CLOUD_IP }} 
          username: ${{ secrets.TENCENT_CLOUD_NAME }} 
          password: ${{ secrets.TENCENT_CLOUD_PASSWORD }} 
          script:  cd /tmp/scraper && sh teek.sh
    

如上内容需要依赖以下配置信息:

  • host:云服务器IP

  • username:云服务器登录用户名,一般为root

  • password:云服务器登录密码

    以上三个配置需到对应的github仓库填写secret

    202309242306643

  • script:登录之后执行的指令,此处就是重新爬取内容

如上内容准备完毕之后,当我们提交了新的代码,部署上去之后,就会自动运行抓取内容

声明

作者: liyao

版权:本博客所有文章除特别声明外,均采用CCBY-NC-SA4.O许可协议。转载请注明!

最后更新于 2025-10-02 15:54 history