feat: 添加搜索功能
- 使用meilisearch进行搜索 - 增添搜索界面
This commit is contained in:
@ -63,9 +63,9 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Search } from "@element-plus/icons-vue";
|
import { Search } from "@element-plus/icons-vue";
|
||||||
import { MeiliSearch } from "meilisearch";
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
const { setLocale } = useI18n();
|
const { setLocale } = useI18n();
|
||||||
|
|
||||||
@ -73,19 +73,16 @@ const searchQuery = ref("");
|
|||||||
|
|
||||||
const activeName = ref<string | undefined>(undefined);
|
const activeName = ref<string | undefined>(undefined);
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSearch = () => {
|
||||||
if (searchQuery.value.trim()) {
|
const trimmed = searchQuery.value.trim();
|
||||||
// 这里可以添加搜索逻辑,例如导航到搜索结果页面
|
if (!trimmed) return;
|
||||||
console.log("Searching for:", searchQuery.value);
|
navigateTo({
|
||||||
const meiliClient = new MeiliSearch({
|
path: localePath('/search'),
|
||||||
host: process.env.MEILI_HOST || "http://192.168.86.5:7700",
|
query: {
|
||||||
});
|
query: trimmed
|
||||||
const index = meiliClient.index("production");
|
}
|
||||||
const search = await index.search(searchQuery.value, {
|
})
|
||||||
limit: 20,
|
searchQuery.value = "";
|
||||||
});
|
|
||||||
console.log("Search results:", search.hits);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshMenu = () => {
|
const refreshMenu = () => {
|
||||||
|
|||||||
128
app/composables/useMeilisearch.ts
Normal file
128
app/composables/useMeilisearch.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { MeiliSearch } from "meilisearch";
|
||||||
|
import type { SearchParams, SearchResponse } from "meilisearch";
|
||||||
|
|
||||||
|
interface RawSearchSection {
|
||||||
|
indexUid: string;
|
||||||
|
response: SearchResponse<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchHit extends Record<string, unknown> {
|
||||||
|
indexUid: string;
|
||||||
|
objectID?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchSection {
|
||||||
|
indexUid: string;
|
||||||
|
hits: SearchHit[];
|
||||||
|
estimatedTotalHits: number;
|
||||||
|
processingTimeMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseIndexes = (indexes: string | string[] | undefined): string[] => {
|
||||||
|
if (!indexes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(indexes)) {
|
||||||
|
return indexes.map((item) => item.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
return indexes
|
||||||
|
.split(",")
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMeilisearch = () => {
|
||||||
|
const runtimeConfig = useRuntimeConfig();
|
||||||
|
|
||||||
|
const indexes = computed(() =>
|
||||||
|
parseIndexes(runtimeConfig.public?.meili?.indexes)
|
||||||
|
);
|
||||||
|
|
||||||
|
let meiliClient: MeiliSearch | null = null;
|
||||||
|
|
||||||
|
const ensureClient = () => {
|
||||||
|
if(meiliClient) return meiliClient;
|
||||||
|
|
||||||
|
const host = runtimeConfig.public?.meili?.host;
|
||||||
|
if (!host) {
|
||||||
|
console.warn("Meilisearch host is not configured.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const apiKey = runtimeConfig.public?.meili?.searchKey;
|
||||||
|
meiliClient = new MeiliSearch({
|
||||||
|
host,
|
||||||
|
apiKey: apiKey || undefined,
|
||||||
|
});
|
||||||
|
return meiliClient;
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = async (
|
||||||
|
query: string,
|
||||||
|
params: SearchParams = {}
|
||||||
|
): Promise<SearchSection[]> => {
|
||||||
|
const trimmedQuery = query.trim();
|
||||||
|
if (!trimmedQuery) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = ensureClient();
|
||||||
|
if (!client) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeIndexes = indexes.value;
|
||||||
|
if (!activeIndexes.length) {
|
||||||
|
console.warn("No Meilisearch indexes configured.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const requests = activeIndexes.map(async (indexUid) => {
|
||||||
|
const response = await client.index(indexUid).search(trimmedQuery, {
|
||||||
|
limit: params.limit ?? 10,
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
const safeResponse = JSON.parse(JSON.stringify(response));
|
||||||
|
return {
|
||||||
|
indexUid,
|
||||||
|
response: {
|
||||||
|
hits: safeResponse.hits,
|
||||||
|
estimatedTotalHits: safeResponse.estimatedTotalHits ?? safeResponse.hits.length,
|
||||||
|
processingTimeMs: safeResponse.processingTimeMs ?? 0,
|
||||||
|
query: safeResponse.query,
|
||||||
|
},
|
||||||
|
} satisfies RawSearchSection;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log((await requests[0])?.response.hits[0]?.locale);
|
||||||
|
|
||||||
|
const settled = await Promise.allSettled(requests);
|
||||||
|
|
||||||
|
settled
|
||||||
|
.filter((result): result is PromiseRejectedResult => result.status === "rejected")
|
||||||
|
.forEach((result) => {
|
||||||
|
console.error("Meilisearch query failed", result.reason);
|
||||||
|
});
|
||||||
|
|
||||||
|
return settled
|
||||||
|
.filter((result) => result.status === "fulfilled")
|
||||||
|
.map((result) => {
|
||||||
|
const fulfilled = result as PromiseFulfilledResult<RawSearchSection>;
|
||||||
|
return {
|
||||||
|
indexUid: fulfilled.value.indexUid,
|
||||||
|
hits: fulfilled.value.response.hits.map((hit) => ({
|
||||||
|
...hit,
|
||||||
|
indexUid: fulfilled.value.indexUid,
|
||||||
|
})),
|
||||||
|
estimatedTotalHits:
|
||||||
|
fulfilled.value.response.estimatedTotalHits ??
|
||||||
|
fulfilled.value.response.hits.length,
|
||||||
|
processingTimeMs: fulfilled.value.response.processingTimeMs ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
search,
|
||||||
|
indexes,
|
||||||
|
};
|
||||||
|
};
|
||||||
365
app/pages/search.vue
Normal file
365
app/pages/search.vue
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search-page">
|
||||||
|
<div class="search-header">
|
||||||
|
<h1 class="page-title">{{ $t("search.title") }}</h1>
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="keyword"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="$t('search-placeholder')"
|
||||||
|
:prefix-icon="Search"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="navigateToQuery(keyword)"
|
||||||
|
@input="handleInput(keyword)"
|
||||||
|
@clear="handleClear"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="navigateToQuery(keyword)">
|
||||||
|
{{ $t("search.search-button") }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<p v-if="keyword && hasResults" class="search-meta">
|
||||||
|
{{ $t("search.results-for", { query: keyword }) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="search-state">
|
||||||
|
<el-skeleton :rows="4" animated />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="hasResults" class="search-results">
|
||||||
|
<section
|
||||||
|
v-for="section in filteredSections"
|
||||||
|
:key="section.indexUid"
|
||||||
|
class="search-section"
|
||||||
|
>
|
||||||
|
<header class="section-header">
|
||||||
|
<h2 class="section-title">
|
||||||
|
{{ getIndexLabel(section.indexUid) }}
|
||||||
|
<span class="section-count">{{
|
||||||
|
$t("search.result-count", { count: section.estimatedTotalHits })
|
||||||
|
}}</span>
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
<div class="section-results">
|
||||||
|
<el-card
|
||||||
|
v-for="(hit, hitIndex) in section.hits"
|
||||||
|
:key="`${section.indexUid}-${getHitIdentifier(hit, hitIndex)}`"
|
||||||
|
class="result-card"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
v-if="resolveHitLink(hit)"
|
||||||
|
:to="resolveHitLink(hit) || ''"
|
||||||
|
class="result-title"
|
||||||
|
>
|
||||||
|
{{ getHitTitle(hit) }}
|
||||||
|
</NuxtLink>
|
||||||
|
<h3 v-else class="result-title">{{ getHitTitle(hit) }}</h3>
|
||||||
|
<p v-if="getHitSummary(hit)" class="result-summary">
|
||||||
|
{{ getHitSummary(hit) }}
|
||||||
|
</p>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<div v-else class="search-state">
|
||||||
|
<el-empty
|
||||||
|
:description="
|
||||||
|
keyword
|
||||||
|
? $t('search.no-results', { query: keyword })
|
||||||
|
: $t('search.no-query')
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Search } from "@element-plus/icons-vue";
|
||||||
|
import type { SearchHit, SearchSection } from "~/composables/useMeilisearch";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const localePath = useLocalePath();
|
||||||
|
|
||||||
|
const { getStrapiLocale } = useLocalizations();
|
||||||
|
const strapiLocale = getStrapiLocale();
|
||||||
|
|
||||||
|
const { search } = useMeilisearch();
|
||||||
|
|
||||||
|
const loading = ref(true);
|
||||||
|
const keyword = ref("");
|
||||||
|
const sections = ref<SearchSection[]>([]);
|
||||||
|
const localeSections = computed(() =>
|
||||||
|
sections.value.map((section) => ({
|
||||||
|
...section,
|
||||||
|
hits: section.hits.filter(
|
||||||
|
(hit) =>
|
||||||
|
!hit.locale ||
|
||||||
|
String(hit.locale).toLowerCase() === strapiLocale.toLowerCase()
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const filteredSections = computed(() =>
|
||||||
|
localeSections.value.filter((section) => section.hits.length > 0)
|
||||||
|
);
|
||||||
|
const activeRequestId = ref(0);
|
||||||
|
|
||||||
|
const indexLabels = computed<Record<string, string>>(() => ({
|
||||||
|
production: t("search.sections.production"),
|
||||||
|
solution: t("search.sections.solution"),
|
||||||
|
support: t("search.sections.support"),
|
||||||
|
default: t("search.sections.default"),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const getIndexLabel = (indexUid: string) =>
|
||||||
|
indexLabels.value[indexUid] || indexLabels.value.default;
|
||||||
|
|
||||||
|
const hasResults = computed(() =>
|
||||||
|
localeSections.value.some((section) => section.hits.length > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const getHitIdentifier = (hit: SearchHit, index: number) => {
|
||||||
|
const candidate = [hit.objectID, hit.documentId, hit.id, hit.slug].find(
|
||||||
|
(value) =>
|
||||||
|
["string", "number"].includes(typeof value) && String(value).length > 0
|
||||||
|
);
|
||||||
|
return candidate != null ? String(candidate) : String(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHitTitle = (hit: SearchHit) => {
|
||||||
|
const candidate = [hit.title, hit.name, hit.heading, hit.documentTitle].find(
|
||||||
|
(value) => typeof value === "string" && value.trim().length > 0
|
||||||
|
);
|
||||||
|
return candidate ? String(candidate) : t("search.untitled");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHitSummary = (hit: SearchHit) => {
|
||||||
|
const candidate = [
|
||||||
|
hit.summary,
|
||||||
|
hit.description,
|
||||||
|
hit.snippet,
|
||||||
|
hit.content,
|
||||||
|
hit.text,
|
||||||
|
].find((value) => typeof value === "string" && value.trim().length > 0);
|
||||||
|
return candidate ? String(candidate) : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveHitLink = (hit: SearchHit) => {
|
||||||
|
if (typeof hit.route === "string" && hit.route.trim().length > 0) {
|
||||||
|
return localePath(hit.route);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugCandidate = [hit.slug, hit.documentId, hit.id, hit.objectID].find(
|
||||||
|
(value) =>
|
||||||
|
["string", "number"].includes(typeof value) && String(value).length > 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!slugCandidate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = String(slugCandidate);
|
||||||
|
|
||||||
|
if (hit.indexUid === "production") {
|
||||||
|
return localePath({ path: `/productions/${slug}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit.indexUid === "solution") {
|
||||||
|
return localePath({ path: `/solutions/${slug}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateToQuery = (value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
navigateTo({
|
||||||
|
path: localePath("/search"),
|
||||||
|
query: { query: trimmed },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const performSearch = async (value: string) => {
|
||||||
|
activeRequestId.value += 1;
|
||||||
|
const requestId = activeRequestId.value;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
if (requestId === activeRequestId.value) {
|
||||||
|
sections.value = [];
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const results = await search(trimmed, { limit: 12 });
|
||||||
|
if (requestId === activeRequestId.value) {
|
||||||
|
sections.value = results;
|
||||||
|
}
|
||||||
|
console.log(results);
|
||||||
|
console.log(localeSections.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to perform search", error);
|
||||||
|
if (requestId === activeRequestId.value) {
|
||||||
|
sections.value = [];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === activeRequestId.value) {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function debounce<T extends (...args: never[]) => void>(fn: T, delay: number): (...args: Parameters<T>) => void {
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
return function (this: never, ...args: Parameters<T>) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
fn.apply(this, args);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = debounce((value: string) => {
|
||||||
|
performSearch(value);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
keyword.value = "";
|
||||||
|
sections.value = [];
|
||||||
|
router.replace(localePath({ path: "/search" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (typeof route.query.query === "string" && route.query.query.trim()) {
|
||||||
|
keyword.value = route.query.query;
|
||||||
|
performSearch(route.query.query);
|
||||||
|
} else {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead(() => ({
|
||||||
|
title: t("search.head-title"),
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-page {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 960px;
|
||||||
|
padding: 2.5rem 1.5rem 3rem;
|
||||||
|
min-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-meta {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-state {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-count {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-results {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card:hover {
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-summary {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.search-page {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -2,6 +2,22 @@
|
|||||||
"back": "Back",
|
"back": "Back",
|
||||||
"not-found": "Page Not Found",
|
"not-found": "Page Not Found",
|
||||||
"search-placeholder": "Search...",
|
"search-placeholder": "Search...",
|
||||||
|
"search": {
|
||||||
|
"title": "Search",
|
||||||
|
"head-title": "Search",
|
||||||
|
"search-button": "Search",
|
||||||
|
"results-for": "Results for \"{query}\"",
|
||||||
|
"result-count": "{count} results",
|
||||||
|
"no-results": "No results found for \"{query}\".",
|
||||||
|
"no-query": "Enter a keyword to start searching.",
|
||||||
|
"untitled": "Untitled",
|
||||||
|
"sections": {
|
||||||
|
"production": "Products",
|
||||||
|
"solution": "Solutions",
|
||||||
|
"support": "Support",
|
||||||
|
"default": "Other"
|
||||||
|
}
|
||||||
|
},
|
||||||
"company-name": "Jinshen Machinary Manufacturing Co., Ltd.",
|
"company-name": "Jinshen Machinary Manufacturing Co., Ltd.",
|
||||||
"company-description": "We specialize in manufacturing a range of paper tube and can equipment, integrating design, manufacturing, sales, and service.",
|
"company-description": "We specialize in manufacturing a range of paper tube and can equipment, integrating design, manufacturing, sales, and service.",
|
||||||
"learn-more": "Learn More",
|
"learn-more": "Learn More",
|
||||||
|
|||||||
@ -2,6 +2,22 @@
|
|||||||
"back": "返回",
|
"back": "返回",
|
||||||
"not-found": "页面不存在",
|
"not-found": "页面不存在",
|
||||||
"search-placeholder": "搜索...",
|
"search-placeholder": "搜索...",
|
||||||
|
"search": {
|
||||||
|
"title": "站内搜索",
|
||||||
|
"head-title": "搜索",
|
||||||
|
"search-button": "搜索",
|
||||||
|
"results-for": "“{query}” 的搜索结果",
|
||||||
|
"result-count": "共 {count} 条结果",
|
||||||
|
"no-results": "没有找到与 “{query}” 匹配的内容。",
|
||||||
|
"no-query": "请输入关键字开始搜索。",
|
||||||
|
"untitled": "未命名条目",
|
||||||
|
"sections": {
|
||||||
|
"production": "产品",
|
||||||
|
"solution": "解决方案",
|
||||||
|
"support": "服务支持",
|
||||||
|
"default": "其他内容"
|
||||||
|
}
|
||||||
|
},
|
||||||
"company-name": "金申机械制造有限公司",
|
"company-name": "金申机械制造有限公司",
|
||||||
"company-description": "专业生产一系列纸管、纸罐设备,集设计、制造、销售、服务于一体。",
|
"company-description": "专业生产一系列纸管、纸罐设备,集设计、制造、销售、服务于一体。",
|
||||||
"learn-more": "了解更多",
|
"learn-more": "了解更多",
|
||||||
|
|||||||
@ -18,6 +18,20 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
meili: {
|
||||||
|
host: process.env.MEILI_HOST || "http://localhost:7700",
|
||||||
|
searchKey: process.env.MEILI_SEARCH_KEY || "",
|
||||||
|
indexes: process.env.MEILI_SEARCH_INDEXES
|
||||||
|
? (typeof process.env.MEILI_SEARCH_INDEXES === "string"
|
||||||
|
? process.env.MEILI_SEARCH_INDEXES.split(",").map(i => i.trim())
|
||||||
|
: process.env.MEILI_SEARCH_INDEXES)
|
||||||
|
: ["production", "solution"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
fonts: {
|
fonts: {
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user