feat(i18n): add full-stack internationalization support
Add languages and entity_translations tables to database schema Implement localized queries and translation management in backend Integrate frontend i18n and add translation UI components
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import EditMeta from '../components/EditMeta.vue';
|
||||
import EntityCard from '../components/EntityCard.vue';
|
||||
import FilterPanel from '../components/FilterPanel.vue';
|
||||
@@ -10,6 +11,7 @@ import TagsSelect from '../components/TagsSelect.vue';
|
||||
import { api, type Item, type Options } from '../services/api';
|
||||
|
||||
const options = ref<Options | null>(null);
|
||||
const { t } = useI18n();
|
||||
const items = ref<Item[]>([]);
|
||||
const loading = ref(true);
|
||||
const search = ref('');
|
||||
@@ -22,7 +24,7 @@ const filterSkeletonWidths = ['52px', '48px', '48px'];
|
||||
const skeletonCardCount = 6;
|
||||
|
||||
const categoryTabs = computed<TabOption[]>(() => [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '', label: t('common.all') },
|
||||
...(options.value?.itemCategories.map((item) => ({ value: String(item.id), label: item.name })) ?? [])
|
||||
]);
|
||||
|
||||
@@ -69,14 +71,14 @@ watch(itemQuery, loadItems);
|
||||
|
||||
<template>
|
||||
<section class="page-stack">
|
||||
<PageHeader title="材料单" subtitle="按分类、用途、标签查看材料单。">
|
||||
<PageHeader :title="t('pages.recipes.title')" :subtitle="t('pages.recipes.subtitle')">
|
||||
<template #kicker>Recipes</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/recipes/new">新增</RouterLink>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/recipes/new">{{ t('common.add') }}</RouterLink>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<Tabs v-if="options" id="recipe-category" v-model="categoryId" :tabs="categoryTabs" label="分类" />
|
||||
<Tabs v-if="options" id="recipe-category" v-model="categoryId" :tabs="categoryTabs" :label="t('pages.items.category')" />
|
||||
<div v-else class="tabs tabs--component" aria-hidden="true">
|
||||
<div class="tab-list tab-list--skeleton">
|
||||
<Skeleton
|
||||
@@ -92,25 +94,25 @@ watch(itemQuery, loadItems);
|
||||
|
||||
<FilterPanel v-if="options">
|
||||
<div class="field">
|
||||
<label for="recipe-search">搜索</label>
|
||||
<input id="recipe-search" v-model="search" type="search" placeholder="名称" />
|
||||
<label for="recipe-search">{{ t('common.search') }}</label>
|
||||
<input id="recipe-search" v-model="search" type="search" :placeholder="t('common.name')" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="recipe-usage">用途</label>
|
||||
<label for="recipe-usage">{{ t('pages.items.usage') }}</label>
|
||||
<TagsSelect
|
||||
id="recipe-usage"
|
||||
v-model="usageId"
|
||||
:options="options.itemUsages"
|
||||
:multiple="false"
|
||||
placeholder="全部"
|
||||
search-placeholder="搜索用途"
|
||||
:placeholder="t('common.all')"
|
||||
:search-placeholder="t('pages.items.searchUsage')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="recipe-tags">标签</label>
|
||||
<TagsSelect id="recipe-tags" v-model="tagIds" :options="options.itemTags" placeholder="搜索标签" />
|
||||
<label for="recipe-tags">{{ t('pages.items.tags') }}</label>
|
||||
<TagsSelect id="recipe-tags" v-model="tagIds" :options="options.itemTags" :placeholder="t('pages.items.searchTags')" />
|
||||
</div>
|
||||
</FilterPanel>
|
||||
<FilterPanel v-else class="filter-panel--skeleton" aria-hidden="true">
|
||||
@@ -120,7 +122,7 @@ watch(itemQuery, loadItems);
|
||||
</div>
|
||||
</FilterPanel>
|
||||
|
||||
<div v-if="loading" class="entity-grid" aria-busy="true" aria-label="正在加载材料单列表">
|
||||
<div v-if="loading" class="entity-grid" aria-busy="true" :aria-label="t('pages.recipes.loadingList')">
|
||||
<article v-for="index in skeletonCardCount" :key="`recipe-skeleton-${index}`" class="entity-card entity-card--skeleton">
|
||||
<Skeleton variant="box" width="42px" height="42px" class="skeleton-entity-mark" />
|
||||
<div class="entity-card__content">
|
||||
@@ -142,7 +144,7 @@ watch(itemQuery, loadItems);
|
||||
>
|
||||
<EditMeta v-if="item.recipe" :entity="item.recipe" />
|
||||
<RouterLink v-else-if="!item.noRecipe" class="ui-button ui-button--primary ui-button--small" :to="createRecipeTarget(item)">
|
||||
创建材料单
|
||||
{{ t('pages.items.createRecipe') }}
|
||||
</RouterLink>
|
||||
</EntityCard>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user