Files
everything-is-suitable/everything-is-suitable-uniapp/src/components/TemplatePanel/index.vue
T
张翔 330c3af227 feat(i18n): 改造10个组件为i18n国际化,同步更新9种语言翻译文件
- 组件改造:EmptyState、ExportPanel、SearchConditionItem、SearchHistoryPanel、
  SearchResultCard、SearchResultList、Select、SortSwitcher、TemplatePanel、
  BottomNavigation、SearchConditionPanel
- 替换硬编码中文为 $t() 调用和 computed 响应式翻译
- 新增翻译键:search(搜索历史/模板/条件)、export(导出选项)、
  common.pleaseSelect 等
- 同步更新 zh-CN、en、zh-TW、ja、ko、es、fr、de、pt 共9种语言文件
- 重构 errorHandler:从 API 错误处理改为本地应用错误处理
- 移除 search.ts 中未使用的 SearchService 接口
2026-04-29 19:13:55 +08:00

242 lines
5.3 KiB
Vue

<template>
<Card class="template-panel">
<view class="panel-header">
<Typography variant="h4" weight="semibold">{{ $t('search.searchTemplates') }}</Typography>
<Button size="small" type="text" @click="handleRefresh">
<Icon name="refresh" size="16rpx" color="rgba(113, 113, 122, 255)" />
</Button>
</view>
<view class="search-bar">
<view class="search-input">
<Icon name="search" size="16rpx" color="rgba(113, 113, 122, 255)" />
<input
v-model="searchKeyword"
:placeholder="$t('search.searchTemplates')"
class="input-field"
/>
</view>
</view>
<view class="category-tabs">
<view
v-for="category in categories"
:key="category"
class="category-tab"
:class="{ 'category-tab--active': selectedCategory === category }"
@click="selectCategory(category)"
>
<Typography variant="body" class="category-label">{{ category }}</Typography>
</view>
</view>
<view v-if="filteredTemplates.length === 0" class="empty-templates">
<EmptyState
icon="empty"
:title="$t('search.noTemplatesTitle')"
:description="$t('search.noTemplatesDesc')"
size="small"
/>
</view>
<view v-else class="template-list">
<view
v-for="template in filteredTemplates"
:key="template.id"
class="template-item"
@click="handleSelectTemplate(template)"
>
<view class="template-content">
<Typography variant="body" weight="semibold" class="template-name">
{{ template.name }}
</Typography>
<Typography variant="caption" class="template-description">
{{ template.description }}
</Typography>
<Typography variant="caption" class="template-category">
{{ template.category }}
</Typography>
</view>
<Icon name="right" size="16rpx" color="rgba(113, 113, 122, 255)" />
</view>
</view>
</Card>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import type { SearchRequest, SearchTemplate } from '../../types/search'
import templateService from '../../services/templateService'
import Card from '../Card/Card.vue'
import Button from '../Button/Button.vue'
import Icon from '../Icon/Icon.vue'
import Typography from '../Typography/Typography.vue'
import EmptyState from '../EmptyState/index.vue'
const { t } = useI18n()
const emit = defineEmits<{
select: [condition: SearchRequest]
}>()
const searchKeyword = ref('')
const selectedCategory = ref('__all__')
const templates = ref<SearchTemplate[]>([])
const categories = computed(() => {
const allCategories = templateService.getCategories()
return [t('search.allCategories'), ...allCategories]
})
const filteredTemplates = computed(() => {
let result = templates.value
if (selectedCategory.value !== '__all__') {
result = templateService.getTemplatesByCategory(selectedCategory.value)
}
if (searchKeyword.value.trim()) {
result = templateService.searchTemplates(searchKeyword.value.trim())
}
return result
})
onMounted(() => {
loadTemplates()
})
function loadTemplates() {
templates.value = templateService.getSearchTemplates()
}
function handleRefresh() {
loadTemplates()
}
function selectCategory(category: string) {
selectedCategory.value = category
}
function handleSelectTemplate(template: SearchTemplate) {
emit('select', template.condition)
}
</script>
<style scoped lang="scss">
.template-panel {
margin-bottom: 16px;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.search-bar {
margin-bottom: 16px;
}
.search-input {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: rgba(244, 244, 245, 255);
border-radius: var(--radius-md);
}
.input-field {
flex: 1;
border: none;
background: transparent;
font-size: 14px;
color: var(--color-text);
outline: none;
&::placeholder {
color: var(--color-text-secondary);
}
}
.category-tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.category-tab {
padding: 6px 12px;
background: rgba(244, 244, 245, 255);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-fast);
&:active {
background: rgba(230, 225, 220, 255);
}
}
.category-tab--active {
background: var(--color-primary);
}
.category-label {
color: var(--color-text);
font-size: 13px;
.category-tab--active & {
color: #FFFFFF;
}
}
.empty-templates {
padding: 24px 0;
}
.template-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.template-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: rgba(244, 244, 245, 255);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
&:active {
background: rgba(230, 225, 220, 255);
}
}
.template-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.template-name {
color: var(--color-text);
}
.template-description {
color: var(--color-text-secondary);
}
.template-category {
color: var(--color-primary);
font-weight: 500;
}
</style>