This commit is contained in:
Felix
2025-12-10 16:16:34 +08:00
parent fd1d94477c
commit dda6290b15
10 changed files with 73 additions and 1549 deletions

View File

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

View File

@@ -1,18 +0,0 @@
{
"usingComponents": {
"mp-icon": "weui-miniprogram/icon/icon",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-check-tag": "tdesign-miniprogram/check-tag/check-tag",
"t-collapse": "tdesign-miniprogram/collapse/collapse",
"t-collapse-panel": "tdesign-miniprogram/collapse-panel/collapse-panel",
"t-cell": "tdesign-miniprogram/cell/cell"
},
"navigationBarTitleText": "识别结果",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#f8f9fa",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
}

View File

@@ -1,745 +0,0 @@
// result.ts - 结果展示页面(动画效果和识别结果展示)
import { DictItem, IRecognitionResponse, SenseItem } from 'miniprogram/types/app';
import apiManager from '../../utils/api'
import imageManager from '../../utils/image'
// 从全局类型定义中导入接口
interface BubbleData {
id: string;
word?: string | null;
}
// 为DictItem添加辅助属性用于WXML中简化访问
interface ProcessedDictItem extends DictItem {
primaryWord: string; // 主要单词
ukPronunciation: string | null; // 英式发音
usPronunciation: string | null; // 美式发音
senses: SenseItem[];
}
Page({
data: {
imageId: '',
imagePath: '', // 图片路径
imageInfo: null as any,
// 动画状态
animationStage: 'loading', // loading, processing, result
showBubbles: false,
showResultArea: false,
showCardBubbles: false, // 卡片周围闪烁气泡
cardBubbles: [] as BubbleData[], // 卡片气泡数据
// 识别结果
recognitionResult: null as IRecognitionResponse | null,
bubbleList: [] as BubbleData[],
// 单词详情缓存
wordDetailCache: {} as Record<string, any>,
// 显示内容
displayContent: '',
displayType: '', // description | word
currentWord: '',
currentWordDetail: null as any,
processedWordDetail: null as ProcessedDictItem[] | null, // 处理后的单词详情用于WXML简化访问
isLoadingContent: false,
// 图片样式
imageStyle: '',
cardStyle: '',
// 系统信息
systemInfo: null as any
},
onLoad(options: any) {
console.log('结果页面加载', options)
// 获取系统信息
this.getSystemInfo()
// 获取传入的图片路径
const imagePath = decodeURIComponent(options.imagePath || '')
if (!imagePath) {
wx.showToast({
title: '图片路径错误',
icon: 'none'
})
this.goBack()
return
}
this.setData({ imagePath })
this.initImageDisplay()
},
// 获取系统信息
getSystemInfo() {
const systemInfo = wx.getSystemInfoSync()
this.setData({ systemInfo })
},
// 初始化图片显示
async initImageDisplay() {
try {
// 获取图片信息
const imageInfo = await imageManager.getImageInfo(this.data.imagePath)
this.setData({ imageInfo })
// 设置全屏图片样式
this.setFullscreenImageStyle()
// 立即开始动画
this.startProcessingAnimation()
} catch (error) {
console.error('获取图片信息失败:', error)
wx.showToast({
title: '图片加载失败',
icon: 'none'
})
this.goBack()
}
},
// 设置全屏图片样式
setFullscreenImageStyle() {
const { systemInfo, imageInfo } = this.data
if (!systemInfo || !imageInfo) return
const imageStyle = `
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 0;
z-index: 100;
transition: all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
`
const cardStyle = `
position: fixed;
width: 100%;
height: 100%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 1;
`
this.setData({ imageStyle, cardStyle })
},
// 开始处理动画
async startProcessingAnimation() {
this.setData({ animationStage: 'processing' })
// 立即开始识别,但内部控制显示时机
this.startRecognition()
// // 图片缩小到中央卡片
setTimeout(() => {
this.animateToCard()
}, 600)
},
// 动画到卡片状态
animateToCard() {
const imageStyle = `
border-radius: 24rpx;
`
const cardStyle = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 400rpx;
height: 400rpx;
opacity: 1;
transition: all 1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
`
this.setData({
imageStyle,
cardStyle,
showBubbles: true
})
// 延迟生成卡片周围的闪烁气泡
setTimeout(() => {
this.generateCardBubbles()
}, 800) // 与卡片动画同步完成
},
// 生成卡片周围的闪烁气泡
generateCardBubbles() {
console.log('生成卡片周围的闪烁气泡')
// 使用相对定位,不需要复杂的坐标计算
// 气泡位置通过CSS类名控制
const cardBubbles: BubbleData[] = [
{ id: 'card_bubble_1' },
{ id: 'card_bubble_2' },
{ id: 'card_bubble_3' },
{ id: 'card_bubble_4' },
]
// console.log('生成的卡片气泡数据:', cardBubbles)
this.setData({
cardBubbles,
showCardBubbles: true
})
},
// 隐藏卡片气泡
hideCardBubbles() {
// 添加淡出动画效果
const cardBubbles = this.data.cardBubbles.map(bubble => ({
...bubble,
fadeOut: true
}));
this.setData({
cardBubbles
});
// 延迟隐藏气泡,让淡出动画完成
setTimeout(() => {
this.setData({
showCardBubbles: false,
cardBubbles: []
});
}, 1000); // 与CSS过渡时间保持一致
},
// 开始识别
async startRecognition() {
// 记录开始时间
const startTime = Date.now()
const minDuration = 2500 // 最少等待2500ms确保动画完整播放
try {
console.log('开始图片识别,图片路径:', this.data.imagePath)
// 在上传前压缩图片
console.log('开始压缩图片...')
// const compressedImagePath = await imageManager.compressImage(this.data.imagePath, {
// quality: 80,
// maxWidth: 1200,
// maxHeight: 1200
// })
// const result = await apiManager.uploadImage(compressedImagePath)
const result = await apiManager.uploadImage(this.data.imagePath)
// 验证识别结果的数据结构
if (!result) {
throw new Error('识别结果为空')
}
// 设置识别结果
this.setData({ recognitionResult: result })
// 计算已经耗费的时间
const elapsedTime = Date.now() - startTime
const remainingTime = Math.max(0, minDuration - elapsedTime)
console.log(`识别完成,耗时: ${elapsedTime}ms, 还需等待: ${remainingTime}ms`)
// 如果还没到最少等待时间,则等待剩余时间后再显示结果
if (remainingTime > 0) {
setTimeout(() => {
// 隐藏卡片气泡
this.hideCardBubbles()
this.showRecognitionResult()
}, remainingTime)
} else {
// 隐藏卡片气泡
this.hideCardBubbles()
// 如果已经超过最少等待时间,立即显示结果
this.showRecognitionResult()
}
} 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('网络')) {
errorMessage = '网络连接失败,请检查网络'
} else if (error.message.includes('文件')) {
errorMessage = '文件上传失败,请重试'
}
}
wx.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
})
// 失败后允许返回
setTimeout(() => {
this.goBack()
}, 3000)
}
},
// 显示识别结果
showRecognitionResult() {
const { recognitionResult } = this.data
if (!recognitionResult) {
console.error('识别结果为空')
return
}
console.log('开始显示识别结果:', recognitionResult)
// 图片移动到上方
setTimeout(() => {
this.animateToTop()
}, 500)
// 生成气泡数据 - 添加防护性检查
// const refWords = recognitionResult.result?.level1?.ref_word || []
// const bubbleList = this.generateBubbleData(refWords)
// console.log('生成的气泡数据:', bubbleList)
setTimeout(() => {
this.setData({
imageId: recognitionResult.image_id,
// bubbleList,
animationStage: 'result',
// showResultArea: true
})
wx.navigateTo({
url: '/pages/assessment/assessment?imageId=' + recognitionResult.image_id
})
}, 1000)
// 默认显示描述内容
setTimeout(() => {
this.showDescription()
}, 500)
},
// 动画到顶部
animateToTop() {
const cardStyle = `
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);
`
this.setData({ cardStyle })
},
// 生成气泡数据
generateBubbleData(refWords: string[]): BubbleData[] {
console.log('generateBubbleData 输入参数:', refWords)
const { systemInfo } = this.data
if (!systemInfo) {
console.warn('systemInfo 为空,无法生成气泡数据')
return []
}
// 防护性检查确保refWords是数组且不为空
if (!refWords || !Array.isArray(refWords) || refWords.length === 0) {
console.warn('refWords 为空或不是数组,返回空气泡列表')
return []
}
const bubbles: BubbleData[] = []
// 生成气泡数据
for (let i = 0; i < refWords.length; i++) {
const word = refWords[i]
if (!word || typeof word !== 'string') {
console.warn(`refWords[${i}] 数据格式错误:`, word)
continue
}
bubbles.push({
id: `bubble_${i}`,
word: word
})
}
console.log('生成的气泡数据rpx:', bubbles)
return bubbles
},
// 点击图片 - 显示描述
handleImageClick() {
this.showDescription()
},
// 点击气泡 - 显示单词详情
// 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]
// // 处理单词详情数据简化WXML中的访问
// 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,
// 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({
// displayContent: '获取单词详情失败,请稍后重试',
// 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;
});
},
// 显示描述内容
showDescription() {
const { recognitionResult } = this.data
if (!recognitionResult) {
console.error('recognitionResult 为空,无法显示描述')
return
}
// 防护性检查确保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',
displayContent: '暂无描述信息',
currentWord: '',
isLoadingContent: false
})
return
}
// 过滤空值并格式化描述内容
const validDescriptions = recognitionResult?.result?.level1?.desc_en
.filter(desc => desc && typeof desc === 'string' && desc.trim().length > 0)
.map(desc => {
// 确保首字母大写
return desc.charAt(0).toUpperCase() + desc.slice(1)
})
const displayContent = validDescriptions.length > 0
? validDescriptions.join('\n\n')
: '暂无描述信息'
console.log('显示描述内容:', displayContent)
this.setData({
displayType: 'description',
displayContent,
currentWord: '',
isLoadingContent: false
})
},
// 返回上一页
goBack() {
if (getCurrentPages().length > 1) {
wx.navigateBack()
} else {
wx.switchTab({
url: '/pages/upload/upload'
})
}
},
// 重新识别
handleReRecognize() {
wx.switchTab({
url: '/pages/upload/upload'
})
},
// 保存图片
async handleSaveImage() {
try {
await imageManager.saveImageToPhotosAlbum(this.data.imagePath)
} catch (error) {
console.error('保存图片失败:', error)
}
},
// 页面卸载时不清除音频缓存
onUnload() {
console.log('页面卸载,保留音频缓存')
// 不再清除音频缓存,让用户可以跨页面使用
},
// 切换signpost显示
toggleSignpost(e: any) {
const { index, dictIndex } = e.currentTarget.dataset
const currentWordDetail = this.data.currentWordDetail
if (!currentWordDetail || !currentWordDetail.dict_list) return
const dictList = [...currentWordDetail.dict_list]
if (dictList[dictIndex] && dictList[dictIndex].senses[index]) {
dictList[dictIndex].senses[index].showSignpostCn = !dictList[dictIndex].senses[index].showSignpostCn
// 同时更新处理后的数据
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null
if (processedWordDetail && processedWordDetail[dictIndex] && processedWordDetail[dictIndex].senses[index]) {
processedWordDetail[dictIndex].senses[index].showSignpostCn = dictList[dictIndex].senses[index].showSignpostCn
}
this.setData({
[`currentWordDetail.dict_list[${dictIndex}].senses[${index}].showSignpostCn`]: dictList[dictIndex].senses[index].showSignpostCn,
processedWordDetail
})
}
},
// 切换definition显示
toggleDefinition(e: any) {
const { index, dictIndex } = e.currentTarget.dataset
const currentWordDetail = this.data.currentWordDetail
if (!currentWordDetail || !currentWordDetail.dict_list) return
const dictList = [...currentWordDetail.dict_list]
if (dictList[dictIndex] && dictList[dictIndex].senses[index]) {
dictList[dictIndex].senses[index].showDefinitionCn = !dictList[dictIndex].senses[index].showDefinitionCn
// 同时更新处理后的数据
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null
if (processedWordDetail && processedWordDetail[dictIndex] && processedWordDetail[dictIndex].senses[index]) {
processedWordDetail[dictIndex].senses[index].showDefinitionCn = dictList[dictIndex].senses[index].showDefinitionCn
}
this.setData({
[`currentWordDetail.dict_list[${dictIndex}].senses[${index}].showDefinitionCn`]: dictList[dictIndex].senses[index].showDefinitionCn,
processedWordDetail
})
}
},
// 切换examples显示
toggleExamples(e: any) {
const { index, dictIndex } = e.currentTarget.dataset
const currentWordDetail = this.data.currentWordDetail
if (!currentWordDetail || !currentWordDetail.dict_list) return
const dictList = [...currentWordDetail.dict_list]
if (dictList[dictIndex] && dictList[dictIndex].senses[index]) {
dictList[dictIndex].senses[index].showExamples = !dictList[dictIndex].senses[index].showExamples
// 同时更新处理后的数据
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null
if (processedWordDetail && processedWordDetail[dictIndex] && processedWordDetail[dictIndex].senses[index]) {
processedWordDetail[dictIndex].senses[index].showExamples = dictList[dictIndex].senses[index].showExamples
}
this.setData({
[`currentWordDetail.dict_list[${dictIndex}].senses[${index}].showExamples`]: dictList[dictIndex].senses[index].showExamples,
processedWordDetail
})
}
},
// 播放音频
playAudio(e: any) {
const { audio } = e.currentTarget.dataset
if (audio) {
// 调用API管理器播放音频
apiManager.playAudio(audio).catch(error => {
console.error('播放音频失败:', error)
// 检查是否是因为音频正在播放导致的拒绝
if (error.message === '音频正在播放中,请等待播放完成') {
// 不显示错误提示,因为这是正常的行为限制
console.log('音频正在播放中,已阻止重复播放')
} else {
wx.showToast({
title: '音频播放失败',
icon: 'none'
})
}
})
}
},
preventTagChange() {
return false;
},
// 切换例句翻译显示
toggleDefTranslation(e: any) {
const definition = e.currentTarget.dataset.definition
if (!definition) return false;
// 更新处理后的数据中的显示状态
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null
if (processedWordDetail) {
for (let dictIndex = 0; dictIndex < processedWordDetail.length; dictIndex++) {
const dictItem = processedWordDetail[dictIndex]
if (dictItem.senses) {
for (let senseIndex = 0; senseIndex < dictItem.senses.length; senseIndex++) {
const sense = dictItem.senses[senseIndex]
if (sense.definitions) {
const ex = sense.definitions[0]
if (ex.en === definition.en) {
// 切换显示状态
ex.showTranslation = !ex.showTranslation
// 更新数据
this.setData({
processedWordDetail
})
return false;
}
}
}
}
}
}
return false;
},
// 切换例句翻译显示
toggleExampleTranslation(e: any) {
const example = e.currentTarget.dataset.example
if (!example) return
// 更新处理后的数据中的显示状态
const processedWordDetail = this.data.processedWordDetail ? [...this.data.processedWordDetail] : null
if (processedWordDetail) {
for (let dictIndex = 0; dictIndex < processedWordDetail.length; dictIndex++) {
const dictItem = processedWordDetail[dictIndex]
if (dictItem.senses) {
for (let senseIndex = 0; senseIndex < dictItem.senses.length; senseIndex++) {
const sense = dictItem.senses[senseIndex]
if (sense.examples) {
for (let exampleIndex = 0; exampleIndex < sense.examples.length; exampleIndex++) {
const ex = sense.examples[exampleIndex]
// 检查是否是同一个例句(通过英文内容匹配)
if (ex.en === example.en) {
// 切换显示状态
ex.showTranslation = !ex.showTranslation
// 更新数据
this.setData({
processedWordDetail
})
return
}
}
}
}
}
}
}
},
// 点击描述文本跳转到 assessment 页面
handleDescriptionClick(e: WechatMiniprogram.BaseEvent) {
const content = e.currentTarget.dataset.content
if (content) {
wx.navigateTo({
url: `/pages/assessment/assessment?imageId=${this.data.imageId}&index=0`
})
}
},
})

View File

@@ -1,116 +0,0 @@
<!-- result.wxml - 结果展示页面 -->
<view class="result-container">
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 图片卡片容器(用于光晕效果) -->
<view wx:if="{{animationStage !== 'loading'}}" class="image-card-container" style="{{cardStyle}}">
<!-- 图片显示 -->
<image wx:if="{{imagePath}}" class="result-image {{animationStage === 'result' ? 'glow-effect' : ''}}" style="{{imageStyle}}" src="{{imagePath}}" mode="aspectFill" bindtap="handleImageClick" />
<!-- 卡片周围的闪烁气泡 -->
<view wx:if="{{showCardBubbles && cardBubbles.length > 0}}" class="bubbles-container card-bubbles-relative">
<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: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 wx:if="{{showResultArea}}" 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" bindtap="handleDescriptionClick" data-content="{{displayContent}}">
<text class="text-primary">{{displayContent || '点击图片查看描述'}}</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> -->
</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,655 +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 {
opacity: 0;
z-index: 100;
flex: 1; /* 占据剩余空间 */
display: flex;
align-items: center;
justify-content: center;
}
@keyframes processingFloat {
0%, 100% { transform: translateY(0); opacity: 0.3; }
50% { transform: translateY(-20rpx); opacity: 0.8; }
}
/* 通用气泡容器 */
.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;
}
/* 卡片气泡容器 */
.card-bubbles-relative {
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.card-bubbles-relative .bubble-item.fade-out {
animation: cardBubbleBlink 2.2s infinite linear,
fadeBubble .5s forwards ease-in-out !important;
}
@keyframes fadeBubble {
to {
opacity: 0;
transform: scale(0);
}
}
@keyframes showBubble {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
.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;
}
.bubble-interactive:active {
transform: scale(0.95);
}
@keyframes bubbleJump {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-10rpx) scale(1.05); }
}
/* 位置1左侧最上方 */
.card-bubble-1 {
left: -100rpx;
top: 20rpx;
animation: showCardBubble .4s forwards linear, cardBubbleBlink 2s ease-in-out infinite;
animation-delay: 0s;
}
/* 位置5左侧中下方 */
.card-bubble-2 {
left: -100rpx;
top: 200rpx;
animation: showCardBubble .4s forwards linear, cardBubbleBlink 2s ease-in-out infinite;
animation-delay: 0.4s;
}
/* 位置4右侧中上方 */
.card-bubble-3 {
right: -100rpx;
top: 110rpx;
animation: showCardBubble .4s forwards linear, cardBubbleBlink 2s ease-in-out infinite;
animation-delay: 0.2s;
}
/* 位置8右侧最下方 */
.card-bubble-4 {
right: -100rpx;
top: 290rpx;
animation: showCardBubble .4s forwards linear, cardBubbleBlink 2s ease-in-out infinite;
animation-delay: 0.6s;
}
/* 位置1左侧最上方 */
.result-bubble-1 {
left: -80rpx;
top: -25rpx;
animation-delay: 0s;
}
/* 位置5左侧中下方 */
.result-bubble-2 {
left: -80rpx;
top: 170rpx;
animation-delay: 0.4s;
}
/* 位置4右侧中上方 */
.result-bubble-3 {
right: -80rpx;
top: 80rpx;
animation-delay: 0.2s;
}
/* 位置8右侧最下方 */
.result-bubble-4 {
right: -80rpx;
top: 268rpx;
animation-delay: 0.6s;
}
/* 卡片气泡 */
.card-bubble {
opacity: 0;
width: 80rpx;
height: 80rpx;
background: rgba(0, 122, 255, 0.6);
box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.4);
}
@keyframes showCardBubble {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 0.3;
transform: scale(0.8);
}
}
@keyframes cardBubbleBlink {
0%, 100% {
opacity: 0.3;
transform: scale(0.8);
box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.2);
}
50% {
opacity: 0.9;
transform: scale(1.1);
box-shadow: 0 8rpx 24rpx rgba(0, 122, 255, 0.6);
}
}
/* 结果气泡 */
.result-bubbles {
z-index: 50;
}
.bubble-interactive {
pointer-events: auto;
cursor: pointer;
}
.result-bubble {
background: #007AFF;
color: #ffffff;
box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.3);
}
.result-bubble:active {
transform: scale(0.9);
background: #0056CC;
}
.bubble-text {
font-size: 32rpx;
font-weight: 600;
text-align: center;
word-break: break-all;
padding: 8rpx 16rpx;
}
/* 结果显示区域 */
.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; /* 确保padding不会增加元素的总高度 */
display: flex;
flex-direction: column;
overflow: hidden; /* 防止内容溢出 */
z-index: 1000; /* 确保显示在其他元素之上 */
}
.display-header {
padding: 32rpx 32rpx 0;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0; /* 防止在flex布局中被压缩 */
}
.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; /* 让flexbox自动计算高度 */
max-height: none; /* 移除最大高度限制 */
box-sizing: border-box; /* 确保padding不会增加元素的总高度 */
overflow-y: auto; /* 确保内容溢出时可以滚动 */
display: flex;
flex-direction: column;
}
.loading-content {
text-align: center;
padding: 40rpx;
}
.loading-bar {
width: 200rpx;
height: 4rpx;
background-color: #e5e5e5;
border-radius: 2rpx;
margin: 0 auto 16rpx;
overflow: hidden;
}
.loading-bar::after {
content: '';
display: block;
width: 50%;
height: 100%;
background-color: #007AFF;
border-radius: 2rpx;
animation: loading 1.5s infinite ease-in-out;
}
@keyframes loading {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
.loading-text {
font-size: 28rpx;
color: #666666;
}
.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;
}
.display-actions {
padding: 16rpx 32rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
flex-shrink: 0; /* 防止在flex布局中被压缩 */
}
.action-btn-secondary {
width: 100%;
background: #f8f8f8;
color: #333333;
border: none;
padding: 24rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
.action-btn-secondary:active {
background-color: #e8e8e8;
}
/* 加载状态提示 */
.loading-overlay {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0, 0, 0, 0.8);
color: #ffffff;
padding: 40rpx;
border-radius: 16rpx;
z-index: 1000;
}
.processing-overlay {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0, 0, 0, 0.8);
color: #ffffff;
padding: 40rpx;
border-radius: 16rpx;
z-index: 1000;
}
.processing-text {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 24rpx;
}
.processing-progress {
width: 300rpx;
height: 6rpx;
background-color: rgba(255, 255, 255, 0.3);
border-radius: 3rpx;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: #007AFF;
border-radius: 3rpx;
animation: progress 2s ease-in-out infinite;
}
@keyframes progress {
0% { width: 0%; }
50% { width: 70%; }
100% { width: 100%; }
}
/* 单词详情样式 */
.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;
}
.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;
}
.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);
}
.word-collapse-content-icon {
width: 40rpx;
height: 40rpx;
display: inline-block;
vertical-align: middle;
padding: 0 6rpx;
}

View File

@@ -63,6 +63,7 @@ Page({
waterfallLeft: [] as any[],
waterfallRight: [] as any[],
stripCentered: true,
todayKey: '',
},
onLoad() {
@@ -877,7 +878,17 @@ Page({
(yg.items || []).forEach(pushRecord);
});
if (!records.length) {
this.setData({ dateStripItems: [], selectedDateKey: '', selectedDateImages: [], selectedMonthTitle: '', imagesByDate: {} });
const imagesByDate: Record<string, any[]> = {};
const strip: any[] = [];
const today = new Date();
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
strip.push({ type: 'day', key: `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`, weekday: weekdays[today.getDay()], day: today.getDate(), year: today.getFullYear(), month: today.getMonth() + 1 });
const selectedKey = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
const selectedImages: any[] = [];
const gridCols = 1;
const monthTitle = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}`;
const stripCentered = true;
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall: false, waterfallLeft: [], waterfallRight: [], stripCentered, todayKey: selectedKey });
return;
}
records.sort((a, b) => a.date.getTime() - b.date.getTime());
@@ -897,14 +908,14 @@ Page({
}
strip.push({ type: 'day', key: r.key, weekday: r.weekday, day: r.day, year: r.year, month: r.month });
}
let selectedKey = '';
const today = new Date();
const todayKey = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
if (imagesByDate[todayKey]) selectedKey = todayKey; else selectedKey = records[records.length - 1].key;
let selectedKey = this.data.selectedDateKey || todayKey;
const selectedImages = imagesByDate[selectedKey] || [];
const gridCols = selectedImages.length <= 1 ? 1 : selectedImages.length <= 4 ? 2 : 3;
const sel = records.find(r => r.key === selectedKey)!;
const monthTitle = `${sel.year}-${String(sel.month).padStart(2,'0')}`;
const sel = records.find(r => r.key === selectedKey);
const partsForTitle = selectedKey.split('-');
const monthTitle = sel ? `${sel.year}-${String(sel.month).padStart(2,'0')}` : `${partsForTitle[0]}-${partsForTitle[1]}`;
const useWaterfall = selectedImages.length > 3;
let waterfallLeft: any[] = [], waterfallRight: any[] = [];
if (useWaterfall) {
@@ -912,8 +923,14 @@ Page({
(idx % 2 === 0 ? waterfallLeft : waterfallRight).push(img);
});
}
const hasTodayInStrip = strip.some(it => it.type === 'day' && it.key === todayKey);
if (!hasTodayInStrip) {
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const weekday = weekdays[today.getDay()];
strip.push({ type: 'day', key: todayKey, weekday, day: today.getDate(), year: today.getFullYear(), month: today.getMonth() + 1 });
}
const stripCentered = strip.length <= 5;
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall, waterfallLeft, waterfallRight, stripCentered });
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall, waterfallLeft, waterfallRight, stripCentered, todayKey });
if (useWaterfall) {
this.balanceWaterfall(selectedImages);
}
@@ -933,10 +950,24 @@ Page({
if (useWaterfall) {
images.forEach((img, idx) => { (idx % 2 === 0 ? waterfallLeft : waterfallRight).push(img); });
}
this.setData({ selectedDateKey: dateKey, selectedDateImages: images, gridCols, selectedMonthTitle: `${y}-${String(m).padStart(2,'0')}`, useWaterfall, waterfallLeft, waterfallRight });
if (useWaterfall) {
this.balanceWaterfall(images);
}
// console.log('选择日期:', dateKey, '对应图片数量:', images.length);
// 先设置基础数据,再处理瀑布流
this.setData({
selectedDateKey: dateKey,
selectedDateImages: images,
gridCols,
selectedMonthTitle: `${y}-${String(m).padStart(2,'0')}`,
useWaterfall,
waterfallLeft,
waterfallRight
}, () => {
// 在setData回调中处理瀑布流平衡
if (useWaterfall && images.length > 0) {
this.balanceWaterfall(images);
}
});
},
async ensureThumbSize(img: any): Promise<{ w: number; h: number } | null> {

View File

@@ -108,6 +108,15 @@
</scroll-view>
</view>
</view>
<view wx:if="{{!takePhoto && selectedDateKey === todayKey && (!selectedDateImages || selectedDateImages.length === 0)}}" class="history-wrap">
<view class="history-card-item blank-card">
<view class="blank-content" bindtap="handleImageSelect">
<view class="photo-inner">
<image class="blank-image" src="{{photoSvgData}}" mode="{{'aspectFit'}}"></image>
</view>
</view>
</view>
</view>
<!-- 历史记录区域 -->
<!-- 仅在加载更多时显示骨架屏 -->
<t-skeleton wx:if="{{isLoading && page >= 1}}" theme="paragraph" animation="gradient" loading="{{true}}"></t-skeleton>

View File

@@ -331,11 +331,12 @@
.cloud-icon {
position: fixed;
top: 500rpx;
top: 400rpx;
left: -100rpx;
color: #eff0f3;
opacity: 0.8;
animation: moveLeftRight 4s infinite alternate linear;
z-index: -1;
}
/* 新:底部日期条样式 */
@@ -343,7 +344,6 @@
padding: 20rpx;
background: transparent;
color: #001858;
z-index: 1;
}
.night .date-section {
color: #fffffe;
@@ -1242,3 +1242,22 @@
50%{background-position:100% 50%}
100%{background-position:0% 50%}
}
.blank-content {
position: relative;
top: 0%;
left: 50%;
transform: translate(-50%, 10%) rotate(3deg);
width: 480rpx;
height: 480rpx;
border-style: solid;
border-color: #ffffff;
border-width: 24rpx 24rpx 64rpx 24rpx;
box-shadow: 0 8rpx 12rpx rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.blank-image {
width: 100%;
height: 100%;
}

View File

@@ -34,7 +34,7 @@ export interface IRecognitionResponse {
status: string
error_message?: string
image_id?: string
result?: IRecognitionResponseResult
// result?: IRecognitionResponseResult
}
export interface IRecognitionResponseResultLevel {

View File

@@ -983,7 +983,7 @@ class ApiManager {
try {
const res = await this.recognizeGetTask(task_id)
console.log('--lisa-res', res)
if (res.status === 'completed' && res.result) {
if (res.status === 'completed') {
recognitionResult = res
break
}