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 接口
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<view class="bottom-navigation">
|
||||
<view
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:class="['nav-item', { 'nav-item--active': currentTab === tab.key }]"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<Icon
|
||||
:name="tab.icon"
|
||||
:size="'22rpx'"
|
||||
:color="currentTab === tab.key ? 'rgba(196, 30, 58, 255)' : 'rgba(113, 113, 122, 255)'"
|
||||
/>
|
||||
<text :class="['nav-label', { 'nav-label--active': currentTab === tab.key }]">
|
||||
{{ tab.label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '../Icon/Icon.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Tab {
|
||||
key: string
|
||||
label: string
|
||||
icon: string
|
||||
path: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
currentTab: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
currentTab: 'almanac'
|
||||
})
|
||||
|
||||
const tabs = computed<Tab[]>(() => [
|
||||
{ key: 'almanac', label: t('nav.almanac'), icon: 'calendar', path: '/pages/almanac-search/index' },
|
||||
{ key: 'ziwei', label: t('nav.ziwei'), icon: 'star', path: '/pages/ziwei/index' },
|
||||
{ key: 'fortune', label: t('nav.fortune'), icon: 'heart', path: '/pages/fortune/index' }
|
||||
])
|
||||
|
||||
const switchTab = (key: string) => {
|
||||
const tab = tabs.find(t => t.key === key)
|
||||
if (tab) {
|
||||
uni.navigateTo({ url: tab.path })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bottom-navigation {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 56px;
|
||||
background-color: rgba(255, 255, 255, 255);
|
||||
border-top: 1px solid rgba(230, 225, 220, 255);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 6px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
color: rgba(113, 113, 122, 255);
|
||||
font-weight: 400;
|
||||
|
||||
&--active {
|
||||
color: rgba(196, 30, 58, 255);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 375px) {
|
||||
.bottom-navigation {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<view
|
||||
:class="[
|
||||
'button',
|
||||
`button--${type}`,
|
||||
`button--${size}`,
|
||||
{
|
||||
'button--block': block,
|
||||
'button--disabled': disabled,
|
||||
'button--loading': loading
|
||||
}
|
||||
]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<view v-if="loading" class="button-spinner"></view>
|
||||
<Icon v-if="icon && !loading" :name="icon" :size="iconSize" :color="iconColor" />
|
||||
<text class="button-text"><slot /></text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Icon from '../Icon/Icon.vue'
|
||||
|
||||
interface Props {
|
||||
type?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
block?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
icon?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'primary',
|
||||
size: 'medium',
|
||||
block: false,
|
||||
disabled: false,
|
||||
loading: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const iconSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'small': return '14rpx'
|
||||
case 'large': return '22rpx'
|
||||
default: return '18rpx'
|
||||
}
|
||||
})
|
||||
|
||||
const iconColor = computed(() => {
|
||||
if (props.type === 'primary') return '#ffffff'
|
||||
return 'rgba(44, 24, 16, 255)'
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.disabled && !props.loading) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
|
||||
&--small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
&--large {
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
&--block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
background-color: rgba(196, 30, 58, 255);
|
||||
color: #ffffff;
|
||||
|
||||
&:active:not(.button--disabled):not(.button--loading) {
|
||||
background-color: rgba(170, 20, 45, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.button--secondary {
|
||||
background-color: rgba(244, 244, 245, 255);
|
||||
color: rgba(44, 24, 16, 255);
|
||||
border-color: rgba(230, 225, 220, 255);
|
||||
|
||||
&:active:not(.button--disabled):not(.button--loading) {
|
||||
background-color: rgba(230, 225, 220, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.button--outline {
|
||||
background-color: transparent;
|
||||
color: rgba(196, 30, 58, 255);
|
||||
border-color: rgba(196, 30, 58, 255);
|
||||
|
||||
&:active:not(.button--disabled):not(.button--loading) {
|
||||
background-color: rgba(196, 30, 58, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.button--ghost {
|
||||
background-color: transparent;
|
||||
color: rgba(44, 24, 16, 255);
|
||||
|
||||
&:active:not(.button--disabled):not(.button--loading) {
|
||||
background-color: rgba(244, 244, 245, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.button--danger {
|
||||
background-color: rgba(220, 38, 38, 255);
|
||||
color: #ffffff;
|
||||
|
||||
&:active:not(.button--disabled):not(.button--loading) {
|
||||
background-color: rgba(185, 28, 28, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.button-text {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.button-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid #ffffff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.button--secondary .button-spinner,
|
||||
.button--ghost .button-spinner {
|
||||
border-color: rgba(44, 24, 16, 0.2);
|
||||
border-top-color: rgba(44, 24, 16, 0.8);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 375px) {
|
||||
.button--small {
|
||||
padding: 5px 10px;
|
||||
font-size: 11px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.button--medium {
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.button--large {
|
||||
padding: 10px 20px;
|
||||
font-size: 15px;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<view :class="['card', { 'card--clickable': clickable, 'card--bordered': bordered }]" @click="handleClick">
|
||||
<view v-if="$slots.header" class="card-header">
|
||||
<slot name="header" />
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<slot />
|
||||
</view>
|
||||
<view v-if="$slots.footer" class="card-footer">
|
||||
<slot name="footer" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
clickable?: boolean
|
||||
bordered?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
clickable: false,
|
||||
bordered: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const handleClick = () => {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.card {
|
||||
background-color: rgba(255, 255, 255, 255);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&--bordered {
|
||||
border: 1px solid rgba(230, 225, 220, 255);
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
background-color: rgba(244, 244, 245, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgba(230, 225, 220, 255);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid rgba(230, 225, 220, 255);
|
||||
background-color: rgba(244, 244, 245, 0.3);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 375px) {
|
||||
.card-header {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,23 @@
|
||||
<template>
|
||||
<view class="empty-state">
|
||||
<Icon :name="iconName" :size="iconSize" color="rgba(200, 200, 200, 255)" />
|
||||
<Typography variant="h4" weight="semibold" class="empty-title">{{ title }}</Typography>
|
||||
<Typography variant="body" class="empty-description">{{ description }}</Typography>
|
||||
<Typography variant="h4" weight="semibold" class="empty-title">{{ displayTitle }}</Typography>
|
||||
<Typography variant="body" class="empty-description">{{ displayDescription }}</Typography>
|
||||
<Button v-if="showAction" type="primary" @click="handleAction">
|
||||
{{ actionText }}
|
||||
{{ displayActionText }}
|
||||
</Button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '../Icon/Icon.vue'
|
||||
import Typography from '../Typography/Typography.vue'
|
||||
import Button from '../Button/Button.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
icon?: string
|
||||
title?: string
|
||||
@@ -26,13 +29,17 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
icon: 'empty',
|
||||
title: '暂无数据',
|
||||
description: '没有找到相关结果',
|
||||
title: undefined,
|
||||
description: undefined,
|
||||
showAction: true,
|
||||
actionText: '重新搜索',
|
||||
actionText: undefined,
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const displayTitle = computed(() => props.title ?? t('common.noData'))
|
||||
const displayDescription = computed(() => props.description ?? t('common.noResult'))
|
||||
const displayActionText = computed(() => props.actionText ?? t('common.retry'))
|
||||
|
||||
const emit = defineEmits<{
|
||||
action: []
|
||||
}>()
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
@click="handleExport"
|
||||
>
|
||||
<Icon name="download" size="16rpx" color="#FFFFFF" />
|
||||
<Typography variant="body" color="#FFFFFF">导出结果</Typography>
|
||||
<Typography variant="body" color="#FFFFFF">{{ $t('export.exportResults') }}</Typography>
|
||||
</Button>
|
||||
|
||||
<view v-if="showExportModal" class="export-modal">
|
||||
<view class="modal-content">
|
||||
<view class="modal-header">
|
||||
<Typography variant="h4" weight="semibold">导出搜索结果</Typography>
|
||||
<Typography variant="h4" weight="semibold">{{ $t('export.exportSearchResults') }}</Typography>
|
||||
<Button size="small" type="text" @click="closeModal">
|
||||
<Icon name="close" size="16rpx" color="rgba(113, 113, 122, 255)" />
|
||||
</Button>
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
<view class="modal-body">
|
||||
<view class="export-options">
|
||||
<Typography variant="body" class="option-title">选择导出格式</Typography>
|
||||
<Typography variant="body" class="option-title">{{ $t('export.selectFormat') }}</Typography>
|
||||
<view class="format-options">
|
||||
<view
|
||||
v-for="format in exportFormats"
|
||||
@@ -40,7 +40,7 @@
|
||||
</view>
|
||||
|
||||
<view class="export-options">
|
||||
<Typography variant="body" class="option-title">选择导出内容</Typography>
|
||||
<Typography variant="body" class="option-title">{{ $t('export.selectContent') }}</Typography>
|
||||
<view class="content-options">
|
||||
<view
|
||||
v-for="option in contentOptions"
|
||||
@@ -61,8 +61,8 @@
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<Button type="text" @click="closeModal">取消</Button>
|
||||
<Button type="primary" @click="confirmExport">导出</Button>
|
||||
<Button type="text" @click="closeModal">{{ $t('common.cancel') }}</Button>
|
||||
<Button type="primary" @click="confirmExport">{{ $t('export.export') }}</Button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -70,7 +70,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SearchResult } from '../../types/search'
|
||||
import Button from '../Button/Button.vue'
|
||||
import Icon from '../Icon/Icon.vue'
|
||||
@@ -86,6 +87,8 @@ const emit = defineEmits<{
|
||||
export: [format: string, content: string[]]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const showExportModal = ref(false)
|
||||
const selectedFormat = ref('excel')
|
||||
const selectedContent = ref(['date', 'lunarDate', 'matchedItems'])
|
||||
@@ -96,13 +99,13 @@ const exportFormats = [
|
||||
{ label: 'CSV (.csv)', value: 'csv' }
|
||||
]
|
||||
|
||||
const contentOptions = [
|
||||
{ label: '日期', value: 'date' },
|
||||
{ label: '农历日期', value: 'lunarDate' },
|
||||
{ label: '星期', value: 'weekday' },
|
||||
{ label: '匹配事项', value: 'matchedItems' },
|
||||
{ label: '匹配度', value: 'matchCount' }
|
||||
]
|
||||
const contentOptions = computed(() => [
|
||||
{ label: t('export.contentDate'), value: 'date' },
|
||||
{ label: t('export.contentLunarDate'), value: 'lunarDate' },
|
||||
{ label: t('export.contentWeekday'), value: 'weekday' },
|
||||
{ label: t('export.contentMatchedItems'), value: 'matchedItems' },
|
||||
{ label: t('export.contentMatchCount'), value: 'matchCount' }
|
||||
])
|
||||
|
||||
function handleExport() {
|
||||
showExportModal.value = true
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="icon-wrapper" :style="iconStyle" @click="$emit('click')">
|
||||
<text class="icon-text" :style="textStyle">{{ iconChar }}</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
size?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: '20rpx',
|
||||
color: 'rgba(44, 24, 16, 255)'
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const iconMap: Record<string, string> = {
|
||||
'arrow-up': '↑',
|
||||
'arrow-down': '↓',
|
||||
'arrow-left': '←',
|
||||
'arrow-right': '→',
|
||||
'check': '✓',
|
||||
'close': '✕',
|
||||
'plus': '+',
|
||||
'minus': '−',
|
||||
'search': '🔍',
|
||||
'empty': '📭',
|
||||
'calendar': '📅',
|
||||
'star': '★',
|
||||
'heart': '♥',
|
||||
'settings': '⚙',
|
||||
'home': '🏠',
|
||||
'user': '👤',
|
||||
'info': 'ℹ',
|
||||
'warning': '⚠',
|
||||
'error': '✕',
|
||||
'success': '✓',
|
||||
'refresh': '↻',
|
||||
'download': '⬇',
|
||||
'upload': '⬆',
|
||||
'share': '↗',
|
||||
'copy': '📋',
|
||||
'delete': '🗑',
|
||||
'edit': '✎',
|
||||
'filter': '⊞',
|
||||
'sort': '⇅',
|
||||
'export': '📤',
|
||||
'import': '📥',
|
||||
'history': '🕐',
|
||||
'bookmark': '🔖',
|
||||
'chevron-up': '‹',
|
||||
'chevron-down': '›',
|
||||
'chevron-left': '‹',
|
||||
'chevron-right': '›'
|
||||
}
|
||||
|
||||
const iconChar = computed(() => {
|
||||
return iconMap[props.name] || props.name.charAt(0).toUpperCase()
|
||||
})
|
||||
|
||||
const iconStyle = computed(() => ({
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: props.size,
|
||||
height: props.size
|
||||
}))
|
||||
|
||||
const textStyle = computed(() => ({
|
||||
fontSize: props.size,
|
||||
color: props.color,
|
||||
lineHeight: '1'
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-text {
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<view class="condition-body">
|
||||
<view class="condition-section">
|
||||
<Typography variant="caption" class="section-label">事项</Typography>
|
||||
<Typography variant="caption" class="section-label">{{ $t('search.activityItems') }}</Typography>
|
||||
<view class="items-selector">
|
||||
<view
|
||||
v-for="item in availableItems"
|
||||
@@ -28,7 +28,7 @@
|
||||
</view>
|
||||
|
||||
<view class="condition-section">
|
||||
<Typography variant="caption" class="section-label">逻辑运算符</Typography>
|
||||
<Typography variant="caption" class="section-label">{{ $t('search.logicalOperator') }}</Typography>
|
||||
<Select
|
||||
v-model="localCondition.operator"
|
||||
:options="operatorOptions"
|
||||
@@ -43,7 +43,7 @@
|
||||
size="20rpx"
|
||||
:color="localCondition.exclude ? 'rgba(196, 30, 58, 255)' : 'rgba(113, 113, 122, 255)'"
|
||||
/>
|
||||
<Typography variant="body" class="exclude-label">排除条件</Typography>
|
||||
<Typography variant="body" class="exclude-label">{{ $t('search.excludeCondition') }}</Typography>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SearchCondition } from '../../types/search'
|
||||
import Card from '../Card/Card.vue'
|
||||
import Button from '../Button/Button.vue'
|
||||
@@ -70,17 +71,19 @@ const emit = defineEmits<{
|
||||
delete: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const localCondition = ref<SearchCondition>({ ...props.condition })
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '宜', value: 'suitable' },
|
||||
{ label: '忌', value: 'unsuitable' }
|
||||
]
|
||||
const typeOptions = computed(() => [
|
||||
{ label: t('search.suitable'), value: 'suitable' },
|
||||
{ label: t('search.unsuitable'), value: 'unsuitable' }
|
||||
])
|
||||
|
||||
const operatorOptions = [
|
||||
{ label: '与(AND)', value: 'and' },
|
||||
{ label: '或(OR)', value: 'or' }
|
||||
]
|
||||
const operatorOptions = computed(() => [
|
||||
{ label: t('search.operatorAnd'), value: 'and' },
|
||||
{ label: t('search.operatorOr'), value: 'or' }
|
||||
])
|
||||
|
||||
const suitableActivities = [
|
||||
'祭祀', '祈福', '求嗣', '开光', '出行', '嫁娶', '订盟', '纳采', '裁衣', '安床',
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<Card class="search-condition-panel">
|
||||
<view class="panel-header">
|
||||
<Typography variant="h4" weight="semibold" class="panel-title">{{ $t('search.searchConditions') }}</Typography>
|
||||
<Button size="small" type="primary" icon="plus" @click="addCondition">{{ $t('search.addCondition') }}</Button>
|
||||
</view>
|
||||
|
||||
<view v-if="conditions.length === 0" class="empty-conditions">
|
||||
<Typography variant="body" class="empty-text">{{ $t('search.addConditionHint') }}</Typography>
|
||||
</view>
|
||||
|
||||
<view v-else class="conditions-list">
|
||||
<SearchConditionItem
|
||||
v-for="(condition, index) in conditions"
|
||||
:key="condition.id"
|
||||
:condition="condition"
|
||||
@update="updateCondition(index, $event)"
|
||||
@delete="removeCondition(index)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="days-selector">
|
||||
<Typography variant="body" class="days-label">{{ $t('search.searchDays') }}</Typography>
|
||||
<view class="days-options">
|
||||
<view
|
||||
v-for="dayOption in dayOptions"
|
||||
:key="dayOption"
|
||||
:class="['day-tag', { 'day-tag-selected': days === dayOption }]"
|
||||
@click="updateDays(dayOption)"
|
||||
>
|
||||
<Typography variant="caption" class="day-text">{{ $t('search.daysUnit', { count: dayOption }) }}</Typography>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-footer">
|
||||
<Button type="primary" block @click="handleSearch">{{ $t('common.search') }}</Button>
|
||||
</view>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { SearchCondition } from '../../types/search'
|
||||
import Card from '../Card/Card.vue'
|
||||
import Button from '../Button/Button.vue'
|
||||
import Typography from '../Typography/Typography.vue'
|
||||
import SearchConditionItem from '../SearchConditionItem/index.vue'
|
||||
|
||||
interface Props {
|
||||
conditions: SearchCondition[]
|
||||
days: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:conditions': [conditions: SearchCondition[]]
|
||||
'update:days': [days: number]
|
||||
search: []
|
||||
}>()
|
||||
|
||||
const dayOptions = [7, 15, 30, 60, 90]
|
||||
|
||||
const addCondition = () => {
|
||||
const newCondition: SearchCondition = {
|
||||
id: Date.now().toString(),
|
||||
type: 'suitable',
|
||||
items: [],
|
||||
operator: 'and',
|
||||
exclude: false
|
||||
}
|
||||
const updated = [...props.conditions, newCondition]
|
||||
emit('update:conditions', updated)
|
||||
}
|
||||
|
||||
const updateCondition = (index: number, condition: SearchCondition) => {
|
||||
const updated = [...props.conditions]
|
||||
updated[index] = condition
|
||||
emit('update:conditions', updated)
|
||||
}
|
||||
|
||||
const removeCondition = (index: number) => {
|
||||
const updated = props.conditions.filter((_, i) => i !== index)
|
||||
emit('update:conditions', updated)
|
||||
}
|
||||
|
||||
const updateDays = (days: number) => {
|
||||
emit('update:days', days)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('search')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-condition-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
color: rgba(44, 24, 16, 255);
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-conditions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: rgba(113, 113, 122, 255);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conditions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.days-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.days-label {
|
||||
color: rgba(44, 24, 16, 255);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.days-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.day-tag {
|
||||
padding: 6px 14px;
|
||||
background-color: rgba(244, 244, 245, 255);
|
||||
border: 1px solid rgba(230, 225, 220, 255);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
background-color: rgba(230, 225, 220, 255);
|
||||
}
|
||||
}
|
||||
|
||||
.day-tag-selected {
|
||||
background-color: rgba(220, 252, 231, 1);
|
||||
border-color: rgba(185, 248, 207, 1);
|
||||
}
|
||||
|
||||
.day-text {
|
||||
color: rgba(44, 24, 16, 255);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 375px) {
|
||||
.panel-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.conditions-list {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.days-options {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.day-tag {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<Card class="search-history-panel">
|
||||
<view class="panel-header">
|
||||
<Typography variant="h4" weight="semibold">搜索历史</Typography>
|
||||
<Typography variant="h4" weight="semibold">{{ $t('search.searchHistory') }}</Typography>
|
||||
<Button size="small" type="text" @click="handleClearHistory">
|
||||
<Typography variant="caption" color="rgba(196, 30, 58, 255)">清除历史</Typography>
|
||||
<Typography variant="caption" color="rgba(196, 30, 58, 255)">{{ $t('search.clearHistory') }}</Typography>
|
||||
</Button>
|
||||
</view>
|
||||
|
||||
<view v-if="history.length === 0" class="empty-history">
|
||||
<EmptyState
|
||||
icon="empty"
|
||||
title="暂无搜索历史"
|
||||
description="执行搜索后,搜索条件将显示在这里"
|
||||
:title="$t('search.noHistoryTitle')"
|
||||
:description="$t('search.noHistoryDesc')"
|
||||
size="small"
|
||||
/>
|
||||
</view>
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SearchRequest } from '../../types/search'
|
||||
import searchService from '../../services/searchService'
|
||||
import Card from '../Card/Card.vue'
|
||||
@@ -53,6 +54,8 @@ interface HistoryItem {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [condition: SearchRequest]
|
||||
}>()
|
||||
@@ -73,8 +76,8 @@ function handleSelectHistory(item: HistoryItem) {
|
||||
|
||||
function handleClearHistory() {
|
||||
uni.showModal({
|
||||
title: '确认清除',
|
||||
content: '确定要清除所有搜索历史吗?',
|
||||
title: t('search.confirmClear'),
|
||||
content: t('search.confirmClearContent'),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
searchService.clearSearchHistory()
|
||||
@@ -88,11 +91,11 @@ function formatHistoryTitle(condition: SearchRequest): string {
|
||||
const conditionCount = condition.conditions.length
|
||||
const days = condition.days
|
||||
|
||||
let title = `${conditionCount}个条件,${days}天范围`
|
||||
let title = t('search.historyConditionDays', { count: conditionCount, days })
|
||||
|
||||
if (conditionCount > 0) {
|
||||
const firstCondition = condition.conditions[0]
|
||||
const type = firstCondition.type === 'suitable' ? '宜' : '忌'
|
||||
const type = firstCondition.type === 'suitable' ? t('search.suitable') : t('search.unsuitable')
|
||||
const items = firstCondition.items.slice(0, 2).join('、')
|
||||
|
||||
title += `(${type}${items}${firstCondition.items.length > 2 ? '...' : ''})`
|
||||
@@ -108,13 +111,13 @@ function formatTime(timeStr: string): string {
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||||
|
||||
if (diffHours < 1) {
|
||||
return '刚刚'
|
||||
return t('search.justNow')
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}小时前`
|
||||
return t('search.hoursAgo', { count: diffHours })
|
||||
} else {
|
||||
const diffDays = Math.floor(diffHours / 24)
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}天前`
|
||||
return t('search.daysAgo', { count: diffDays })
|
||||
} else {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
@@ -9,21 +9,22 @@
|
||||
<Typography variant="caption" class="lunar-date">{{ result.lunarDate }}</Typography>
|
||||
|
||||
<view v-if="result.matchedItems.suitable.length > 0" class="matched-items">
|
||||
<Typography variant="caption" class="label suitable-label">宜</Typography>
|
||||
<Typography variant="caption" class="label suitable-label">{{ $t('search.suitable') }}</Typography>
|
||||
<Typography variant="body" class="items suitable-items">{{ result.matchedItems.suitable.join('、') }}</Typography>
|
||||
</view>
|
||||
|
||||
<view v-if="result.matchedItems.unsuitable.length > 0" class="matched-items">
|
||||
<Typography variant="caption" class="label unsuitable-label">忌</Typography>
|
||||
<Typography variant="caption" class="label unsuitable-label">{{ $t('search.unsuitable') }}</Typography>
|
||||
<Typography variant="body" class="items unsuitable-items">{{ result.matchedItems.unsuitable.join('、') }}</Typography>
|
||||
</view>
|
||||
|
||||
<Typography variant="caption" class="match-count">匹配度: {{ result.matchCount }}</Typography>
|
||||
<Typography variant="caption" class="match-count">{{ $t('search.matchCount') }}: {{ result.matchCount }}</Typography>
|
||||
</view>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SearchResult } from '../../types/search'
|
||||
import Card from '../Card/Card.vue'
|
||||
import Typography from '../Typography/Typography.vue'
|
||||
@@ -34,12 +35,14 @@ interface Props {
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}年${month}月${day}日`
|
||||
return t('search.dateFormat', { year, month, day })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<view class="search-result-list">
|
||||
<view class="list-header">
|
||||
<Typography variant="h4" weight="semibold">搜索结果</Typography>
|
||||
<Typography variant="caption" class="result-count">共{{ results.length }}条结果</Typography>
|
||||
<Typography variant="h4" weight="semibold">{{ $t('search.searchResults') }}</Typography>
|
||||
<Typography variant="caption" class="result-count">{{ $t('search.resultCount', { count: results.length }) }}</Typography>
|
||||
<SortSwitcher
|
||||
:sortBy="sortBy"
|
||||
:sortOrder="sortOrder"
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
|
||||
<view v-if="loadingMore" class="loading-more">
|
||||
<LoadingIndicator text="加载中..." />
|
||||
<LoadingIndicator :text="$t('common.loading')" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:variant="size === 'small' ? 'caption' : 'body'"
|
||||
class="select-placeholder"
|
||||
>
|
||||
{{ placeholder }}
|
||||
{{ displayPlaceholder }}
|
||||
</Typography>
|
||||
<Icon
|
||||
:name="isOpen ? 'arrow-up' : 'arrow-down'"
|
||||
@@ -65,6 +65,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Typography from '@/components/Typography/Typography.vue'
|
||||
import Icon from '@/components/Icon/Icon.vue'
|
||||
|
||||
@@ -81,12 +82,16 @@ interface Props {
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
}
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: '请选择',
|
||||
placeholder: undefined,
|
||||
disabled: false,
|
||||
size: 'medium'
|
||||
})
|
||||
|
||||
const displayPlaceholder = computed(() => props.placeholder ?? t('common.pleaseSelect'))
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
(e: 'change', value: string | number): void
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<view class="sort-switcher">
|
||||
<Typography variant="caption" class="sort-label">排序</Typography>
|
||||
<Typography variant="caption" class="sort-label">{{ $t('search.sort') }}</Typography>
|
||||
|
||||
<view class="sort-options">
|
||||
<view
|
||||
@@ -8,7 +8,7 @@
|
||||
:class="{ 'sort-option-active': sortBy === 'date' }"
|
||||
@click="handleSortByChange('date')"
|
||||
>
|
||||
<Typography variant="caption" class="sort-option-text">日期</Typography>
|
||||
<Typography variant="caption" class="sort-option-text">{{ $t('search.sortByDate') }}</Typography>
|
||||
<Icon
|
||||
v-if="sortBy === 'date'"
|
||||
:name="sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
@@ -22,7 +22,7 @@
|
||||
:class="{ 'sort-option-active': sortBy === 'matchCount' }"
|
||||
@click="handleSortByChange('matchCount')"
|
||||
>
|
||||
<Typography variant="caption" class="sort-option-text">匹配度</Typography>
|
||||
<Typography variant="caption" class="sort-option-text">{{ $t('search.sortByMatch') }}</Typography>
|
||||
<Icon
|
||||
v-if="sortBy === 'matchCount'"
|
||||
:name="sortOrder === 'asc' ? 'arrow-up' : 'arrow-down'"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Card class="template-panel">
|
||||
<view class="panel-header">
|
||||
<Typography variant="h4" weight="semibold">搜索模板</Typography>
|
||||
<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>
|
||||
@@ -12,7 +12,7 @@
|
||||
<Icon name="search" size="16rpx" color="rgba(113, 113, 122, 255)" />
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索模板"
|
||||
:placeholder="$t('search.searchTemplates')"
|
||||
class="input-field"
|
||||
/>
|
||||
</view>
|
||||
@@ -33,8 +33,8 @@
|
||||
<view v-if="filteredTemplates.length === 0" class="empty-templates">
|
||||
<EmptyState
|
||||
icon="empty"
|
||||
title="暂无模板"
|
||||
description="没有找到符合条件的模板"
|
||||
:title="$t('search.noTemplatesTitle')"
|
||||
:description="$t('search.noTemplatesDesc')"
|
||||
size="small"
|
||||
/>
|
||||
</view>
|
||||
@@ -65,6 +65,7 @@
|
||||
|
||||
<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'
|
||||
@@ -73,23 +74,25 @@ 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('全部')
|
||||
const selectedCategory = ref('__all__')
|
||||
const templates = ref<SearchTemplate[]>([])
|
||||
|
||||
const categories = computed(() => {
|
||||
const allCategories = templateService.getCategories()
|
||||
return ['全部', ...allCategories]
|
||||
return [t('search.allCategories'), ...allCategories]
|
||||
})
|
||||
|
||||
const filteredTemplates = computed(() => {
|
||||
let result = templates.value
|
||||
|
||||
if (selectedCategory.value !== '全部') {
|
||||
if (selectedCategory.value !== '__all__') {
|
||||
result = templateService.getTemplatesByCategory(selectedCategory.value)
|
||||
}
|
||||
|
||||
@@ -105,7 +108,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
function loadTemplates() {
|
||||
templates.value = templateService.getTemplates()
|
||||
templates.value = templateService.getSearchTemplates()
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<view :class="['typography', `typography--${variant}`, `typography--${weight}`, { 'typography--ellipsis': ellipsis }]">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
variant?: 'h1' | 'h2' | 'h3' | 'h4' | 'body' | 'caption' | 'overline'
|
||||
weight?: 'regular' | 'medium' | 'semibold' | 'bold'
|
||||
ellipsis?: boolean
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
variant: 'body',
|
||||
weight: 'regular',
|
||||
ellipsis: false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.typography {
|
||||
color: rgba(44, 24, 16, 255);
|
||||
|
||||
&--ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.typography--h1 {
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.typography--h2 {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.typography--h3 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.typography--h4 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.typography--body {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.typography--caption {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.typography--overline {
|
||||
font-size: 10px;
|
||||
line-height: 14px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.typography--regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.typography--medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.typography--semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.typography--bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 375px) {
|
||||
.typography--h1 {
|
||||
font-size: 24px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.typography--h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.typography--h3 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.typography--h4 {
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.typography--body {
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.typography--caption {
|
||||
font-size: 11px;
|
||||
line-height: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user