fix structure

This commit is contained in:
chenlisha02
2025-11-04 17:38:43 +08:00
parent 4aae48a317
commit 1a1ee52ac5
14 changed files with 298 additions and 1525 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.history
.DS_Store
miniprogram/static/.DS_Store

View File

@@ -2,7 +2,6 @@
"pages": [
"pages/upload/upload",
"pages/result/result",
"pages/result_show/result_show",
"pages/assessment/assessment",
"pages/profile/profile",
"pages/index/index",

View File

@@ -25,6 +25,17 @@ interface IPageData {
word: string
pronAccuracy: number
pronFluency: number
matchTag?: number // 0匹配单词、1新增单词、2缺少单词、3错读的词、4未录入单词
phoneInfos?: Array<{ // 音标信息数组
phone: string // 国际音标
pronAccuracy: number // 音标评分
matchTag: number // 音标匹配度
}>
}>
matchTagLegend: Array<{ // MatchTag 说明
tag: number
description: string
color: string
}>
standardAudioMap: { [key: string]: string } // 标准语音文件ID映射
isPlaying: boolean // 是否正在播放音频
@@ -48,7 +59,6 @@ type IPageMethods = {
playStandardVoice: () => void
resetAudioState: () => void
handleSliderChange: (e: any) => void
formatTime: (seconds: number | undefined) => string
}
interface IPageInstance extends IPageMethods {
@@ -77,6 +87,13 @@ Page<IPageData, IPageInstance>({
ipas: [], // 音标的单词数组
isScoreExpanded: false, // 评分区域是否展开
wordScores: [], // 单词评分数组
matchTagLegend: [
{ tag: 0, description: '匹配', color: '#ffffff' },
{ tag: 1, description: '新增', color: '#ffebee' },
{ tag: 2, description: '缺少', color: '#e3f2fd' },
{ tag: 3, description: '错读', color: '#fff3e0' },
{ tag: 4, description: '未录入', color: '#f5f5f5' }
],
standardAudioMap: {}, // 标准语音文件ID映射
isPlaying: false, // 是否正在播放音频
audioDuration: 0, // 音频总时长
@@ -149,7 +166,13 @@ Page<IPageData, IPageInstance>({
const wordScores = assessmentResult?.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2))
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
})) || []
})) || []
this.setData({
@@ -195,7 +218,13 @@ Page<IPageData, IPageInstance>({
const wordScores = assessmentResult?.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2))
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
})) || []
})) || []
this.setData({
@@ -475,14 +504,16 @@ Page<IPageData, IPageInstance>({
}
},
// 格式化时间
formatTime(seconds: number | undefined) {
if (seconds === undefined || isNaN(seconds)) {
return '00:00'
async handleWordClick(e: any) {
const { word } = e.currentTarget.dataset
if (!word) return
try {
// 调用API获取单词详情
const wordDetail: any = await apiManager.getWordDetail(word)
} catch (error) {
console.error('获取单词详情失败:', error)
}
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`
},
onLoad(options: Record<string, string>) {
@@ -516,7 +547,13 @@ Page<IPageData, IPageInstance>({
const wordScores = assessmentResult?.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2))
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
})) || []
})) || []
this.setData({
@@ -597,7 +634,13 @@ Page<IPageData, IPageInstance>({
const wordScores = assessmentResult.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2))
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
}))
})) || []
// 更新评分信息

View File

@@ -16,8 +16,13 @@
<view class="sentence-wrapper">
<view class="sentence-content-wrapper">
<view class="word-wrapper" wx:for="{{words}}" wx:key="index">
<text class="sentence-text">{{item}}</text>
<text wx:if="{{ipas[index]}}" class="sentence-ipa">{{ipas[index]}}</text>
<text class="sentence-text" data-word="{{item}}" bindtap="handleWordClick">
{{item}}
</text>
<text wx:if="{{ipas[index] && ipas[index] !== '*'}}" class="sentence-ipa">
{{ipas[index]}}
</text>
<text wx:else class="sentence-ipa"></text>
</view>
</view>
</view>
@@ -39,11 +44,7 @@
<t-button class="play-btn" theme="primary" size="large" variant="outline" bind:tap="playStandardVoice">
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" size="48rpx" />
</t-button>
<view class="audio-progress">
<text class="time-text">{{currentTime ? formatTime(currentTime) : '00:00'}}</text>
<t-slider class="progress-slider" value="{{sliderValue}}" bind:change="handleSliderChange" theme="primary" max="100" step="1" />
<text class="time-text">{{audioDuration ? formatTime(audioDuration) : '00:00'}}</text>
</view>
<t-slider class="progress-slider" value="{{sliderValue}}" bind:change="handleSliderChange" theme="primary" max="100" step="1" />
</view>
</view>
</view>
@@ -98,18 +99,39 @@
</view>
</view>
</view>
<!-- MatchTag 说明 -->
<view class="match-tag-legend">
<view class="legend-header">
<text class="legend-title">匹配说明:</text>
<view class="legend-items">
<view class="legend-item" wx:for="{{matchTagLegend}}" wx:key="tag">
<view class="color-box" style="background-color: {{item.color}}"></view>
<text class="legend-text">{{item.description}}</text>
</view>
</view>
</view>
</view>
<!-- 单词评分列表 -->
<view class="word-scores-list">
<view class="word-score-item" wx:for="{{wordScores}}" wx:key="word">
<text class="word-text">{{item.word}}</text>
<view class="word-score-details">
<view class="word-score-row">
<text class="word-score-label">准确性</text>
<text class="word-score-value">{{item.pronAccuracy}}</text>
<view class="word-score-item" wx:for="{{wordScores}}" wx:key="word" style="background-color: {{matchTagLegend[item.matchTag || 0].color}}">
<view class="word-header">
<text class="word-text">{{item.word}}</text>
<view class="word-score-details">
<view class="word-score-row">
<text class="word-score-label">准确性</text>
<text class="word-score-value">{{item.pronAccuracy}}</text>
</view>
<view class="word-score-row">
<text class="word-score-label">流利度</text>
<text class="word-score-value">{{item.pronFluency}}</text>
</view>
</view>
<view class="word-score-row">
<text class="word-score-label">流利度</text>
<text class="word-score-value">{{item.pronFluency}}</text>
</view>
<!-- 音标信息 -->
<view class="phone-infos" wx:if="{{item.phoneInfos && item.phoneInfos.length > 0}}">
<view class="phone-info-item" wx:for="{{item.phoneInfos}}" wx:for-item="phoneInfo" wx:key="phone" style="background-color: {{matchTagLegend[phoneInfo.matchTag || 0].color}}">
<text class="phone-text">[{{phoneInfo.phone}}]</text>
<text class="phone-score">{{phoneInfo.pronAccuracy}}</text>
</view>
</view>
</view>

View File

@@ -152,26 +152,8 @@
border: 6rpx solid #000;
}
.audio-progress {
flex: 1;
display: flex;
align-items: center;
gap: 16rpx;
/* background: #f8f8f8; */
padding: 16rpx;
border-radius: 12rpx;
}
.time-text {
font-size: 24rpx;
color: #001858;
min-width: 70rpx;
text-align: center;
}
.progress-slider {
flex: 1;
/* margin: 0 16rpx; */
--td-slider-bar-height: 8rpx;
--td-slider-dot-size: 32rpx;
}
@@ -222,7 +204,7 @@
/* 底部评分结果区域 */
.score-section {
padding: 32rpx 24rpx;
padding: 24rpx 16rpx;
background: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
@@ -234,6 +216,7 @@
z-index: 100;
transition: all 0.3s ease-in-out;
max-height: 20vh;
touch-action: pan-y;
}
.score-section.expanded {
@@ -285,26 +268,22 @@
.score-overview {
display: flex;
align-items: flex-start;
gap: 32rpx;
width: 100%;
}
.expanded .score-overview {
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
margin-bottom: 20rpx;
/* align-items: center; */
}
.expanded .total-score {
margin-bottom: 48rpx;
transform: scale(1.2);
/* margin-bottom: 48rpx; */
}
.expanded .score-details {
width: 100%;
padding: 0 32rpx;
opacity: 1;
transform: translateY(0);
}
.score-details {
@@ -422,54 +401,126 @@
}
/* 单词评分列表样式 */
.match-tag-legend {
padding: 16rpx;
border-top: 1rpx solid #f0f0f0;
}
.legend-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.legend-title {
font-size: 24rpx;
color: #333;
white-space: nowrap;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.color-box {
width: 20rpx;
height: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 4rpx;
}
.legend-text {
font-size: 22rpx;
color: #666;
}
.word-scores-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 24rpx;
gap: 16rpx;
max-height: calc(80vh - 400rpx);
overflow-y: auto;
border-top: 1rpx solid #f0f0f0;
margin-top: 32rpx;
padding: 16rpx;
}
.word-score-item {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 16rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
transition: background-color 0.3s ease;
}
.word-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx;
background: #f8f8f8;
border-radius: 12rpx;
}
.phone-infos {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
padding-top: 8rpx;
border-top: 1rpx dashed #e0e0e0;
}
.phone-info-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 4rpx 8rpx;
border-radius: 4rpx;
border: 1rpx solid #e0e0e0;
}
.phone-text {
font-size: 24rpx;
color: #333;
}
.phone-score {
font-size: 22rpx;
color: #666;
}
.word-text {
font-size: 32rpx;
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.word-score-details {
display: flex;
flex-direction: column;
gap: 12rpx;
gap: 24rpx;
}
.word-score-row {
display: flex;
align-items: center;
gap: 12rpx;
gap: 8rpx;
justify-content: flex-end;
}
.word-score-label {
font-size: 24rpx;
font-size: 22rpx;
color: #666;
}
.word-score-value {
font-size: 28rpx;
font-size: 22rpx;
color: #333;
font-weight: 500;
min-width: 80rpx;
text-align: right;
}

View File

@@ -1,5 +1,5 @@
// result.ts - 结果展示页面(动画效果和识别结果展示)
import { DictItem, IRecognitionResult, SenseItem } from 'miniprogram/types/app';
import { DictItem, IRecognitionResponse, SenseItem } from 'miniprogram/types/app';
import apiManager from '../../utils/api'
import imageManager from '../../utils/image'
@@ -31,7 +31,7 @@ Page({
cardBubbles: [] as BubbleData[], // 卡片气泡数据
// 识别结果
recognitionResult: null as IRecognitionResult | null,
recognitionResult: null as IRecognitionResponse | null,
bubbleList: [] as BubbleData[],
// 单词详情缓存
@@ -267,10 +267,11 @@ Page({
} catch (error) {
console.error('图片识别失败:', error)
this.hideCardBubbles()
// 更详细的错误信息
let errorMessage = '识别失败,请重试'
if (error instanceof Error) {
errorMessage = error.message || '识别失败,请重试'
if (error.message.includes('登录')) {
errorMessage = '登录已过期,请重新登录'
} else if (error.message.includes('网络')) {
@@ -309,15 +310,15 @@ Page({
}, 500)
// 生成气泡数据 - 添加防护性检查
const refWords = recognitionResult.res.ref_word || []
const bubbleList = this.generateBubbleData(refWords)
// const refWords = recognitionResult.result?.level1?.ref_word || []
// const bubbleList = this.generateBubbleData(refWords)
console.log('生成的气泡数据:', bubbleList)
// console.log('生成的气泡数据:', bubbleList)
setTimeout(() => {
this.setData({
imageId: recognitionResult.image_id,
bubbleList,
// bubbleList,
animationStage: 'result',
showResultArea: true
})
@@ -391,65 +392,65 @@ Page({
},
// 点击气泡 - 显示单词详情
async handleBubbleClick(e: any) {
const { word } = e.currentTarget.dataset
if (!word) return
// async handleBubbleClick(e: any) {
// const { word } = e.currentTarget.dataset
// if (!word) return
// 检查是否已经缓存了该单词的详情
if (this.data.wordDetailCache[word]) {
console.log('使用缓存的单词详情:', word)
const wordDetail = this.data.wordDetailCache[word]
// // 检查是否已经缓存了该单词的详情
// if (this.data.wordDetailCache[word]) {
// console.log('使用缓存的单词详情:', word)
// const wordDetail = this.data.wordDetailCache[word]
// 处理单词详情数据简化WXML中的访问
const processedData = this.processWordDetail(wordDetail)
// // 处理单词详情数据简化WXML中的访问
// const processedData = this.processWordDetail(wordDetail)
this.setData({
currentWord: word,
currentWordDetail: wordDetail,
processedWordDetail: processedData,
displayType: 'word',
isLoadingContent: false
})
return
}
// this.setData({
// currentWord: word,
// currentWordDetail: wordDetail,
// processedWordDetail: processedData,
// displayType: 'word',
// isLoadingContent: false
// })
// return
// }
this.setData({
currentWord: word,
displayType: 'word',
isLoadingContent: true,
displayContent: ''
})
// this.setData({
// currentWord: word,
// displayType: 'word',
// isLoadingContent: true,
// displayContent: ''
// })
try {
// 调用API获取单词详情
const wordDetail: any = await apiManager.getWordDetail(word)
// try {
// // 调用API获取单词详情
// const wordDetail: any = await apiManager.getWordDetail(word)
// 处理单词详情数据简化WXML中的访问
const processedData = this.processWordDetail(wordDetail)
// // 处理单词详情数据简化WXML中的访问
// const processedData = this.processWordDetail(wordDetail)
// 将单词详情缓存起来
const wordDetailCache = {
...this.data.wordDetailCache,
[word]: wordDetail
}
// // 将单词详情缓存起来
// const wordDetailCache = {
// ...this.data.wordDetailCache,
// [word]: wordDetail
// }
this.setData({
currentWordDetail: wordDetail,
processedWordDetail: processedData,
isLoadingContent: false,
wordDetailCache
})
// this.setData({
// currentWordDetail: wordDetail,
// processedWordDetail: processedData,
// isLoadingContent: false,
// wordDetailCache
// })
} catch (error) {
console.error('获取单词详情失败:', error)
this.setData({
displayContent: '获取单词详情失败,请稍后重试',
isLoadingContent: false,
currentWordDetail: null,
processedWordDetail: null
})
}
},
// } catch (error) {
// console.error('获取单词详情失败:', error)
// this.setData({
// displayContent: '获取单词详情失败,请稍后重试',
// isLoadingContent: false,
// currentWordDetail: null,
// processedWordDetail: null
// })
// }
// },
// 处理单词详情数据简化WXML中的访问
processWordDetail(wordDetail: any): ProcessedDictItem[] | null {
@@ -496,7 +497,8 @@ Page({
}
// 防护性检查确保description存在且是数组
if (!recognitionResult.res.description || !Array.isArray(recognitionResult.res.description)) {
console.log('--lisa0-check', recognitionResult)
if (!recognitionResult?.result?.level1?.desc_en || !Array.isArray(recognitionResult?.result?.level1?.desc_en)) {
console.warn('description 为空或不是数组,使用默认文本')
this.setData({
displayType: 'description',
@@ -508,7 +510,7 @@ Page({
}
// 过滤空值并格式化描述内容
const validDescriptions = recognitionResult.res.description
const validDescriptions = recognitionResult?.result?.level1?.desc_en
.filter(desc => desc && typeof desc === 'string' && desc.trim().length > 0)
.map(desc => {
// 确保首字母大写
@@ -730,32 +732,6 @@ Page({
}
},
// 临时调试方法 - 模拟识别结果(用于测试)
debugSimulateResult() {
console.log('模拟识别结果 - 用于调试')
const mockResult: IRecognitionResult = {
id: 'mock_001',
res: {
description: [
'这是一张包含英文单词的图片',
'图片中识别到了多个关键单词'
],
ref_word: [
'hello',
'world',
'test',
'example',
'sample'
]
}
}
console.log('设置模拟结果:', mockResult)
this.setData({ recognitionResult: mockResult })
this.showRecognitionResult()
},
// 点击描述文本跳转到 assessment 页面
handleDescriptionClick(e: WechatMiniprogram.BaseEvent) {
const content = e.currentTarget.dataset.content

View File

@@ -11,11 +11,11 @@
<view wx:for="{{cardBubbles}}" wx:key="id" class="bubble-item card-bubble card-bubble-{{index + 1}} {{item.fadeOut ? 'fade-out' : ''}}"></view>
</view>
<!-- 结果气泡 -->
<view wx:if="{{animationStage === 'result' && bubbleList.length > 0}}" class="bubbles-container result-bubbles">
<!-- <view wx:if="{{animationStage === 'result' && bubbleList.length > 0}}" class="bubbles-container result-bubbles">
<view wx:for="{{bubbleList}}" wx:key="id" class="bubble-item bubble-interactive result-bubble result-bubble-{{index + 1}}" data-word="{{item.word}}" bindtap="handleBubbleClick">
<text class="bubble-text">{{item.word}}</text>
</view>
</view>
</view> -->
</view>
<!-- 结果显示区域 -->
<view wx:if="{{showResultArea}}" class="result-display-area display-area">

View File

@@ -1,12 +0,0 @@
{
"navigationBarTitleText": "识别结果",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f8f9fa",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -1,336 +0,0 @@
// result_show.ts - 结果展示页面(仅展示识别结果)
import { DictItem, IRecognitionResult, SenseItem } from 'miniprogram/types/app';
import apiManager from '../../utils/api'
import { FILE_BASE_URL } from '../../utils/config'
// 为DictItem添加辅助属性用于WXML中简化访问
interface ProcessedDictItem extends DictItem {
primaryWord: string; // 主要单词
ukPronunciation: string | null; // 英式发音
usPronunciation: string | null; // 美式发音
senses: SenseItem[];
}
Page({
data: {
imageId: '',
imagePath: '',
// 识别结果
recognitionResult: null as IRecognitionResult | null,
bubbleList: [] as Array<{id: string; word?: string | null}>,
// 单词详情缓存
wordDetailCache: {} as Record<string, any>,
// 显示内容
displayContent: [] as string[],
errorTip: '',
displayType: '', // description | word
currentWord: '',
currentWordDetail: null as any,
processedWordDetail: null as ProcessedDictItem[] | null, // 处理后的单词详情用于WXML简化访问
isLoadingContent: false,
},
onLoad(options: any) {
console.log('结果展示页面加载', options)
// 获取传入的图片ID
const imageId = options.imageId
if (!imageId) {
wx.showToast({
title: '参数错误',
icon: 'none'
})
this.goBack()
return
}
this.setData({ imageId })
// 获取识别结果
this.loadRecognitionResult(imageId)
},
// 加载识别结果
async loadRecognitionResult(imageId: string) {
try {
const { task_id } = await apiManager.recognizeImageAsync(imageId)
// 轮询获取识别结果
const result = await apiManager.recognizeGetTask(task_id)
// 设置识别结果
this.setData({ recognitionResult: result })
// 设置图片路径
this.setData({ imagePath: `${FILE_BASE_URL}/${result.file_id}` })
// 生成气泡数据
const refWords = result.res.ref_word || []
const bubbleList = this.generateBubbleData(refWords)
this.setData({
bubbleList,
displayType: 'description'
})
// 显示描述内容
this.showDescription()
} catch (error) {
console.error('获取识别结果失败:', error)
wx.showToast({
title: '获取结果失败',
icon: 'none'
})
this.goBack()
}
},
// 生成气泡数据
generateBubbleData(refWords: string[]) {
if (!refWords || !Array.isArray(refWords) || refWords.length === 0) {
return []
}
return refWords.map((word, index) => ({
id: `bubble_${index}`,
word: word
}))
},
// 点击气泡 - 显示单词详情
async handleBubbleClick(e: any) {
const { word } = e.currentTarget.dataset
if (!word) return
// 检查是否已经缓存了该单词的详情
if (this.data.wordDetailCache[word]) {
const wordDetail = this.data.wordDetailCache[word]
const processedData = this.processWordDetail(wordDetail)
this.setData({
currentWord: word,
currentWordDetail: wordDetail,
processedWordDetail: processedData,
displayType: 'word',
isLoadingContent: false
})
return
}
this.setData({
currentWord: word,
displayType: 'word',
isLoadingContent: true,
errorTip: '',
displayContent: []
})
try {
// 调用API获取单词详情
const wordDetail: any = await apiManager.getWordDetail(word)
// 处理单词详情数据简化WXML中的访问
const processedData = this.processWordDetail(wordDetail)
// 将单词详情缓存起来
const wordDetailCache = {
...this.data.wordDetailCache,
[word]: wordDetail
}
this.setData({
currentWordDetail: wordDetail,
processedWordDetail: processedData,
isLoadingContent: false,
wordDetailCache
})
} catch (error) {
console.error('获取单词详情失败:', error)
this.setData({
errorTip: '获取单词详情失败,请稍后重试',
isLoadingContent: false,
currentWordDetail: null,
processedWordDetail: null
})
}
},
// 处理单词详情数据简化WXML中的访问
processWordDetail(wordDetail: any): ProcessedDictItem[] | null {
if (!wordDetail.dict_list) return null;
return wordDetail.dict_list.map((dictItem: DictItem) => {
// 提取发音信息
let ukPronunciation = null;
let usPronunciation = null;
if (dictItem.pronunciations) {
ukPronunciation = dictItem.pronunciations.uk_ipa || null;
usPronunciation = dictItem.pronunciations.us_ipa || null;
}
// 处理 examples
dictItem.senses.forEach(sense => {
sense.examples = sense.examples || [];
sense.examples = sense.examples.map(example => {
example.en = example.en || '';
example.en = example.en.replace(/[^a-zA-Z\s]/g, '').trim();
return {
...example
};
});
});
return {
...dictItem,
primaryWord: this.data.currentWord,
ukPronunciation,
usPronunciation,
senses: dictItem.senses
} as ProcessedDictItem;
});
},
// 点击图片 - 显示描述
handleImageClick() {
this.showDescription()
},
// 点击描述文本跳转到 assessment 页面
handleDescriptionClick(e: WechatMiniprogram.BaseEvent) {
const index = e.currentTarget.dataset.index
wx.navigateTo({
url: `/pages/assessment/assessment?imageId=${this.data.imageId}&index=${index}`
});
},
// 显示描述内容
showDescription() {
const { recognitionResult } = this.data
if (!recognitionResult) {
console.error('recognitionResult 为空,无法显示描述')
return
}
// 防护性检查确保description存在且是数组
if (!recognitionResult.res.description || !Array.isArray(recognitionResult.res.description)) {
this.setData({
displayType: 'description',
errorTip: '暂无描述信息',
currentWord: '',
isLoadingContent: false
})
return
}
// 过滤空值并格式化描述内容
const validDescriptions = recognitionResult.res.description
.filter(desc => desc && typeof desc === 'string' && desc.trim().length > 0)
.map(desc => {
// 确保首字母大写
return desc.charAt(0).toUpperCase() + desc.slice(1)
})
if (validDescriptions.length) {
this.setData({
displayContent: validDescriptions,
errorTip: ''
})
} else {
this.setData({
errorTip: '暂无描述信息'
})
}
this.setData({
displayType: 'description',
currentWord: '',
isLoadingContent: false
})
},
// 返回上一页
goBack() {
if (getCurrentPages().length > 1) {
wx.navigateBack()
} else {
wx.switchTab({
url: '/pages/upload/upload'
})
}
},
// 播放音频
playAudio(e: any) {
const { audio } = e.currentTarget.dataset
if (audio) {
apiManager.playAudio(audio).catch(error => {
console.error('播放音频失败:', error)
if (error.message !== '音频正在播放中,请等待播放完成') {
wx.showToast({
title: '音频播放失败',
icon: 'none'
})
}
})
}
},
// 切换释义翻译显示
toggleDefTranslation(e: WechatMiniprogram.BaseEvent) {
const definition = e.currentTarget.dataset.definition;
if (!definition) return false;
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null;
if (processedWordDetail) {
for (const dictItem of processedWordDetail) {
if (dictItem.senses) {
for (const sense of dictItem.senses) {
if (sense.definitions) {
const def = sense.definitions.find(d => d.en === definition.en);
if (def) {
def.showTranslation = !def.showTranslation;
this.setData({ processedWordDetail });
return false;
}
}
}
}
}
}
return false;
},
// 切换例句翻译显示
toggleExampleTranslation(e: WechatMiniprogram.BaseEvent) {
const example = e.currentTarget.dataset.example;
if (!example) return false;
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null;
if (processedWordDetail) {
for (const dictItem of processedWordDetail) {
if (dictItem.senses) {
for (const sense of dictItem.senses) {
if (sense.examples) {
const ex = sense.examples.find(e => e.en === example.en);
if (ex) {
ex.showTranslation = !ex.showTranslation;
this.setData({ processedWordDetail });
return false;
}
}
}
}
}
}
return false;
},
// 阻止标签点击事件
preventTagChange() {
return false;
}
})

View File

@@ -1,133 +0,0 @@
<!-- result_show.wxml - 结果展示页面 -->
<view class="result-container">
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 图片卡片容器(用于光晕效果) -->
<view class="image-card-container">
<!-- 图片显示 -->
<image class="result-image glow-effect" src="{{imagePath}}" mode="aspectFill" bindtap="handleImageClick" />
<!-- 结果气泡 -->
<view class="bubbles-container result-bubbles">
<view wx:for="{{bubbleList}}" wx:key="id" class="bubble-item bubble-interactive result-bubble result-bubble-{{index + 1}}" data-word="{{item.word}}" bindtap="handleBubbleClick" style="background: #007AFF; color: #fff;">
<text class="bubble-text">{{item.word}}</text>
</view>
</view>
</view>
<!-- 结果显示区域 -->
<view class="result-display-area display-area">
<!-- 内容区域 -->
<scroll-view class="display-content" scroll-y="true" enable-back-to-top="true" show-scrollbar="true" scroll-with-animation="true" style="flex: 1;">
<!-- 显示内容 -->
<view wx:if="{{!isLoadingContent}}" class="content-area">
<!-- 显示图片描述内容 -->
<view wx:if="{{displayType === 'description'}}" class="content-text">
<view wx:if="{{displayContent.length}}">
<view class="text-primary" wx:for="{{displayContent}}" wx:key="index" bindtap="handleDescriptionClick" data-index="{{index}}">
<text>{{item}}</text>
<t-icon name="microphone-1" class="microphone" size="40rpx" />
</view>
</view>
<text class="text-primary" wx:else>{{errorTip}}</text>
</view>
<!-- 显示单词详情内容 -->
<view wx:elif="{{displayType === 'word' && processedWordDetail}}" class="word-detail-container">
<!-- 遍历处理后的dict_list数组 -->
<view wx:for="{{processedWordDetail}}" wx:key="index" class="dict-item">
<!-- 标题栏 -->
<view class="word-header">
<view class="word-header-container">
<text class="word-title">{{currentWord}}</text>
<!-- 音标 -->
<view class="pronunciations">
<!-- 如果两种发音都存在 -->
<text wx:if="{{item.ukPronunciation && item.usPronunciation}}" class="ipa">
/{{item.ukPronunciation}} $ {{item.usPronunciation}}/
</text>
<!-- 只有 uk 发音 -->
<text wx:elif="{{item.ukPronunciation}}" class="ipa">
/{{item.ukPronunciation}}/
</text>
<!-- 只有 us 发音 -->
<text wx:elif="{{item.usPronunciation}}" class="ipa">
/{{item.usPronunciation}}/
</text>
<!-- 音频按钮 -->
<t-icon class="ipa-audio" wx:if="{{item.ukPronunciation && item.pronunciations && item.pronunciations.uk_audio}}" bind:click="playAudio" data-audio="{{item.pronunciations.uk_audio}}" name="sound" size="40rpx" data-name="{{item}}" />
<t-icon class="ipa-audio" wx:if="{{item.usPronunciation && item.pronunciations && item.pronunciations.us_audio}}" bind:click="playAudio" data-audio="{{item.pronunciations.us_audio}}" name="sound" size="40rpx" data-name="{{item}}" style="margin-left: 10rpx;" />
</view>
</view>
<!-- 频率标签 -->
<view class="frequency-tags">
<t-tag wx:if="{{item.transitive && item.transitive.length > 0}}" wx:for="{{item.transitive}}" wx:key="index" variant="light" theme="success" style="margin-right: 12rpx;">
{{item}}
</t-tag>
<t-tag wx:if="{{item.part_of_speech}}" variant="light" theme="success" style="margin-right: 12rpx;">
{{item.part_of_speech}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.level_tag}}" variant="light" theme="danger" style="margin-left: 10rpx;font-family:arial, helvetica;">
{{item.frequency.level_tag}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.spoken_tag}}" variant="light" theme="warning" style="margin-left: 10rpx;">
{{item.frequency.spoken_tag}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.written_tag}}" variant="light" theme="warning" style="margin-left: 10rpx;">
{{item.frequency.written_tag}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.level_tag}}" variant="light" theme="danger" style="margin-left: 10rpx;font-family:arial, helvetica;">
{{item.frequency.level_tag}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.spoken_tag}}" variant="light" theme="warning" style="margin-left: 10rpx;">
{{item.frequency.spoken_tag}}
</t-tag>
<t-tag wx:if="{{item.frequency && item.frequency.written_tag}}" variant="light" theme="warning" style="margin-left: 10rpx;">
{{item.frequency.written_tag}}
</t-tag>
</view>
</view>
<!-- senses内容 -->
<view wx:if="{{item.senses}}" class="senses-container">
<t-collapse wx:for="{{item.senses}}" wx:key="index" class="word-collapse" defaultValue="{{item.senses[0].id}}" expandIcon>
<t-collapse-panel value="{{item.id}}" expand-icon="{{item.examples && item.examples.length > 0}}" disabled="{{item.examples && item.examples.length < 1}}">
<view class="word-collapse-header" slot="header">
<view wx:if="{{item.countability && item.countability.length > 0}}" style="display: inline;">
<t-tag wx:for="{{item.countability}}" wx:key="index" variant="light" style="margin-right: 12rpx;">
{{item}}
</t-tag>
</view>
<view wx:if="{{item.signpost_en && item.signpost_cn}}" style="display: inline; margin-right: 12rpx;">
<t-check-tag variant="light" default-checked content="{{ [item.signpost_en, item.signpost_en + ' / ' + item.signpost_cn ] }}" catch:tap="preventTagChange" />
</view>
<text>{{item.definitions[0].en}}</text>
<view wx:if="{{item.definitions[0] && item.definitions[0].cn}}" style="display: inline-block; margin-left: 12rpx; vertical-align: middle;height: 46rpx;">
<t-icon wx:if="{{!item.definitions[0].showTranslation}}" bind:click="toggleDefTranslation" catch:tap="preventTagChange" data-definition="{{item.definitions[0]}}" name="translate" size="36rpx" />
</view>
<text wx:if="{{item.definitions[0].cn && item.definitions[0].showTranslation}}" style="margin-left: 12rpx;color: #007AFF;">
{{item.definitions[0].cn}}
</text>
</view>
<view wx:for="{{item.examples}}" wx:key="index" style="vertical-align: middle;padding: 8rpx 0;">
<text>{{item.en}}</text>
<view wx:if="{{item.audio}}" class="word-collapse-content-icon">
<t-icon bind:click="playAudio" data-audio="{{item.audio}}" name="sound" size="34rpx" data-name="{{item}}" />
</view>
<view wx:if="{{item.cn && !item.showTranslation}}" class="word-collapse-content-icon">
<t-icon bind:click="toggleExampleTranslation" data-example="{{item}}" name="translate" size="34rpx" />
</view>
<text wx:if="{{item.cn && item.showTranslation}}" style="color: #007AFF;">
{{item.cn}}
</text>
</view>
</t-collapse-panel>
</t-collapse>
</view>
</view>
</view>
<!-- 默认提示 -->
<view wx:else class="content-text">
<text class="text-primary">点击图片查看描述,点击气泡查看单词详情</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>

View File

@@ -1,838 +0,0 @@
/* result.wxss - 结果展示页面样式(扁平化设计) */
.result-container {
min-height: 100vh;
background-color: #f5f5f5;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main-content {
min-height: 100vh;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
}
/* 图片显示 */
.result-image {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
object-fit: cover;
}
.glow-effect {
box-shadow: 0 8rpx 32rpx rgba(0, 122, 255, 0.3);
}
/* 图片卡片容器 */
.image-card-container {
z-index: 100;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 15%;
left: 50%;
transform: translate(-50%, -50%);
width: 300rpx;
height: 300rpx;
opacity: 1;
}
/* 通用气泡容器 */
.bubbles-container {
position: fixed;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 50;
}
/* 通用气泡项 */
.bubble-item {
position: absolute;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.8s ease;
}
.bubble-interactive {
position: absolute;
display: flex;
border-radius: 16rpx;
align-items: center;
justify-content: center;
color: white;
font-size: 24rpx;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
animation: showBubble .8s forwards ease-in-out,
bubbleJump 2s ease-in-out infinite !important;
pointer-events: auto;
padding: 8rpx 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.3);
}
.bubble-interactive:active {
transform: scale(0.9);
background: #0056CC;
}
@keyframes bubbleJump {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-10rpx) scale(1.05); }
}
@keyframes showBubble {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
.result-bubble-1 {
left: -80rpx;
top: -25rpx;
animation-delay: 0s;
z-index: 101;
}
/* 位置5左侧中下方 */
.result-bubble-2 {
left: -80rpx;
top: 170rpx;
animation-delay: 0.4s;
z-index: 101;
}
/* 位置4右侧中上方 */
.result-bubble-3 {
right: -80rpx;
top: 80rpx;
animation-delay: 0.2s;
z-index: 101;
}
/* 位置8右侧最下方 */
.result-bubble-4 {
right: -80rpx;
top: 268rpx;
animation-delay: 0.6s;
z-index: 101;
}
.bubble-text {
font-size: 32rpx;
font-weight: 600;
text-align: center;
word-break: break-all;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
}
.result-bubbles {
z-index: 50;
pointer-events: auto;
}
.bubble-interactive {
pointer-events: auto;
cursor: pointer;
}
.result-bubble {
width: auto;
height: auto;
min-width: 80rpx;
min-height: 40rpx;
padding: 8rpx 16rpx;
background: #007AFF;
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.3);
}
.result-bubble:active {
transform: scale(0.9);
background: #0056CC;
}
@keyframes bubbleJump {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-10rpx) scale(1.05); }
}
.image-card-container {
position: fixed;
top: 15%;
left: 50%;
transform: translate(-50%, -50%);
width: 300rpx;
height: 300rpx;
opacity: 1;
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
z-index: 100;
}
.result-image {
width: 100%;
height: 100%;
border-radius: 16rpx;
object-fit: cover;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.glow-effect {
box-shadow: 0 8rpx 32rpx rgba(0, 122, 255, 0.3);
}
/* 结果显示区域 */
.result-display-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
height: 60vh;
max-height: 60vh;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1000;
}
.display-header {
padding: 32rpx 32rpx 0;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.display-tabs {
display: flex;
gap: 16rpx;
}
.tab-btn {
flex: 1;
background: none;
border: none;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
color: #666666;
transition: all 0.3s ease;
}
.tab-btn.active {
background-color: #007AFF;
color: #ffffff;
}
.tab-word {
font-size: 24rpx;
opacity: 0.8;
}
.display-content {
flex: 1;
height: auto;
box-sizing: border-box;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.content-area {
height: 100%;
width: 100%;
box-sizing: border-box;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
padding: 24rpx;
}
.content-text {
font-size: 28rpx;
color: #333333;
line-height: 1.6;
padding: 24rpx;
}
/* 单词详情样式 */
.word-detail-container {
flex: 1;
overflow-y: auto;
height: 100%;
width: 100%;
box-sizing: border-box;
padding: 24rpx;
}
.dict-item {
margin-bottom: 40rpx;
border-radius: 16rpx;
width: 100%;
box-sizing: border-box;
}
.word-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e9ecef;
}
.word-header-container {
display: flex;
align-items: center;
justify-content: flex-start;
}
.word-title {
font-size: 36rpx;
font-weight: bold;
color: #212529;
}
.frequency-tags {
display: inline-block;
margin-top: 10rpx;
}
.sense-item {
margin-bottom: 20rpx;
padding: 15rpx;
background-color: white;
border-radius: 12rpx;
}
.signpost {
padding: 10rpx;
background-color: #e9ecef;
border-radius: 8rpx;
margin-bottom: 15rpx;
cursor: pointer;
}
.signpost-en {
font-weight: bold;
color: #495057;
margin-right: 10rpx;
}
.signpost-cn {
color: #6c757d;
}
.definitions {
padding: 10rpx 0;
cursor: pointer;
}
.definition-en {
font-size: 28rpx;
color: #212529;
margin-bottom: 8rpx;
display: block;
}
.definition-cn {
font-size: 26rpx;
color: #6c757d;
display: block;
}
.examples-section {
margin-top: 15rpx;
}
.examples-toggle {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
cursor: pointer;
}
.toggle-icon {
font-size: 24rpx;
color: #6c757d;
}
.examples-list {
margin-top: 15rpx;
padding: 10rpx;
background-color: #ffffff;
border-radius: 8rpx;
}
.example-item {
padding: 10rpx 0;
border-bottom: 1rpx solid #e9ecef;
}
.example-item:last-child {
border-bottom: none;
}
.example-en {
font-size: 26rpx;
color: #212529;
display: block;
margin-bottom: 4rpx;
}
.example-cn {
font-size: 24rpx;
color: #6c757d;
display: block;
}
.toggle-tip {
font-size: 22rpx;
color: #007AFF;
font-style: italic;
}
.dict-item {
margin-bottom: 40rpx;
border-radius: 16rpx;
width: 100%;
box-sizing: border-box;
}
.word-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e9ecef;
}
.word-header-container {
display: flex;
align-items: center;
justify-content: flex-start;
}
.word-title {
font-size: 36rpx;
font-weight: bold;
color: #212529;
}
.frequency-tags {
display: inline-block;
margin-top: 10rpx;
}
.tag {
display: inline-block;
padding: 4rpx 12rpx;
font-size: 20rpx;
border-radius: 8rpx;
margin-right: 10rpx;
}
.level-tag {
background-color: #007AFF;
color: white;
}
.spoken-tag {
background-color: #28a745;
color: white;
}
.written-tag {
background-color: #ffc107;
color: #212529;
}
.level-tag {
background-color: #007AFF;
color: white;
}
.spoken-tag {
background-color: #28a745;
color: white;
}
.written-tag {
background-color: #ffc107;
color: #212529;
}
.pronunciations {
margin-top: 10rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.ipa {
font-family: arial, helvetica, sans-serif;
font-size: 24rpx;
color: #495057;
margin-left: 12rpx;
}
.ipa-audio {
padding: 12rpx;
}
.audio-btn {
background-color: #007AFF;
color: white;
border: none;
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 20rpx;
margin-left: 10rpx;
cursor: pointer;
}
.audio-btn.small {
padding: 4rpx 12rpx;
font-size: 18rpx;
}
.audio-btn:active {
background-color: #0056CC;
}
.senses-container {
padding: 10rpx 0;
}
.sense-item {
margin-bottom: 20rpx;
padding: 15rpx;
background-color: white;
border-radius: 12rpx;
}
.signpost {
padding: 10rpx;
background-color: #e9ecef;
border-radius: 8rpx;
margin-bottom: 15rpx;
cursor: pointer;
}
.signpost-en {
font-weight: bold;
color: #495057;
margin-right: 10rpx;
}
.signpost-cn {
color: #6c757d;
}
.definitions {
padding: 10rpx 0;
cursor: pointer;
}
.definition-en {
font-size: 28rpx;
color: #212529;
margin-bottom: 8rpx;
display: block;
}
.definition-cn {
font-size: 26rpx;
color: #6c757d;
display: block;
}
.examples-section {
margin-top: 15rpx;
}
.examples-toggle {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
cursor: pointer;
}
.toggle-icon {
font-size: 24rpx;
color: #6c757d;
}
.examples-list {
margin-top: 15rpx;
padding: 10rpx;
background-color: #ffffff;
border-radius: 8rpx;
}
.example-item {
padding: 10rpx 0;
border-bottom: 1rpx solid #e9ecef;
}
.example-item:last-child {
border-bottom: none;
}
.example-en {
font-size: 26rpx;
color: #212529;
display: block;
margin-bottom: 4rpx;
}
.example-cn {
font-size: 24rpx;
color: #6c757d;
display: block;
}
.toggle-tip {
font-size: 22rpx;
color: #007AFF;
font-style: italic;
}
.word-collapse {
--td-cell-vertical-padding: 8rpx;
--td-cell-horizontal-padding: 0rpx;
--td-collapse-horizontal-padding: 0rpx;
--td-collapse-content-padding: 8rpx;
--td-text-color-disabled: rgba(0,0,0,.9);
margin-bottom: 20rpx;
}
.word-collapse-content-icon {
width: 40rpx;
height: 40rpx;
display: inline-block;
vertical-align: middle;
padding: 0 6rpx;
}
/* 加载状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f3f3f3;
border-top: 4rpx solid #007AFF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #666666;
}
/* 气泡列表 */
.bubble-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 30rpx;
}
.bubble-item {
padding: 16rpx 30rpx;
background: #ffffff;
border-radius: 30rpx;
font-size: 28rpx;
color: #333;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.bubble-item:active {
transform: scale(0.95);
background: #f0f0f0;
}
/* 结果显示区域 */
.result-display-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
border-top-left-radius: 24rpx;
border-top-right-radius: 24rpx;
height: 60vh;
max-height: 60vh;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 1000;
}
.display-content {
flex: 1;
height: auto;
max-height: none;
box-sizing: border-box;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.content-text {
font-size: 28rpx;
color: #333333;
line-height: 1.6;
padding: 24rpx;
}
.content-area {
height: 100%;
width: 100%;
box-sizing: border-box;
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
}
/* 单词详情样式 */
.word-detail-container {
flex: 1;
overflow-y: auto;
height: 100%;
width: 100%;
box-sizing: border-box;
padding: 24rpx;
}
.dict-item {
margin-bottom: 40rpx;
border-radius: 16rpx;
width: 100%;
box-sizing: border-box;
}
.word-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #e9ecef;
}
.word-header-container {
display: flex;
align-items: center;
justify-content: flex_start;
}
.word-title {
font-size: 36rpx;
font-weight: bold;
color: #212529;
}
.frequency-tags {
display: inline-block;
margin-top: 10rpx;
}
.pronunciations {
margin-top: 10rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.ipa {
font-family: arial, helvetica, sans-serif;
font-size: 24rpx;
color: #495057;
margin-left: 12rpx;
}
.ipa-audio {
padding: 12rpx;
}
.senses-container {
padding: 10rpx 0;
}
.word-collapse {
--td-cell-vertical-padding: 8rpx;
--td-cell-horizontal-padding: 0rpx;
--td-collapse-horizontal-padding: 0rpx;
--td-collapse-content-padding: 8rpx;
--td-text-color-disabled: rgba(0,0,0,.9);
}
.word-collapse-content-icon {
width: 40rpx;
height: 40rpx;
display: inline-block;
vertical-align: middle;
padding: 0 6rpx;
}
.microphone {
display: inline-block;
vertical-align: middle;
}
.text-primary {
margin-bottom: 20rpx;
}

View File

@@ -31,14 +31,6 @@ interface ILoginResponse {
dict_level?: string; // 新增字段,用户词典等级配置
}
// 图片识别结果接口
interface IRecognitionResult {
id: string | number;
res:{
description: string[];
ref_word: string[];
}
}
// 添加单词详情的数据接口定义
interface DictItem {
@@ -145,7 +137,6 @@ export {
IAppOption,
IUserInfo,
ILoginResponse,
IRecognitionResult,
DictItem,
SenseItem,
ExampleItem,

View File

@@ -29,14 +29,23 @@ export interface IApiResponse<T> {
data: T
}
// 识别结果接口
export interface IRecognitionResult {
text: string
words: Array<{
word: string
start: number
end: number
}>
export interface IRecognitionResponse {
task_id: string
status: string
error_message?: string
image_id?: string
result?: IRecognitionResponseResult
}
export interface IRecognitionResponseResultLevel {
desc_en: string[]
desc_zh: string[]
}
export interface IRecognitionResponseResult {
level1: IRecognitionResponseResultLevel,
level2: IRecognitionResponseResultLevel,
level3: IRecognitionResponseResultLevel,
}
// 单词详情接口
@@ -56,6 +65,16 @@ export interface ExtendedWordDetail {
audio: string
}
export interface YdWordDetail {
ee: {}
ec: {}
discriminate: {}
etym: {}
expand_ec: {}
phrs: {}
simple: {}
}
// 审核历史记录接口
export interface IAuditHistoryResponse {
total: number

View File

@@ -3,11 +3,12 @@ import {
IAppOption,
ILoginResponse,
IApiResponse,
IRecognitionResult,
IRecognitionResponse,
ExtendedWordDetail,
IAuditHistoryResponse,
IUserInfo,
IDailySummaryResponse
IDailySummaryResponse,
YdWordDetail
} from '../types/app';
import { BASE_URL } from './config';
@@ -317,6 +318,11 @@ class ApiManager {
this.handleTokenExpired()
reject(new Error('登录已过期'))
}
} else if (res.statusCode === 403) {
const response = res.data as IApiResponse<T>
const errorMsg = response.msg || '请求失败'
console.error('403 错误:', errorMsg, response)
reject(new Error(errorMsg))
} else {
console.error('HTTP错误:', res.statusCode, res.data)
wx.showToast({
@@ -704,24 +710,6 @@ class ApiManager {
})
}
// 图片识别第二步通过文件ID进行识别
private async recognizeImage(fileId: string, type: string = 'word'): Promise<IRecognitionResult> {
console.log('开始图片识别请求:', { fileId, type })
// 获取当前的词典等级配置
const app = getApp<IAppOption>()
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY'
const response = await this.request<IRecognitionResult>('/api/v1/image/recognize', 'POST', {
file_id: fileId,
type: type,
dict_level: dictLevel // 添加词典等级参数
})
console.log('图片识别成功:', response.data)
return response.data
}
// 图片识别第二步通过文件ID进行识别
async recognizeImageAsync(fileId: string, type: string = 'word'): Promise<{task_id: string, status: string, message: string}> {
console.log('开始图片识别请求:', { fileId, type })
@@ -741,16 +729,16 @@ class ApiManager {
}
// 获取识别结果
async recognizeGetTask(taskId: string | number): Promise<{image_id: string, task_id: string, status: string, error_message: string, result?: IRecognitionResult}> {
async recognizeGetTask(taskId: string | number): Promise<IRecognitionResponse> {
const response = await this.request<{image_id: string, task_id: string, status: string, error_message: string, result?: IRecognitionResult}>(`/api/v1/image/recognize/task/${taskId}`, 'GET')
const response = await this.request<IRecognitionResponse>(`/api/v1/image/recognize/task/${taskId}`, 'GET')
console.log('图片识别成功:', response.data)
return response.data
}
// 上传图片并识别(对外接口,整合两个步骤)
async uploadImage(filePath: string, type: string = 'word'): Promise<IRecognitionResult> {
async uploadImage(filePath: string, type: string = 'word'): Promise<IRecognitionResponse> {
try {
// wx.showLoading({ title: '上传中...' })
@@ -766,19 +754,13 @@ class ApiManager {
const { task_id } = await this.recognizeImageAsync(fileId, type)
// 轮询获取识别结果
let recognitionResult: IRecognitionResult | null = null
let recognitionResult: IRecognitionResponse | null = null
while (true) {
try {
const res = await this.recognizeGetTask(task_id)
console.log('--lisa-res', res)
if (res.status === 'completed' && res.result) {
recognitionResult = {
'res': res.result,
'task_id': task_id,
'status': res.status,
'error_message': res.error_message,
'image_id': res.image_id
}
recognitionResult = res
break
}
await new Promise(resolve => setTimeout(resolve, 3000)) // 2秒轮询一次
@@ -820,9 +802,9 @@ class ApiManager {
}
// 获取单词详情
async getWordDetail(word: string): Promise<ExtendedWordDetail> {
async getWordDetail(word: string): Promise<YdWordDetail> {
console.log('获取单词详情')
const response = await this.request<ExtendedWordDetail>(`/api/v1/dict/word/${encodeURIComponent(word)}`)
const response = await this.request<YdWordDetail>(`/api/v1/dict/word/${encodeURIComponent(word)}`)
console.log('获取单词详情成功:', response)
return response.data
}
@@ -1294,6 +1276,12 @@ class ApiManager {
Word: string
PronAccuracy: number,
PronFluency: number,
MatchTag: number,
PhoneInfos?: {
Phone: string,
PronAccuracy: number,
MatchTag: number
}[],
}[]
}
}