diff --git a/miniprogram/app.json b/miniprogram/app.json index b24fbad..79c8668 100755 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -2,6 +2,8 @@ "pages": [ "pages/upload/upload", "pages/result/result", + "pages/result_show/result_show", + "pages/assessment/assessment", "pages/profile/profile", "pages/index/index", "pages/logs/logs", diff --git a/miniprogram/pages/assessment/assessment.json b/miniprogram/pages/assessment/assessment.json new file mode 100644 index 0000000..f756550 --- /dev/null +++ b/miniprogram/pages/assessment/assessment.json @@ -0,0 +1,12 @@ +{ + "navigationBarTitleText": "口语评估", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#f8f9fa", + "backgroundTextStyle": "light", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50, + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon" + } +} \ No newline at end of file diff --git a/miniprogram/pages/assessment/assessment.ts b/miniprogram/pages/assessment/assessment.ts new file mode 100644 index 0000000..1ca8a20 --- /dev/null +++ b/miniprogram/pages/assessment/assessment.ts @@ -0,0 +1,297 @@ +// assessment.ts +import { FILE_BASE_URL } from '../../utils/config' +import apiManager from '../../utils/api' +const recorderManager = wx.getRecorderManager() + +interface IPageData { + imagePath: string + currentSentence: any + sentences: any[] + currentIndex: number + totalScore: number + accuracyScore: number + completenessScore: number + fluencyScore: number + circleProgressStyle: string + isRecording: boolean + recordStartTime: number + recordDuration: number + remainingTime: number // 剩余录音时间 + hasScoreInfo: boolean // 是否有评分信息 +} + +type IPageMethods = { + updateCircleProgress: () => void + startRecording: () => void + stopRecording: () => void + prevSentence: () => void + nextSentence: () => void + handleRecordStart: () => void + handleRecordEnd: () => void +} + +interface IPageInstance extends IPageMethods { + recordTimer?: number + data: IPageData +} + +Page({ + data: { + imagePath: '', // 图片路径 + currentSentence: '', // 当前显示的例句 + sentences: [], // 例句列表 + currentIndex: 0, // 当前例句索引 + totalScore: 0, // 总分 + accuracyScore: 0, // 准确性评分 + completenessScore: 0, // 完整性评分 + fluencyScore: 0, // 流利度评分 + circleProgressStyle: '', // 圆形进度条样式 + isRecording: false, // 是否正在录音 + recordStartTime: 0, // 录音开始时间 + recordDuration: 0, // 录音持续时间 + remainingTime: 30, // 剩余录音时间,默认30秒 + hasScoreInfo: false, // 是否有评分信息 + }, + + // 更新圆形进度条样式 + updateCircleProgress() { + const { totalScore, hasScoreInfo } = this.data + if (!hasScoreInfo || totalScore < 0) { + this.setData({ circleProgressStyle: 'background: #f0f0f0' }) + return + } + const style = `background: conic-gradient(#007AFF ${totalScore * 3.6}deg, #f0f0f0 0deg)` + this.setData({ circleProgressStyle: style }) + }, + + // 切换到上一个例句 + prevSentence() { + const { currentIndex, sentences } = this.data + if (currentIndex <= 0) return; + if (currentIndex > 0) { + const currentSentence = sentences[currentIndex - 1] + const assessmentResult = currentSentence?.details?.assessment?.result + const suggestedScore = assessmentResult?.SuggestedScore + const pronAccuracy = assessmentResult?.PronAccuracy + const pronCompletion = assessmentResult?.PronCompletion + const pronFluency = assessmentResult?.PronFluency + + // 检查是否所有评分都为负数 + const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every(score => score < 0) + + this.setData({ + currentIndex: currentIndex - 1, + currentSentence: currentSentence, + hasScoreInfo: !allNegative && !!currentSentence.details, + totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0, + accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0, + completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0, + fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0 + }) + this.updateCircleProgress() + + } + }, + + // 切换到下一个例句 + nextSentence() { + const { currentIndex, sentences } = this.data + if (currentIndex >= sentences.length - 1) return; + if (currentIndex < sentences.length - 1) { + const index = currentIndex + 1 + const currentSentence = sentences[index] + const assessmentResult = currentSentence?.details?.assessment?.result + const suggestedScore = assessmentResult?.SuggestedScore + const pronAccuracy = assessmentResult?.PronAccuracy + const pronCompletion = assessmentResult?.PronCompletion + const pronFluency = assessmentResult?.PronFluency + + // 检查是否所有评分都为负数 + const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every(score => score < 0) + + this.setData({ + currentIndex: index, + currentSentence: currentSentence, + hasScoreInfo: !allNegative && !!currentSentence.details, + totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0, + accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0, + completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0, + fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0 + }) + this.updateCircleProgress() + } + }, + + // 开始录音 + startRecording() { + const options: WechatMiniprogram.RecorderManagerStartOption = { + duration: 30000, // 最长录音时长,单位 ms + sampleRate: 16000, // 采样率 + numberOfChannels: 1, // 录音通道数 + encodeBitRate: 48000, // 编码码率 + format: 'mp3' as 'mp3', // 音频格式 + } + + recorderManager.start(options) + this.setData({ + isRecording: true, + recordStartTime: Date.now(), + recordDuration: 0 + }) + + // 设置定时器更新录音时长和倒计时 + this.recordTimer = setInterval(() => { + const duration = Date.now() - this.data.recordStartTime + const remaining = Math.max(0, 30 - Math.floor(duration / 1000)) + this.setData({ + recordDuration: duration, + remainingTime: remaining + }) + + // 时间到自动停止录音 + if (remaining === 0) { + this.stopRecording() + } + }, 100) + }, + + // 停止录音 + stopRecording() { + const duration = Date.now() - this.data.recordStartTime + if (this.recordTimer) { + clearInterval(this.recordTimer) + } + + if (duration < 3000) { // 小于3秒 + wx.showToast({ + title: '说话时间太短', + icon: 'none' + }) + recorderManager.stop() + this.setData({ + isRecording: false, + remainingTime: 30 // 重置倒计时 + }) + return + } + + recorderManager.stop() + this.setData({ isRecording: false }) + }, + + // 长按开始录音 + handleRecordStart() { + this.startRecording() + }, + + // 松开结束录音 + handleRecordEnd() { + this.stopRecording() + }, + + onLoad(options: Record) { + //如果有图片ID,调用接口获取文本和评分信息 + if (options.index) { + this.setData({ currentIndex: Number(options.index) }) + } + if (options.imageId) { + wx.showLoading({ title: '加载中...' }) + apiManager.getImageTextInit(options.imageId) + .then(res => { + // 更新图片路径(如果有) + if (res.image_file_id) { + this.setData({ imagePath: `${FILE_BASE_URL}/${res.image_file_id}` }) + } + // 更新例句和评分信息 + if (res.assessments && res.assessments.length > 0) { + const sentences = res.assessments + const index = this.data.currentIndex || 0 + const currentSentence = sentences[index] + const assessmentResult = currentSentence?.details?.assessment?.result + const suggestedScore = assessmentResult?.SuggestedScore ?? -1 + const pronAccuracy = assessmentResult?.PronAccuracy ?? -1 + const pronCompletion = assessmentResult?.PronCompletion ?? -1 + const pronFluency = assessmentResult?.PronFluency ?? -1 + + // 检查是否所有评分都为负数 + const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every((score: number) => score < 0) + + this.setData({ + sentences, + currentSentence, + currentIndex: index, + hasScoreInfo: !allNegative && !!currentSentence.details, + totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0, + accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0, + completenessScore: pronCompletion >= 0 ? Number((pronCompletion * 100).toFixed(2)) : 0, + fluencyScore: pronFluency >= 0 ? Number((pronFluency * 100).toFixed(2)) : 0 + }) + } + // 初始化圆形进度条 + this.updateCircleProgress() + }) + .catch(err => { + console.error('获取图片文本和评分信息失败:', err) + wx.showToast({ + title: '加载失败', + icon: 'none' + }) + }) + .finally(() => { + wx.hideLoading() + }) + } else { + // 初始化圆形进度条 + this.updateCircleProgress() + } + }, + + onReady() { + // 监听录音结束事件 + recorderManager.onStop((res) => { + if (this.data.recordDuration >= 3000) { // 只有录音时长大于3秒才提示确认 + wx.showModal({ + title: '提示', + content: '录音完成,是否确认提交?', + success: (result) => { + if (result.confirm) { + console.log('录音文件路径:', res.tempFilePath) + apiManager.uploadFile(res.tempFilePath).then((fileId) => { + apiManager.getAssessmentResult(fileId, this.data.currentSentence.id).then((result) => { + console.log('口语评估结果:', result) + const assessmentResult = result.assessment_result.assessment.result + const suggestedScore = assessmentResult.SuggestedScore + const pronAccuracy = assessmentResult.PronAccuracy + const pronCompletion = assessmentResult.PronCompletion + const pronFluency = assessmentResult.PronFluency + + // 检查是否所有评分都为负数 + const allNegative = [suggestedScore, pronAccuracy, pronCompletion, pronFluency].every(score => score < 0) + + // 更新评分信息 + this.setData({ + hasScoreInfo: !allNegative && !!assessmentResult, + totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0, + accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0, + completenessScore: pronCompletion >= 0 ? Number(pronCompletion.toFixed(2)) : 0, + fluencyScore: pronFluency >= 0 ? Number(pronFluency.toFixed(2)) : 0 + }) + // 更新圆形进度条 + this.updateCircleProgress() + }) + }) + } + } + }) + } + }) + + // 监听录音错误事件 + recorderManager.onError((res) => { + wx.showToast({ + title: '录音失败', + icon: 'none' + }) + }) + } +}) \ No newline at end of file diff --git a/miniprogram/pages/assessment/assessment.wxml b/miniprogram/pages/assessment/assessment.wxml new file mode 100644 index 0000000..b511fa4 --- /dev/null +++ b/miniprogram/pages/assessment/assessment.wxml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + {{currentSentence.content}} + {{currentIndex + 1}}/{{sentences.length}} + + + + + + + + + + {{remainingTime}}s + + 评分详解 + + + + + + + + + {{totalScore}} + 总分 + + + + + 准确性 + + + + + + {{accuracyScore}} + + 暂无评分 + + + + 完整性 + + + + + + {{completenessScore}} + + 暂无评分 + + + + 流利度 + + + + + + {{fluencyScore}} + + 暂无评分 + + + + + + + 暂无评分 + + + \ No newline at end of file diff --git a/miniprogram/pages/assessment/assessment.wxss b/miniprogram/pages/assessment/assessment.wxss new file mode 100644 index 0000000..2cac9ac --- /dev/null +++ b/miniprogram/pages/assessment/assessment.wxss @@ -0,0 +1,263 @@ +.assessment-container { + min-height: 100vh; + background-color: #f5f5f5; + display: flex; + flex-direction: column; +} + +/* 顶部图片区域 */ +.image-section { + width: 100%; + position: relative; + overflow: hidden; +} + +.assessment-image { + display: block; + width: 50%; + margin: 40rpx auto; + border-radius: 30rpx; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); +} + +/* 中间例句区域 */ +.sentence-section { + flex: 1; + padding: 0 32rpx; + display: flex; + flex-direction: column; +} + +.sentence-container { + display: flex; + align-items: center; + justify-content: space-between; + margin: 40rpx 0; +} + +.arrow-btn { + width: 80rpx; + height: 80rpx; + display: flex; + align-items: center; + justify-content: center; + background: #ffffff; + border-radius: 50%; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1); +} + +.arrow-btn:active { + background: #f0f0f0; +} + +.arrow-btn.disabled { + background: #e0e0e0; + box-shadow: none; + pointer-events: none; +} + +.arrow-btn.disabled t-icon { + color: #999999; +} + +.sentence-content { + flex: 1; + padding: 0 12rpx; + text-align: center; +} + +.sentence-text { + font-size: 32rpx; + color: #333333; + line-height: 1.6; + margin-bottom: 16rpx; + display: block; +} + +.page-indicator { + font-size: 24rpx; + color: #666666; + display: block; + text-align: center; +} + +.microphone-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 40rpx; + position: relative; + gap: 24rpx; +} + +.score-detail-btn { + margin-top: 8rpx; + min-width: 160rpx; +} + +.countdown { + font-size: 32rpx; + color: #ff3b30; + font-weight: bold; + margin-top: 16rpx; + animation: fadeInOut 1s infinite; +} + +@keyframes fadeInOut { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +.microphone { + width: 120rpx; + height: 120rpx; + padding: 20rpx; + background: #007AFF; + border-radius: 50%; + color: #ffffff; + box-shadow: 0 4rpx 16rpx rgba(0, 122, 255, 0.3); + transition: all 0.3s ease; +} + +.microphone.recording { + transform: scale(1.1); + background: #ff3b30; + box-shadow: 0 4rpx 16rpx rgba(255, 59, 48, 0.3); + animation: pulse 1.5s infinite; +} + +@keyframes pulse { + 0% { + transform: scale(1.1); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1.1); + } +} + +/* 底部评分结果区域 */ +.score-section { + padding: 32rpx 24rpx; + background: #ffffff; + border-top-left-radius: 24rpx; + border-top-right-radius: 24rpx; + box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.1); +} + +.score-container { + display: flex; + align-items: center; + gap: 32rpx; +} + +.total-score { + flex-shrink: 0; +} + +.circle-progress { + width: 180rpx; + height: 180rpx; + border-radius: 50%; + background: #f0f0f0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; +} + +.circle-progress::before { + content: ''; + position: absolute; + width: 150rpx; + height: 150rpx; + border-radius: 50%; + background: #ffffff; +} + +.total-score-value { + font-size: 48rpx; + color: #007AFF; + font-weight: bold; + position: relative; + z-index: 1; +} + +.total-score-label { + font-size: 24rpx; + color: #666666; + position: relative; + z-index: 1; + margin-top: 4rpx; +} + +.score-details { + flex: 1; + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.score-item { + display: flex; + align-items: center; + gap: 16rpx; +} + +.score-label { + font-size: 22rpx; + color: #666666; + width: 80rpx; + text-align: right; +} + +.score-content { + flex: 1; + display: flex; + align-items: center; + gap: 12rpx; +} + +.score-value { + font-size: 24rpx; + color: #007AFF; + font-weight: bold; + min-width: 48rpx; + text-align: right; +} + +.score-item .no-score-text { + font-size: 22rpx; + color: #999; +} + +.progress-bar { + flex: 1; + height: 6rpx; + background: #f0f0f0; + border-radius: 3rpx; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: #007AFF; + border-radius: 3rpx; + transition: width 0.3s ease; +} + +.no-score { + display: flex; + justify-content: center; + align-items: center; + height: 300rpx; +} + +.no-score-text { + font-size: 32rpx; + color: #999; +} \ No newline at end of file diff --git a/miniprogram/pages/result_show/result_show.json b/miniprogram/pages/result_show/result_show.json new file mode 100644 index 0000000..f538c3a --- /dev/null +++ b/miniprogram/pages/result_show/result_show.json @@ -0,0 +1,12 @@ +{ + "navigationBarTitleText": "识别结果", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#f8f9fa", + "backgroundTextStyle": "light", + "enablePullDownRefresh": false, + "onReachBottomDistance": 50, + "usingComponents": { + "t-icon": "tdesign-miniprogram/icon/icon" + } +} \ No newline at end of file diff --git a/miniprogram/pages/result_show/result_show.ts b/miniprogram/pages/result_show/result_show.ts new file mode 100644 index 0000000..3123dfb --- /dev/null +++ b/miniprogram/pages/result_show/result_show.ts @@ -0,0 +1,332 @@ +// 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, + + // 显示内容 + 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 result = await apiManager.getRecognizeImage(imageId) + + // 设置识别结果 + 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: '' + }) + console.log('--lisa-displayContent', this.data.displayContent) + } 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; + } +}) \ No newline at end of file diff --git a/miniprogram/pages/result_show/result_show.wxml b/miniprogram/pages/result_show/result_show.wxml new file mode 100644 index 0000000..105b662 --- /dev/null +++ b/miniprogram/pages/result_show/result_show.wxml @@ -0,0 +1,133 @@ + + + + + + + + + + + + {{item.word}} + + + + + + + + + + + + + + {{item}} + + + + {{errorTip}} + + + + + + + + + {{currentWord}} + + + + + /{{item.ukPronunciation}} $ {{item.usPronunciation}}/ + + + + /{{item.ukPronunciation}}/ + + + + /{{item.usPronunciation}}/ + + + + + + + + + + {{item}} + + + {{item.part_of_speech}} + + + {{item.frequency.level_tag}} + + + {{item.frequency.spoken_tag}} + + + {{item.frequency.written_tag}} + + + {{item.frequency.level_tag}} + + + {{item.frequency.spoken_tag}} + + + {{item.frequency.written_tag}} + + + + + + + + + + + {{item}} + + + + + + {{item.definitions[0].en}} + + + + + {{item.definitions[0].cn}} + + + + {{item.en}} + + + + + + + + {{item.cn}} + + + + + + + + + + 点击图片查看描述,点击气泡查看单词详情 + + + + + + \ No newline at end of file diff --git a/miniprogram/pages/result_show/result_show.wxss b/miniprogram/pages/result_show/result_show.wxss new file mode 100644 index 0000000..2f9ad22 --- /dev/null +++ b/miniprogram/pages/result_show/result_show.wxss @@ -0,0 +1,838 @@ +/* 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; +} \ No newline at end of file diff --git a/miniprogram/pages/upload/upload.ts b/miniprogram/pages/upload/upload.ts index 1ebf534..9091e1b 100755 --- a/miniprogram/pages/upload/upload.ts +++ b/miniprogram/pages/upload/upload.ts @@ -7,8 +7,19 @@ import { FILE_BASE_URL } from '../../utils/config' const app = getApp() +type IDayType = 'morning' | 'afternoon' | 'night' +const DayTypeMap: Record = { + morning: '早上好', + afternoon: '下午好', + night: '晚上好' +} + Page({ data: { + move_camera_pos_to: 'top' as 'top' | 'bottom', + DayTypeMap, + current_date: '', + day_type: 'morning' as IDayType, isLoggedIn: false, showLoginView: false, userInfo: null as IUserInfo | null, @@ -32,6 +43,7 @@ Page({ // 只有登录成功后才加载历史数据 if (this.data.isLoggedIn) { this.loadDailySummary() + this.checkDayType() } }).catch((error) => { console.error('登录检查失败:', error) @@ -44,6 +56,18 @@ Page({ this.setData({ isProcessing: false }) // 清理处理状态 }, + + checkDayType() { + const currentHour = new Date().getHours(); + const currentDate = new Date().toLocaleDateString('zh-CN'); + const dayType = currentHour >= 6 && currentHour < 12 ? 'morning' : + currentHour >= 12 && currentHour < 18 ? 'afternoon' : 'night'; + this.setData({ + current_date: currentDate, + day_type: dayType as IDayType + }); + }, + // 加载每日摘要数据 async loadDailySummary(page: number = 1) { try { @@ -53,9 +77,10 @@ Page({ } const result = await apiManager.getDailySummary(page, this.data.size); + const todaySummary = await apiManager.getTodaySummary(page, this.data.size); // 处理数据,按年份分组 - const processedItems = result.items.map(item => ({ + const processedItems = [...result.items, ...todaySummary.items].map(item => ({ ...item, images: item.image_ids && item.thumbnail_ids ? item.image_ids.map((imageId: string, index: number) => ({ @@ -105,6 +130,7 @@ Page({ hasMore: newData.length < result.total, isLoading: false, }); + console.log('---lisa-groupedHistory', this.data.groupedHistory) } catch (error) { console.error('加载每日摘要失败:', error); this.setData({ isLoading: false }); @@ -123,6 +149,19 @@ Page({ } }, + onPageScroll(e: any) { + const scrollTop = e.scrollTop; + if (scrollTop >= 30) { + this.setData({ + move_camera_pos_to: 'top', + }) + } else { + this.setData({ + move_camera_pos_to: 'bottom', + }) + } + }, + // 下拉刷新 onPullDownRefresh() { this.setData({ @@ -137,11 +176,29 @@ Page({ onImageTap(e: any) { const { imageId } = e.currentTarget.dataset; if (imageId) { - console.log('图片点击,image_id:', imageId); - // 未来实现具体功能 + wx.navigateTo({ + url: `/pages/result_show/result_show?imageId=${imageId}` + }) } }, + onImageCardTap(e: any) { + const { imageItems } = e.currentTarget.dataset; + if (!imageItems?.images?.length) return; + + const items = imageItems.images.map((item: any) => ({ + image_id: item.image_id, + image: item.thumbnail_url, + })); + ActionSheet.show({ + theme: ActionSheetTheme.Grid, + selector: '#t-images-sheet', + context: this, + items, + cancelText: '取消' + }); + }, + // 检查登录状态 async checkLoginStatus() { try { @@ -215,6 +272,18 @@ Page({ } }, + handleImageSelected(e: any) { + console.log('用户选择:', e) + if (e.detail.selected === 'cancel') return; + + const { image_id } = e.detail.selected + if (image_id) { + wx.navigateTo({ + url: `/pages/result_show/result_show?imageId=${image_id}` + }) + } + }, + // 拍照 async handleTakePhoto() { try { diff --git a/miniprogram/pages/upload/upload.wxml b/miniprogram/pages/upload/upload.wxml index d19885d..c21e4c2 100755 --- a/miniprogram/pages/upload/upload.wxml +++ b/miniprogram/pages/upload/upload.wxml @@ -1,19 +1,13 @@ - - + + - --> - - - - - - + + + + •ᴗ• + + + + + +
+
+
+ + + {{ current_date }} + {{DayTypeMap[day_type]}} + 用一个新单词, 开启美好的一天 + + + + + + - + + +

{{item.year}}年

+ + +

{{historyItem.monthDay}}

+ + + + + +
+
+
+ +
- - - - {{item.year}} + + + - - - \ No newline at end of file diff --git a/miniprogram/pages/upload/upload.wxss b/miniprogram/pages/upload/upload.wxss index bd402a1..3d3a467 100755 --- a/miniprogram/pages/upload/upload.wxss +++ b/miniprogram/pages/upload/upload.wxss @@ -1,15 +1,24 @@ /* upload.wxss - 主功能页面样式(扁平化设计) */ +.upload-container.morning, +.upload-container.afternoon { + background-color: #f4f4f4; +} +.morning .history-wrap, +.afternoon .history-wrap { + color: #333; +} + + .upload-container { min-height: 100vh; - background-color: #f5f5f5; + background-color: #010321e0; padding: 0; position: relative; } /* 主要内容区域 */ .main-content { - padding: 32rpx; /* min-height: calc(100vh - 64rpx); */ display: flex; flex-direction: column; @@ -66,7 +75,172 @@ line-height: 88rpx; } +/* 历史记录区域 */ +.history-wrap { + padding: 40rpx 40rpx; + color: #ffffff; +} +.history-card-list { + width: 100%; +} +.history-card-item { + padding: 40rpx; + background-color: #ffffffb5; + border-radius: 30rpx; + box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1); + margin: 40rpx 0; + color: #333333; +} +.images-list { + margin-top: 20rpx; + width: 100%; + display: flex; + flex-wrap: nowrap; + overflow: hidden; + gap: 12rpx; +} +.image-item { + width: 100rpx; + height: 100rpx; + border-radius: 30rpx; + flex-shrink: 0; + box-shadow: 0 8rpx 16rpx 0 rgba(0, 0, 0, 0.04); +} + /* 欢迎区域 */ +.moon-wrap { + opacity: 0; + color: #ffef22c7; + animation: slideInFromTopRight 3s forwards ease-in-out, fadeIn 3s forwards linear; + transform: translate(50%, -50%); +} +.moon-icon { + top: 100rpx; + opacity: 0.9; +} +.star-icon { + position: fixed; + top: 140px; + right: 40px; + font-size: 50rpx; + color: #FFD700; + filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7)); + animation: scaleStar1 3s infinite ease-in-out; +} +.star-icon-2 { + position: fixed; + top: 100px; + left: 40px; + font-size: 40rpx; + color: #FFD700; + filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.7)); + animation: scaleStar2 3s infinite ease-in-out; +} +@keyframes scaleStar1 { + 0%, + 100% { + transform: scale(0.8); + } + 50% { + transform: scale(1.2); + } +} +@keyframes scaleStar2 { + 0%, + 100% { + transform: scale(1.2); + } + 50% { + transform: scale(0.8); + } +} +.sunny-wrap { + /* position: absolute; + top: -100px; + right: -100px; + color: #ffef22c7; + animation: appear 3s ease-out forwards; */ + + opacity: 0; + color: #f6c02f; + animation: slideInFromTopRight 3s forwards ease-in-out, fadeIn 3s forwards linear; + transform: translate(50%, -50%); +} +.sunny-icon { + opacity: 0.9; + animation: rotate 50s linear infinite 3s; /* 3秒后开始旋转 */ + transform-origin: center center; +} +.face { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); /* 垂直居中 */ + font-size: 50rpx; + font-weight: 500; + color:black; + opacity: 0.4; +} + +@keyframes slideInFromTopRight { + to { + transform: translate(30%, 40%); + } +} + +@keyframes fadeIn { + to { + opacity: 1; + } +} + +@keyframes appear { + 0% { + top: -100px; + right: -100px; + opacity: 0; + transform: scale(0.2); + } + 70% { + opacity: 0.6; + transform: scale(0.8); + } + 100% { + top: 20px; + right: 60px; + opacity: 0.8; + transform: scale(1.1); + } +} + +@keyframes rotate { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} + +.cloud-icon { + position: fixed; + bottom: 440rpx; + left: -100rpx; + color: #fefefe; + /* opacity: 0.8; */ + opacity: 0.7; + animation: moveLeftRight 4s infinite alternate linear; +} + +@keyframes moveLeftRight { + 0% { + transform: translateX(-20rpx); + } + 100% { + transform: translateX(20rpx); + } +} + .welcome-section { margin-bottom: 40rpx; } @@ -74,9 +248,10 @@ .welcome-card { background: #ffffff; padding: 32rpx; - border-radius: 16rpx; + border-radius: 50%; text-align: center; box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.05); + border: 4rpx dashed #e91e637a; } .user-info { @@ -106,13 +281,40 @@ opacity: 0.7; } +/* 功能描述区域 */ +.feature-section { + display: flex; + flex-direction: column; + align-items: center; + font-size: 28rpx; + color: #666666; + margin-top: 100rpx; +} +.hello { + margin-top: 80rpx; + font-size: 50rpx; + /* color: black; */ +} +.begin-text { + margin-top: 10rpx; + font-size: 36rpx; + color: #666666; +} + /* 功能按钮区域 */ .action-section { - flex: 1; + width: 300rpx; + height: 300rpx; display: flex; align-items: center; justify-content: center; - margin-bottom: 40rpx; + margin-top: 100rpx; + + opacity: 0.8; +} + +.action-section .top { + } .action-buttons { @@ -123,6 +325,7 @@ gap: 32rpx; width: 100%; max-width: 400rpx; + color: #9c27b04f; } .action-btn { diff --git a/miniprogram/types/app.ts b/miniprogram/types/app.ts new file mode 100644 index 0000000..84ec21f --- /dev/null +++ b/miniprogram/types/app.ts @@ -0,0 +1,105 @@ +// app.ts 的类型定义 +import { ApiManager } from '../utils/api' + +// 用户信息接口 +export interface IUserInfo { + id: string + nickname?: string + avatar_url?: string + gender?: number + country?: string + province?: string + city?: string + language?: string +} + +// 登录响应接口 +export interface ILoginResponse { + access_token: string + access_token_expire_time: string + session_uuid: string + dict_level?: string +} + +// API响应接口 +export interface IApiResponse { + code: number + message?: string + msg?: string + data: T +} + +// 识别结果接口 +export interface IRecognitionResult { + text: string + words: Array<{ + word: string + start: number + end: number + }> +} + +// 单词详情接口 +export interface ExtendedWordDetail { + word: string + phonetic: string + definition: string + translation: string + pos: string + collins: number + oxford: number + tag: string + bnc: number + frq: number + exchange: Record + detail: string + audio: string +} + +// 审核历史记录接口 +export interface IAuditHistoryResponse { + total: number + items: Array<{ + id: string + image_url: string + text: string + created_at: string + }> +} + +// 每日总结接口 +export interface IDailySummaryResponse { + total: number + items: Array<{ + date: string + count: number + images: Array<{ + id: string + url: string + }> + }> +} + +// 全局数据接口 +export interface IGlobalData { + isLoggedIn: boolean + userInfo?: IUserInfo + token?: string + dictLevel?: string + apiManager: ApiManager +} + +// 小程序选项接口 +export interface IAppOption { + globalData: IGlobalData + userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback + initLoginStatus: () => void + updateLoginStatus: (loginData: { + access_token: string + access_token_expire_time: string + session_uuid: string + userInfo?: IUserInfo + dict_level?: string + }) => void + clearLoginData: () => void +} \ No newline at end of file diff --git a/miniprogram/utils/api.ts b/miniprogram/utils/api.ts index bc1a041..9ab9d79 100755 --- a/miniprogram/utils/api.ts +++ b/miniprogram/utils/api.ts @@ -571,7 +571,7 @@ class ApiManager { } // 上传文件(第一步:上传文件获取ID) - private async uploadFile(filePath: string, retryCount: number = 0): Promise { + async uploadFile(filePath: string, retryCount: number = 0): Promise { const maxRetries = 1 // 最多重试1次 return new Promise(async (resolve, reject) => { @@ -722,6 +722,20 @@ class ApiManager { return response.data } + // 获取图片识别结果 + async getRecognizeImage(fileId: string): Promise { + + const app = getApp() + const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY' + + const response = await this.request(`/api/v1/image/${fileId}`, 'GET', { + dict_level: dictLevel + }) + + console.log('图片识别结果获取成功:', response.data) + return response.data + } + // 上传图片并识别(对外接口,整合两个步骤) async uploadImage(filePath: string, type: string = 'word'): Promise { try { @@ -1210,39 +1224,144 @@ class ApiManager { throw error; } } + + async getTodaySummary(page: number = 1, size: number = 15): Promise { + try { + console.log('开始获取今日总结数据', { page, size }); + const response = await this.request(`/api/v1/audit/today_summary?page=${page}&size=${size}`); + console.log('获取今日总结数据成功:', response.data); + return response.data; + } catch (error) { + console.error('获取今日总结数据失败:', error); + throw error; + } + } + + // 获取口语评估结果 + async getAssessmentResult(fileId: string, imageTextId: string): Promise<{ + file_id: string + image_text_id: string + assessment_result: { + assessment: { + code: number + final: number + message: string + voice_id: string + result: { + RefTextId: number, + SentenceId: number, + PronAccuracy: number, + PronFluency: number, + PronCompletion: number, + SuggestedScore: number + Words: any + } + } + } + }> { + try { + console.log('开始获取口语评估结果', { fileId }); + const response = await this.request<{ + file_id: string + image_text_id: string + assessment_result: { + assessment: { + code: number + final: number + message: string + voice_id: string + result: { + RefTextId: number, + SentenceId: number, + PronAccuracy: number, + PronFluency: number, + PronCompletion: number, + SuggestedScore: number + Words: any + } + } + } + }>(`/api/v1/recording/assessment`, 'POST', { + file_id: fileId, + image_text_id: imageTextId + }) + + console.log('获取口语评估结果成功:', response.data); + return response.data; + } catch (error) { + console.error('获取口语评估结果失败:', error); + throw error; + } + } + + // 获取图片文本和评分信息 + async getImageTextInit(imageId: string): Promise<{ + image_file_id: string, + assessments: Array<{ + id: string + content: string, + details: { + assessment: { + code: number + final: number + message: string + voice_id: string + result: { + RefTextId: number, + SentenceId: number, + PronAccuracy: number, + PronFluency: number, + PronCompletion: number, + SuggestedScore: number, + Words: any + } + } + } | null + }> + }> { + try { + console.log('开始获取图片文本和评分信息', { imageId }); + const app = getApp() + const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'PRIMARY' + const response = await this.request<{ + image_file_id: string, + assessments: Array<{ + id: string + content: string, + details: { + assessment: { + code: number + final: number + message: string + voice_id: string + result: { + RefTextId: number, + SentenceId: number, + PronAccuracy: number, + PronFluency: number, + PronCompletion: number, + SuggestedScore: number, + Words: any + } + } + } | null + }> + }>(`/api/v1/image_text/init`, 'POST', { + dict_level: dictLevel, + image_id: imageId + }) + + console.log('获取图片文本和评分信息成功:', response.data); + return response.data; + } catch (error) { + console.error('获取图片文本和评分信息失败:', error); + throw error; + } + } + } // 导出单例 const apiManager = new ApiManager() export default apiManager -export { ApiManager } - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +export { ApiManager } \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json index 976ea7a..0a1a5a8 100755 --- a/project.private.config.json +++ b/project.private.config.json @@ -6,7 +6,6 @@ "urlCheck": false, "coverView": false, "lazyloadPlaceholderEnable": false, - "skylineRenderEnable": false, "preloadBackgroundData": false, "autoAudits": false, "useApiHook": false, @@ -20,5 +19,38 @@ "ignoreDevUnusedFiles": true }, "libVersion": "3.9.3", - "condition": {} + "condition": { + "miniprogram": { + "list": [ + { + "name": "", + "pathName": "pages/result/result", + "query": "imagePath=http://tmp/Jg3M9pJYPJdd9ab131e406bc48acb9ed4069809fd559.png", + "launchMode": "default", + "scene": null + }, + { + "name": "", + "pathName": "pages/result_show/result_show", + "query": "imageId=2086560419278880768", + "launchMode": "default", + "scene": null + }, + { + "name": "", + "pathName": "pages/assessment/assessment", + "query": "imageId=2086560419278880768&index=2", + "launchMode": "default", + "scene": null + }, + { + "name": "", + "pathName": "pages/assessment/assessment", + "query": "imageId=2086529549784449024&index=1", + "launchMode": "default", + "scene": null + } + ] + } + } } \ No newline at end of file