add word search

This commit is contained in:
Felix
2025-11-05 19:30:51 +08:00
parent 5070cbfb48
commit 148d87277b
9 changed files with 692 additions and 6 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
.history
.DS_Store
miniprogram/static/.DS_Store
miniprogram/miniprogram_npm
node_modules

View File

@@ -8,6 +8,10 @@
"onReachBottomDistance": 50,
"usingComponents": {
"t-slider": "tdesign-miniprogram/slider/slider",
"t-icon": "tdesign-miniprogram/icon/icon"
"t-icon": "tdesign-miniprogram/icon/icon",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel"
}
}

View File

@@ -42,6 +42,49 @@ interface IPageData {
audioDuration: number // 音频总时长
currentTime: number // 当前播放时间
sliderValue: number // 进度条值0-100
showDictPopup: boolean // 控制弹窗是否显示
showDictExtended: boolean // 控制扩展内容是否显示
wordDict: {
ee: any
ec: any
expandEc: any
etym: any
syno: {
word: string
synos: Array<{
syno: {
ws: Array<{ w: string }>
pos: string
tran: string
}
}>
}
simple: any
phrs: any
individual: any
collins: any
relWord: {
rels: Array<{
rel: {
pos: string
words: Array<{
tran: string
word: string
}>
}
}>
}
discriminate: {
data: Array<{
source: string
usages: Array<{
usage: string
headword: string
}>
}>
}
}
prototypeWord: string
}
type IPageMethods = {
@@ -59,6 +102,12 @@ type IPageMethods = {
playStandardVoice: () => void
resetAudioState: () => void
handleSliderChange: (e: any) => void
handleWordClick: (e: any) => void
handleDictMore: () => void
handleDictClose: () => void
playWordAudio: (e: any) => void
onTabsChange: (event: any) => void
onTabsClick: (event: any) => void
}
interface IPageInstance extends IPageMethods {
@@ -87,6 +136,20 @@ Page<IPageData, IPageInstance>({
ipas: [], // 音标的单词数组
isScoreExpanded: false, // 评分区域是否展开
wordScores: [], // 单词评分数组
prototypeWord: '',
wordDict: {
ee: {},
ec: {},
expandEc: {},
etym: {},
syno: { word: '', synos: [] },
simple: {},
phrs: {},
individual: {},
collins: {},
relWord: { rels: [] },
discriminate: { data: [] }
},
matchTagLegend: [
{ tag: 0, description: '匹配', color: '#ffffff' },
{ tag: 1, description: '新增', color: '#ffebee' },
@@ -98,7 +161,9 @@ Page<IPageData, IPageInstance>({
isPlaying: false, // 是否正在播放音频
audioDuration: 0, // 音频总时长
currentTime: 0, // 当前播放时间
sliderValue: 0 // 进度条值0-100
sliderValue: 0, // 进度条值0-100
showDictPopup: true, // 控制弹窗是否显示
showDictExtended: false // 控制扩展内容是否显示
},
// 切换评分区域展开状态
@@ -508,14 +573,88 @@ Page<IPageData, IPageInstance>({
const { word } = e.currentTarget.dataset
if (!word) return
try {
const cleanedWord = word.replace(/[.,?!*]/g, '')
if (!cleanedWord) return
try {
// 调用API获取单词详情
const wordDetail: any = await apiManager.getWordDetail(word)
const wordDetail: any = await apiManager.getWordDetail(cleanedWord)
console.log('获取到单词详情:', wordDetail)
// 这里可以添加处理单词详情的逻辑,比如显示在页面上
this.setData({
showDictPopup: true,
wordDict: {
ee:wordDetail['ee'], ec:wordDetail['ec'], expandEc:wordDetail['expand_ec'],
simple:wordDetail['simple'], phrs:wordDetail['phrs'], etym:wordDetail['etym'],
individual:wordDetail['individual'], collins:wordDetail['collins'],
relWord: wordDetail['rel_word'], syno: wordDetail['syno'],
discriminate: wordDetail['discriminate']
},
prototypeWord: wordDetail['ec'].word[0]?.prototype
})
} catch (error) {
console.error('获取单词详情失败:', error)
wx.showToast({
title: '获取单词详情失败',
icon: 'none'
})
}
},
// 点击“more”按钮展开/收起扩展内容
handleDictMore() {
this.setData({
showDictExtended: !this.data.showDictExtended
})
},
// 点击“x”按钮关闭弹窗
handleDictClose() {
this.setData({
showDictPopup: false
})
},
onTabsChange(event: any) {
console.log(`Change tab, tab-panel value is ${event.detail.value}.`);
},
onTabsClick(event: any) {
console.log(`Click tab, tab-panel value is ${event.detail.value}.`);
},
// 播放单词音频
playWordAudio(e: any) {
const { audio } = e.currentTarget.dataset
if (!audio) return
// 构造音频URL
const audioUrl = `https://dict.youdao.com/dictvoice?audio=${audio}`
// 创建音频上下文
const wordAudioContext = wx.createInnerAudioContext()
wordAudioContext.onPlay(() => {
console.log('开始播放单词音频')
})
wordAudioContext.onError((res) => {
console.error('播放单词音频失败:', res)
wx.showToast({
title: '播放失败',
icon: 'none'
})
})
wordAudioContext.onEnded(() => {
console.log('单词音频播放结束')
wordAudioContext.destroy()
})
// 设置音频源并播放
wordAudioContext.src = audioUrl
wordAudioContext.play()
},
onLoad(options: Record<string, string>) {
//如果有图片ID调用接口获取文本和评分信息
if (options.index) {

View File

@@ -142,6 +142,136 @@
<text class="no-score-text">暂无评分</text>
</view>
</view>
<!-- 单词查询展示区域 -->
<view class="word-popup" wx:if="{{showDictPopup}}">
<!-- 弹窗头部:单词 + more 按钮 -->
<view class="popup-header">
<!-- 频率标签 -->
<view class="frequency-tags">
<t-tag wx:if="{{wordDict.ec.exam_type && wordDict.ec.exam_type.length > 0}}" wx:for="{{wordDict.ec.exam_type}}" wx:key="index" variant="light" theme="success" style="margin-right: 12rpx;">
{{item}}
</t-tag>
</view>
<view class="close-btn" bindtap="handleDictClose">
<t-icon name="close" size="48rpx" />
</view>
</view>
<view class="popup-header">
<text wx:if="{{wordDict.simple.query}}" class="word-title">{{wordDict.simple.query}}</text>
<text class="word-title" wx:if="{{prototypeWord}}">
{{prototypeWord}}
</text>
<view class="more-btn" bindtap="handleDictMore">
<text wx:if="{{!showDictExtended}}">More</text>
<text wx:else>Less</text>
<t-icon wx:if="{{!showDictExtended}}" name="chevron-up" size="48rpx"></t-icon>
<t-icon wx:else name="chevron-down" size="48rpx"></t-icon>
</view>
</view>
<!-- 发音区域 -->
<view class="pronounce">
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
<text wx:if="{{wordDict.simple.word[0].ukphone}}">UK:[{{wordDict.simple.word[0].ukphone}}]</text>
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].ukspeech}}" bind:click="playWordAudio" data-audio="{{wordDict.simple.word[0].ukspeech}}" name="sound" size="40rpx" />
</view>
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
<text wx:if="{{wordDict.simple.word[0].usphone}}">US:[{{wordDict.simple.word[0].usphone}}]</text>
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].usspeech}}" bind:click="playWordAudio" data-audio="{{wordDict.simple.word[0].usspeech}}" name="sound" size="40rpx" />
</view>
</view>
<!-- 基础词性释义 -->
<view class="word-meanings">
<block wx:if="{{wordDict.ec && wordDict.ec.word && wordDict.ec.word.length > 0}}">
<block wx:for="{{wordDict.ec.word[0].trs}}" wx:key="index">
<block wx:for="{{item.tr}}" wx:key="index">
<block wx:for="{{item.l.i}}" wx:key="index">
<text>{{item}}</text>
</block>
</block>
</block>
</block>
</view>
<!-- 扩展内容(默认隐藏,点击 more 后显示) -->
<t-tabs
animation="{{ { duration: 0.1 } }}"
defaultValue="{{0}}"
bind:change="onTabsChange"
bind:click="onTabsClick"
wx:if="{{showDictExtended}}"
>
<t-tab-panel label="词典" value="3" wx:if="{{wordDict.ee && wordDict.ee.word && wordDict.ee.word.trs && wordDict.ee.word.trs.length > 0}}">
<view class="dictionary">
<view class="dictionary-content" wx:for="{{wordDict.ee.word.trs}}" wx:key="index">
<view class="dictionary-pos">{{item.pos}}</view>
<view class="dictionary-list" wx:for="{{item.tr}}" wx:key="index">
<view class="word-info">{{item.l.i}}</view>
<view class="word-exam" wx:if="{{item.exam && item.exam.i && item.exam.i.f && item.exam.i.f.l}}">
<view wx:for="{{item.exam.i.f.l}}" wx:key="index">{{item.i}}</view>
</view>
<view class="word-similar" wx:if="{{item['similar-words'] && item['similar-words'].length > 0}}">
<view wx:for="{{item['similar-words']}}" wx:key="index">{{item.similar}}</view>
</view>
</view>
</view>
</view>
</t-tab-panel>
<t-tab-panel wx:if="{{wordDict.phrs && wordDict.phrs.phrs && wordDict.phrs.phrs.length > 0}}" label="短语" value="0">
<block wx:for="{{wordDict.phrs.phrs}}" wx:key="index">
<t-cell
title="{{item.phr.headword.l.i}}"
description="{{item.phr.trs[0].tr.l.i}}"
/>
</block>
</t-tab-panel>
<t-tab-panel wx:if="{{wordDict.individual && wordDict.individual.pastExamSents && wordDict.individual.pastExamSents.length > 0}}" label="真题" value="1">
<view wx:for="{{wordDict.individual.pastExamSents}}" wx:key="index" class="exam-sent">
<view class="exam-sent-en">{{item.en}}</view>
<view class="exam-sent-zh">{{item.zh}}</view>
<view class="exam-sent-source">--摘自《{{item.source}}》</view>
</view>
</t-tab-panel>
<t-tab-panel label="相关单词" value="2">
<view wx:if="{{(wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0) || (wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0) || (wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0)}}">
<view class="syno" wx:if="{{wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0}}">
<view class="syno-title">同义词</view>
<view class="syno-list" wx:for="{{wordDict.syno.synos}}" wx:key="index">
<view class="syno-pos">{{item.syno.pos}}</view>
<view class="syno-tran">{{item.syno.tran}}</view>
<view class="syno-item" wx:for="{{item.syno.ws}}" wx:key="w">{{item.w}}</view>
</view>
</view>
<view class="rel-word" wx:if="{{wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0}}">
<view class="rel-word-title">相关单词</view>
<view class="rel-word-list" wx:for="{{wordDict.relWord.rels}}" wx:key="index">
<view class="rel-word-pos">{{item.rel.pos}}</view>
<view class="rel-word-item" wx:for="{{item.rel.words}}" wx:key="word">
<view class="rel-word-tran">{{item.tran}}</view>
<view class="rel-word-word">{{item.word}}</view>
</view>
</view>
</view>
<view class="discriminate" wx:if="{{wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0}}">
<view class="discriminate-title">区分单词</view>
<view class="discriminate-list" wx:for="{{wordDict.discriminate.data}}" wx:key="index">
<view class="discriminate-item" wx:for="{{item.usages}}" wx:key="headword">
<view class="discriminate-tran">{{item.usage}}</view>
<view class="discriminate-headword">{{item.headword}}</view>
</view>
</view>
</view>
</view>
<view wx:else class="no-related-words">
<text class="no-related-words-text">暂无相关单词信息</text>
</view>
</t-tab-panel>
</t-tabs>
</view>
<!-- 遮罩层 -->
<view class="mask" wx:if="{{isScoreExpanded}}" catchtap="toggleScoreSection"></view>
</view>

View File

@@ -523,4 +523,362 @@
color: #333;
font-weight: 500;
text-align: right;
}
}
/* 弹窗整体样式 */
.word-popup {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-top-left-radius: 12rpx;
border-top-right-radius: 12rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
padding: 20rpx;
box-sizing: border-box;
transition: all 0.3s ease;
z-index: 100;
overflow-y: scroll;
max-height: 100%;
}
/* 弹窗头部 */
.popup-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
position: relative;
}
/* 频率标签容器 */
.frequency-tags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
min-height: 48rpx; /* 确保即使没有标签也有最小高度 */
}
/* 关闭按钮 */
.close-btn {
margin-left: 20rpx;
flex-shrink: 0;
}
.more-btn {
display: flex;
}
/* 发音区域 */
.pronounce {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.pron-item {
display: flex;
align-items: center;
margin-right: 20rpx;
}
.sound-btn {
background: transparent;
border: none;
margin-left: 10rpx;
}
/* 基础词性释义 */
.word-meanings {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
}
/* 扩展内容区域 */
.extended-content {
margin-top: 20rpx;
}
.tab-bar {
display: flex;
margin-bottom: 20rpx;
}
.tab {
padding: 10rpx 20rpx;
margin-right: 10rpx;
font-size: 28rpx;
}
.tab.active {
color: #007AFF;
border-bottom: 2rpx solid #007AFF;
}
.syllabus-title, .example-title {
font-weight: bold;
margin-bottom: 10rpx;
display: block;
}
.syllabus-stats {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
}
.progress-item {
margin-bottom: 10rpx;
}
.progress {
height: 10rpx;
background: #eee;
margin-top: 10rpx;
}
.progress-bar {
height: 100%;
background: #007AFF;
}
.example-sentence {
display: flex;
flex-direction: column;
}
/* 真题例句样式 */
.exam-sents {
padding: 20rpx;
}
.exam-sent {
margin-bottom: 30rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
}
.exam-sent-en {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 6rpx;
}
.exam-sent-zh {
font-size: 28rpx;
color: #999;
margin-bottom: 6rpx;
}
.exam-sent-source {
font-size: 24rpx;
color: #ccc;
text-align: right;
}
.no-exam-sents {
text-align: center;
font-size: 28rpx;
color: #999;
padding: 40rpx 0;
}
/* 同义词样式 */
.syno {
margin-bottom: 30rpx;
}
.syno-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.syno-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.syno-pos {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.syno-tran {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
}
.syno-item {
display: inline-block;
padding: 10rpx 20rpx;
background: #e3f2fd;
border-radius: 30rpx;
margin-right: 15rpx;
margin-bottom: 15rpx;
font-size: 26rpx;
color: #001858;
}
/* 相关单词样式 */
.rel-word {
margin-bottom: 30rpx;
}
.rel-word-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.rel-word-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.rel-word-pos {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.rel-word-item {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 1rpx solid #eee;
}
.rel-word-item:last-child {
border-bottom: none;
}
.rel-word-tran {
font-size: 28rpx;
color: #333;
flex: 1;
}
.rel-word-word {
font-size: 28rpx;
color: #001858;
font-weight: 500;
margin-left: 20rpx;
}
/* 区分单词样式 */
.discriminate {
margin-bottom: 30rpx;
}
.discriminate-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.discriminate-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
}
.discriminate-item {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 1rpx solid #eee;
}
.discriminate-item:last-child {
border-bottom: none;
}
.discriminate-tran {
font-size: 28rpx;
color: #333;
flex: 1;
}
.discriminate-headword {
font-size: 28rpx;
color: #001858;
font-weight: 500;
margin-left: 20rpx;
}
/* 无相关单词信息样式 */
.no-related-words {
padding: 40rpx 20rpx;
text-align: center;
}
.no-related-words-text {
font-size: 28rpx;
color: #999;
}
/* 词典样式 */
.dictionary {
padding: 20rpx;
}
.dictionary-content {
background: #f9f9f9;
border-radius: 10rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.dictionary-pos {
font-size: 26rpx;
color: #666;
margin-bottom: 15rpx;
font-weight: bold;
}
.dictionary-list {
padding: 15rpx 0;
}
.word-info {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
line-height: 1.5;
}
.word-exam {
font-size: 26rpx;
color: #666;
margin-bottom: 15rpx;
padding: 10rpx 15rpx;
background: #e3f2fd;
border-radius: 8rpx;
}
.word-similar {
font-size: 26rpx;
color: #001858;
padding: 8rpx 15rpx;
background: #fff3e0;
border-radius: 30rpx;
display: inline-block;
margin-right: 10rpx;
margin-bottom: 10rpx;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 KiB

View File

@@ -376,6 +376,7 @@ Page({
// 清除API管理器中的缓存
apiManager.clearAudioCache()
apiManager.clearWordDetailCache()
wx.showToast({
title: '缓存已清除',

View File

@@ -804,9 +804,35 @@ class ApiManager {
// 获取单词详情
async getWordDetail(word: string): Promise<YdWordDetail> {
console.log('获取单词详情')
// 首先检查本地缓存
const cacheKey = `word_detail_${word}`
try {
const cachedData = wx.getStorageSync(cacheKey)
if (cachedData) {
console.log('使用缓存的单词详情:', word)
return cachedData
}
} catch (e) {
console.warn('读取单词详情缓存失败:', e)
}
// 缓存中没有,从服务器获取
const response = await this.request<YdWordDetail>(`/api/v1/dict/word/${encodeURIComponent(word)}`)
const wordDetail = response.data
// 将结果缓存到本地存储设置7天过期时间
try {
wx.setStorageSync(cacheKey, wordDetail)
// 同时存储缓存时间,用于清理时判断
const cacheTimeKey = `word_detail_time_${word}`
wx.setStorageSync(cacheTimeKey, Date.now())
} catch (e) {
console.warn('存储单词详情缓存失败:', e)
}
console.log('获取单词详情成功:', response)
return response.data
return wordDetail
}
// 播放音频文件
@@ -1056,6 +1082,32 @@ class ApiManager {
keysToDelete.forEach(key => cacheStats.delete(key))
}
// 清除单词详情缓存
clearWordDetailCache(): void {
console.log('清除单词详情缓存')
try {
const storageInfo = wx.getStorageInfoSync()
// 查找所有单词详情相关的缓存键
const wordDetailKeys: string[] = []
storageInfo.keys.forEach(key => {
if (key.startsWith('word_detail_')) {
wordDetailKeys.push(key)
}
})
// 删除所有单词详情缓存
wordDetailKeys.forEach(key => {
wx.removeStorageSync(key)
})
console.log('已清除单词详情缓存,数量:', wordDetailKeys.length)
} catch (e) {
console.error('清除单词详情缓存失败:', e)
}
}
// 获取缓存统计信息
getCacheStats(): { audioCount: number; imageCount: number; totalSize: number; sizeByType: Record<string, number> } {
console.log('开始获取缓存统计信息')