- 移除微信订阅推送功能(云函数/订阅管理页),回归纯客户端零后端架构 - 新增今日黄历默认展示卡片 TodayAlmanacCard(9 语言翻译) - i18n 修复:启用 globalInjection 修复 $t 未注入,插值消息全部改为 Messages Functions 适配小程序端(63 处) - 修复底部导航切换失效与运势页数据不刷新 - 统一设计令牌(深褐主色 + 印红强调色),组件 UI 规范重构 - 图标 iconfont 化(FontAwesome 子集化,修复 Android 豆腐块) - 新增 postcss-px2rpx 机型自适应(仅 mp-weixin 构建生效) - 新增 5 个用户旅程 E2E 测试(31 用例)与验收报告 - 补充微信小程序项目配置(appid)
184 lines
4.4 KiB
Vue
184 lines
4.4 KiB
Vue
<!--
|
||
已实现但当前未接入任何页面(预留组件)。
|
||
若后续要在搜索结果区展示"搜索历史",在页面中引入并监听 select 事件即可。
|
||
-->
|
||
<template>
|
||
<Card class="search-history-panel">
|
||
<view class="panel-header">
|
||
<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)">{{ $t('search.clearHistory') }}</Typography>
|
||
</Button>
|
||
</view>
|
||
|
||
<view v-if="history.length === 0" class="empty-history">
|
||
<EmptyState
|
||
icon="empty"
|
||
:title="$t('search.noHistoryTitle')"
|
||
:description="$t('search.noHistoryDesc')"
|
||
size="small"
|
||
/>
|
||
</view>
|
||
|
||
<view v-else class="history-list">
|
||
<view
|
||
v-for="item in history"
|
||
:key="item.id"
|
||
class="history-item"
|
||
@click="handleSelectHistory(item)"
|
||
>
|
||
<view class="history-content">
|
||
<Typography variant="body" class="history-title">
|
||
{{ formatHistoryTitle(item.condition) }}
|
||
</Typography>
|
||
<Typography variant="caption" class="history-time">
|
||
{{ formatTime(item.createdAt) }}
|
||
</Typography>
|
||
</view>
|
||
<Icon name="right" size="16rpx" color="rgba(113, 113, 122, 255)" />
|
||
</view>
|
||
</view>
|
||
</Card>
|
||
</template>
|
||
|
||
<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'
|
||
import Button from '../Button/Button.vue'
|
||
import Typography from '../Typography/Typography.vue'
|
||
import Icon from '../Icon/Icon.vue'
|
||
import EmptyState from '../EmptyState/index.vue'
|
||
|
||
interface HistoryItem {
|
||
id: string
|
||
condition: SearchRequest
|
||
createdAt: string
|
||
}
|
||
|
||
const { t } = useI18n()
|
||
|
||
const emit = defineEmits<{
|
||
select: [condition: SearchRequest]
|
||
}>()
|
||
|
||
const history = ref<HistoryItem[]>([])
|
||
|
||
onMounted(() => {
|
||
loadHistory()
|
||
})
|
||
|
||
function loadHistory() {
|
||
history.value = searchService.getSearchHistory()
|
||
}
|
||
|
||
function handleSelectHistory(item: HistoryItem) {
|
||
emit('select', item.condition)
|
||
}
|
||
|
||
function handleClearHistory() {
|
||
uni.showModal({
|
||
title: t('search.confirmClear'),
|
||
content: t('search.confirmClearContent'),
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
searchService.clearSearchHistory()
|
||
history.value = []
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
function formatHistoryTitle(condition: SearchRequest): string {
|
||
const conditionCount = condition.conditions.length
|
||
const days = condition.days
|
||
|
||
let title = t('search.historyConditionDays', { count: conditionCount, days })
|
||
|
||
if (conditionCount > 0) {
|
||
const firstCondition = condition.conditions[0]
|
||
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 ? '...' : ''})`
|
||
}
|
||
|
||
return title
|
||
}
|
||
|
||
function formatTime(timeStr: string): string {
|
||
const date = new Date(timeStr)
|
||
const now = new Date()
|
||
const diffMs = now.getTime() - date.getTime()
|
||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
||
|
||
if (diffHours < 1) {
|
||
return t('search.justNow')
|
||
} else if (diffHours < 24) {
|
||
return t('search.hoursAgo', { count: diffHours })
|
||
} else {
|
||
const diffDays = Math.floor(diffHours / 24)
|
||
if (diffDays < 7) {
|
||
return t('search.daysAgo', { count: diffDays })
|
||
} else {
|
||
return date.toLocaleDateString()
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.search-history-panel {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-history {
|
||
padding: 24px 0;
|
||
}
|
||
|
||
.history-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.history-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 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);
|
||
}
|
||
}
|
||
|
||
.history-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.history-title {
|
||
color: var(--color-text);
|
||
}
|
||
|
||
.history-time {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
</style>
|