Files
airz-uniapp/packages/content/search/search.uvue
T
hujun 71b3c25695 feat: 优化账户切换时的页面状态一致性,增加登录回跳校验和课程阅读新逻辑
- 增加账户状态监听定时器,页面切换时自动校验账户归属,避免跨账户操作
- 新增登录回跳路径校验函数,限制仅允许指定页面回跳,防止非法跳转
- 课程页改为整篇连续阅读,底部仅一次标记已读,兼容历史记录
- 完善助手、对比页、资料填写页等多处的账户状态校验逻辑,防止错版数据提交
- 优化账户资料请求逻辑,增加版本校验,避免旧请求覆盖新数据
2026-10-07 10:01:05 +08:00

135 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="搜索" />
<view class="search-bar">
<view class="search-box">
<image class="search-icon" src="/static/icons/search.svg" mode="aspectFit" />
<input v-model="keyword" class="search-input" placeholder="搜索新闻、融资课堂" maxlength="80" confirm-type="search" @input="resetResults" @confirm="search" />
<view v-if="keyword.length > 0" class="clear-search" @click="clearSearch"><text class="clear-mark">×</text></view>
</view>
<button class="search-button" @click="search">搜索</button>
</view>
<view class="search-tabs">
<view v-for="item in tabs" :key="item.id" :class="activeType == item.id ? 'search-tab selected-tab' : 'search-tab'" @click="selectType(item.id)"><text :class="activeType == item.id ? 'tab-label selected-label' : 'tab-label'">{{ item.label }}</text></view>
</view>
<scroll-view scroll-y :key="listVersion" :style="contentStyle(296, false)">
<view class="search-content" :style="bottomStyle(40)">
<view v-if="!searched" class="search-guide">
<text class="guide-title">想了解什么?</text><text class="guide-copy">查找融资资讯,学习实用知识</text>
<view class="suggestion-list"><view class="suggestion" v-for="word in suggestions" :key="word" @click="searchWord(word)"><text class="suggestion-label">{{ word }}</text></view></view>
</view>
<view v-else-if="loading" class="state-panel"><text class="state-title">正在搜索…</text></view>
<view v-else-if="errorMessage.length > 0" class="state-panel"><text class="state-title">搜索暂时不可用</text><text class="state-copy">{{ errorMessage }}</text><button class="retry-button" @click="search">重新搜索</button></view>
<view v-else-if="results.length == 0" class="state-panel"><view class="empty-symbol"><image class="empty-icon" src="/static/icons/search.svg" mode="aspectFit" /></view><text class="state-title">没有找到相关内容</text><text class="state-copy">试试更简短的关键词,或切换搜索分类</text></view>
<view v-else>
<text class="result-count">找到 {{ total }} 条相关内容</text>
<view class="result-card" v-for="item in results" :key="item.type + ':' + item.id" @click="openResult(item)">
<view class="result-copy"><view class="result-heading"><text :class="item.type == 'news' ? 'result-kind' : 'result-kind lesson-kind'">{{ item.type == 'news' ? '新闻' : '融资课堂' }}</text><text class="result-category">{{ item.category }}</text></view><text class="result-title">{{ item.title }}</text><text class="result-summary">{{ item.summary }}</text><text class="result-meta">{{ item.meta }}</text></view>
<image v-if="item.image.length > 0" class="result-image" :src="item.image" mode="aspectFill" />
</view>
<text v-if="moreError.length > 0" class="state-copy">{{ moreError }}</text>
<button v-if="hasMore" class="retry-button" @click="more">{{ loadingMore ? '正在加载…' : '加载更多' }}</button>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import AppHeader from '@/components/AppHeader.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { searchContent, ContentSearchResult } from '@/packages/content/services/search.uts'
import { validateContentAppend } from '@/services/content.uts'
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
type SearchTab = { id: string, label: string }
const tabs: SearchTab[] = [{ id: 'all', label: '全部' }, { id: 'news', label: '新闻' }, { id: 'lesson', label: '融资课堂' }]
const suggestions: string[] = ['个人信用', '企业融资', '利率', '还款', '材料', '政策']
const keyword = ref('')
const activeType = ref('all')
const searched = ref(false)
const loading = ref(false)
const errorMessage = ref('')
const results = ref<ContentSearchResult[]>([])
const listVersion = ref(0)
const total = ref(0); const hasMore = ref(false); const loadingMore = ref(false); const moreError = ref('')
let page = 1
let requestVersion = 0
let visible = false
const resetResults = () => { requestVersion++; results.value = []; total.value = 0; hasMore.value = false; loadingMore.value = false; moreError.value = ''; searched.value = false; loading.value = false; errorMessage.value = ''; listVersion.value++ }
const clearSearch = () => { keyword.value = ''; resetResults() }
async function load(append: boolean) {
if (!visible) return
if (append && (loading.value || loadingMore.value || !hasMore.value)) return
keyword.value = keyword.value.trim()
if (keyword.value.length == 0) { clearSearch(); return }
uni.hideKeyboard()
const currentRequest = append ? requestVersion : ++requestVersion
if (append) { loadingMore.value = true; moreError.value = '' }
else { searched.value = true; loading.value = true; loadingMore.value = false; errorMessage.value = ''; moreError.value = ''; results.value = []; total.value = 0; hasMore.value = false; page = 1; listVersion.value++ }
try {
const result = await searchContent(keyword.value, activeType.value, append ? page + 1 : 1)
// 输入、切换分类或退出页面后,忽略旧请求,避免结果被覆盖。
if (!visible || currentRequest != requestVersion) return
if (append) validateContentAppend(results.value.map((item: ContentSearchResult): string => item.type + ':' + item.id), result.items.map((item: ContentSearchResult): string => item.type + ':' + item.id))
results.value = append ? results.value.concat(result.items) : result.items
total.value = result.total; hasMore.value = result.hasMore
if (append) page++
} catch (err) {
if (visible && currentRequest == requestVersion) {
const message = err instanceof Error ? err.message : '请检查网络连接后重试'
if (append) moreError.value = message; else errorMessage.value = message
}
} finally { if (currentRequest == requestVersion) { loading.value = false; loadingMore.value = false } }
}
const search = () => { load(false) }
const more = () => { load(true) }
const selectType = (type: string) => { if (activeType.value == type) return; activeType.value = type; if (searched.value) search() }
const searchWord = (word: string) => { keyword.value = word; search() }
const openResult = (item: ContentSearchResult) => {
const route = item.type == 'news' ? '/packages/content/article/article' : '/packages/content/lesson/lesson'
uni.navigateTo({ url: route + '?id=' + encodeURIComponent(item.id) })
}
onShow(() => { visible = true; if (searched.value) search() })
onHide(() => { visible = false; requestVersion++; loading.value = false; loadingMore.value = false })
onUnload(() => { visible = false; requestVersion++ })
</script>
<style>
.search-bar { height: 112rpx; padding: 20rpx 28rpx; flex-direction: row; align-items: center; background-color: #ffffff; }
.search-box { flex: 1; min-width: 0; height: 72rpx; border-radius: 16rpx; background-color: #f2f6fc; flex-direction: row; align-items: center; padding-left: 20rpx; }
.search-icon { width: 34rpx; height: 34rpx; margin-right: 14rpx; flex-shrink: 0; }
.search-input { flex: 1; min-width: 0; height: 72rpx; font-size: 27rpx; color: #22324a; }
.clear-search { width: 64rpx; height: 72rpx; align-items: center; justify-content: center; flex-shrink: 0; }
.clear-mark { font-size: 36rpx; line-height: 44rpx; color: #738198; }
.search-button { width: 92rpx; height: 72rpx; line-height: 72rpx; padding: 0; margin: 0 0 0 16rpx; font-size: 28rpx; font-weight: 600; color: #0868f7; background-color: transparent; border-radius: 12rpx; }
.search-tabs { height: 88rpx; padding: 0 28rpx; flex-direction: row; background-color: #ffffff; border-bottom: 1rpx solid #e8eef6; }
.search-tab { flex: 1; height: 88rpx; align-items: center; justify-content: center; border-bottom: 4rpx solid transparent; }
.selected-tab { border-bottom-color: #0868f7; }
.tab-label { font-size: 28rpx; color: #66758a; }
.selected-label { color: #0868f7; font-weight: 700; }
.search-content { padding: 28rpx 28rpx 40rpx; }
.search-guide { padding: 22rpx 4rpx; }
.guide-title { color: #172640; font-size: 34rpx; line-height: 48rpx; font-weight: 700; }
.guide-copy { color: #738198; font-size: 26rpx; line-height: 40rpx; margin-top: 10rpx; }
.suggestion-list { flex-direction: row; flex-wrap: wrap; margin-top: 28rpx; }
.suggestion { padding: 18rpx 28rpx; margin: 0 16rpx 18rpx 0; border: 1rpx solid #e3eaf4; border-radius: 14rpx; background-color: #ffffff; }
.suggestion-label { font-size: 26rpx; color: #405675; }
.result-count { font-size: 24rpx; color: #738198; margin-bottom: 20rpx; }
.result-card { flex-direction: row; align-items: center; background-color: #ffffff; border: 1rpx solid #e6edf7; border-radius: 18rpx; padding: 24rpx; margin-bottom: 18rpx; }
.result-copy { flex: 1; min-width: 0; }
.result-heading { flex-direction: row; align-items: center; flex-wrap: wrap; }
.result-kind { padding: 5rpx 10rpx; border-radius: 6rpx; font-size: 21rpx; color: #0868f7; background-color: #edf5ff; margin-right: 12rpx; }
.lesson-kind { color: #197f70; background-color: #eaf7f3; }
.result-category { color: #738198; font-size: 22rpx; line-height: 34rpx; }
.result-title { color: #172640; font-size: 29rpx; line-height: 42rpx; font-weight: 600; margin-top: 12rpx; }
.result-summary { color: #66758a; font-size: 24rpx; line-height: 36rpx; margin-top: 10rpx; }
.result-meta { color: #738198; font-size: 22rpx; line-height: 32rpx; margin-top: 14rpx; }
.result-image { width: 132rpx; height: 132rpx; border-radius: 12rpx; margin-left: 20rpx; flex-shrink: 0; }
.state-panel { padding: 100rpx 16rpx; align-items: center; }
.empty-symbol { width: 112rpx; height: 112rpx; border-radius: 56rpx; align-items: center; justify-content: center; background-color: #e8f2ff; margin-bottom: 26rpx; }
.empty-icon { width: 50rpx; height: 50rpx; }
.state-title { color: #26374f; font-size: 30rpx; line-height: 44rpx; font-weight: 600; text-align: center; }
.state-copy { color: #738198; font-size: 25rpx; line-height: 40rpx; margin-top: 14rpx; text-align: center; }
.retry-button { color: #0868f7; background-color: #e8f2ff; height: 76rpx; line-height: 76rpx; padding: 0 36rpx; margin-top: 28rpx; border-radius: 12rpx; font-size: 26rpx; }
</style>