Merge pull request '[lisa]feat: improve ui' (#11) from lisa into main

Reviewed-on: http://gitea.xhzone.cn/felix/miniprogram-1/pulls/11
This commit is contained in:
felix
2025-11-08 12:50:55 +00:00
25 changed files with 1292 additions and 329 deletions

View File

@@ -13,26 +13,11 @@
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "default",
"navigationBarTitleText": "图片识别",
"navigationBarTitleText": "",
"backgroundColor": "#ffffff",
"backgroundTextStyle": "light"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#007AFF",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/upload/upload",
"text": "识别"
},
{
"pagePath": "pages/profile/profile",
"text": "我的"
}
]
},
"permission": {
"scope.camera": {
"desc": "需要使用相机拍照识别图片"

View File

@@ -1,5 +1,7 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

View File

@@ -100,6 +100,11 @@ Component({
})
}
this.triggerEvent('back', { delta: data.delta }, {})
},
goProfile() {
wx.navigateTo({
url: '/pages/profile/profile'
})
}
},
})

View File

@@ -1,20 +1,12 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view bindtap="back" class="weui-navigation-bar__btn_goback_wrapper" hover-class="weui-active" hover-stay-time="100" aria-role="button" aria-label="返回">
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
@@ -22,13 +14,7 @@
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view bindtap="home" class="weui-navigation-bar__btn_home_wrapper" hover-class="weui-active" aria-role="button" aria-label="首页">
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
@@ -38,15 +24,10 @@
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
<view class="weui-loading" aria-role="img" aria-label="加载中"></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
@@ -55,10 +36,9 @@
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>
</view>

View File

@@ -14,7 +14,7 @@
}
.weui-navigation-bar__inner {
position: relative;
position: fixed;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
@@ -32,7 +32,7 @@
padding-left: var(--left);
display: flex;
flex-direction: row;
align-items: flex-start;
align-items: center;
height: 100%;
box-sizing: border-box;
}

View File

@@ -1,8 +1,8 @@
{
"navigationBarTitleText": "口语评估",
"navigationBarTitleText": "识别结果",
"navigationBarTextStyle": "black",
"navigationBarBackgroundColor": "#faeee7",
"backgroundColor": "#f8f9fa",
"navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#ffffff",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,

View File

@@ -2,17 +2,24 @@
import { FILE_BASE_URL } from '../../utils/config'
import apiManager from '../../utils/api'
const recorderManager = wx.getRecorderManager()
let recorderHandlersBound = false
interface IPageData {
imagePath: string
imageSmall: boolean
imageMode: 'widthFix' | 'aspectFit'
currentSentence: any
sentences: any[]
currentIndex: number
selectedSentenceIndex: number
totalScore: number
accuracyScore: number
completenessScore: number
fluencyScore: number
circleProgressStyle: string
accuracyCircleStyle: string
completenessCircleStyle: string
fluencyCircleStyle: string
isRecording: boolean
recordStartTime: number
recordDuration: number
@@ -42,6 +49,7 @@ interface IPageData {
audioDuration: number // 音频总时长
currentTime: number // 当前播放时间
sliderValue: number // 进度条值0-100
playIconSrc: string // 播放图标图片源
showDictPopup: boolean // 控制弹窗是否显示
showDictExtended: boolean // 控制扩展内容是否显示
wordDict: {
@@ -93,6 +101,8 @@ interface IPageData {
}>
// 添加翻译显示模式状态
transDisplayMode: 'en' | 'en_ipa' | 'en_zh' // 翻译显示模式
isScoreModalOpen: boolean // 评分结果弹窗是否打开
justSelectedByWord: boolean
}
type IPageMethods = {
@@ -108,40 +118,53 @@ type IPageMethods = {
handleImagePreview: () => void
getStandardVoice: (sentenceId: string) => Promise<void>
playStandardVoice: () => void
// 新增:评分结果音频播放方法
playAssessmentVoice: () => void
resetAudioState: () => void
handleSliderChange: (e: any) => void
handleWordClick: (e: any) => void
handleSentenceSelect: (e: any) => void
handleDictMore: () => void
handleDictClose: () => void
playWordAudio: (e: any) => void
onTabsChange: (event: any) => void
onTabsClick: (event: any) => void
onSearchTap: () => void
onHistoryTap: () => void
onScoreTap: () => void
onTransTap: () => void
// 新增方法:处理句子数据,分割单词和音标
onCloseScoreModal: () => void
// 新增canvas 绘制圆环方法
drawRing: (canvasId: string, percent: number, hasScore: boolean) => void
// 处理句子数据,分割单词和音标
processSentences: (sentences: any[]) => Array<{ words: string[], ipas: string[], zh: string }>
// 获取翻译按钮图标名称
getTransIconName: () => string
onPageScroll: (e: any) => void
}
interface IPageInstance extends IPageMethods {
recordTimer?: number
data: IPageData
audioContext?: WechatMiniprogram.InnerAudioContext
playIconTimer?: number
}
Page<IPageData, IPageInstance>({
data: {
imagePath: '', // 图片路径
imageSmall: false, // 图片是否缩小
imageMode: 'widthFix', // 初始完整展示模式
currentSentence: '', // 当前显示的例句
sentences: [], // 例句列表
currentIndex: 0, // 当前例句索引
selectedSentenceIndex: 0, // 当前选中的例句索引
totalScore: 0, // 总分
accuracyScore: 0, // 准确性评分
completenessScore: 0, // 完整性评分
fluencyScore: 0, // 流利度评分
circleProgressStyle: '', // 圆形进度条样式
accuracyCircleStyle: '',
completenessCircleStyle: '',
fluencyCircleStyle: '',
isRecording: false, // 是否正在录音
recordStartTime: 0, // 录音开始时间
recordDuration: 0, // 录音持续时间
@@ -177,10 +200,13 @@ Page<IPageData, IPageInstance>({
audioDuration: 0, // 音频总时长
currentTime: 0, // 当前播放时间
sliderValue: 0, // 进度条值0-100
playIconSrc: '/static/play.png',
showDictPopup: false, // 控制弹窗是否显示
showDictExtended: false, // 控制扩展内容是否显示
processedSentences: [], // 处理后的句子数据
transDisplayMode: 'en' // 默认显示模式为英文 only
transDisplayMode: 'en', // 默认显示模式为英文 only
isScoreModalOpen: false, // 评分结果弹窗是否打开
justSelectedByWord: false
},
// 切换评分区域展开状态
@@ -199,13 +225,72 @@ Page<IPageData, IPageInstance>({
// 更新圆形进度条样式
updateCircleProgress() {
const { totalScore, hasScoreInfo } = this.data
if (!hasScoreInfo || totalScore < 0) {
this.setData({ circleProgressStyle: 'background: #f0f0f0' })
return
const { totalScore, accuracyScore, completenessScore, fluencyScore, hasScoreInfo } = this.data
const normalize = (v: number) => {
if (v < 0) return 0
if (v <= 1) return Math.round(v * 100)
if (v > 100) return 100
return Math.round(v)
}
const totalPct = normalize(totalScore)
const accPct = normalize(accuracyScore)
const compPct = normalize(completenessScore)
const fluPct = normalize(fluencyScore)
// 样式字符串留空作为兜底(主要使用 canvas 绘制)
this.setData({
circleProgressStyle: '',
accuracyCircleStyle: '',
completenessCircleStyle: '',
fluencyCircleStyle: '',
})
const draw = (id: string, pct: number) => this.drawRing(id, pct, hasScoreInfo)
draw('totalCircleCanvas', totalPct)
draw('accuracyCircleCanvas', accPct)
draw('completenessCircleCanvas', compPct)
draw('fluencyCircleCanvas', fluPct)
},
// 使用 canvas 绘制圆形进度环
drawRing(canvasId: string, percent: number, hasScore: boolean) {
try {
const sys = wx.getSystemInfoSync()
const rpx2px = sys.screenWidth / 750
const size = 150 * rpx2px
const lineWidth = 12 * rpx2px
const radius = size / 2 - lineWidth / 2
const center = size / 2
const ctx = wx.createCanvasContext(canvasId, this)
// 清空画布
ctx.clearRect(0, 0, size, size)
ctx.setLineWidth(lineWidth)
ctx.setLineCap('round')
// 底环
ctx.beginPath()
ctx.arc(center, center, radius, 0, Math.PI * 2)
ctx.setStrokeStyle('#f0f0f0')
ctx.stroke()
// 进度环(从正上方开始)
if (hasScore && percent > 0) {
const start = -Math.PI / 2
const end = start + (Math.PI * 2) * (percent / 100)
ctx.beginPath()
ctx.arc(center, center, radius, start, end)
ctx.setStrokeStyle('#001858')
ctx.stroke()
}
ctx.draw()
} catch (e) {
console.warn('drawRing error:', e)
}
const style = `background: conic-gradient(#001858 ${totalScore * 3.6}deg, #f0f0f0 0deg)`
this.setData({ circleProgressStyle: style })
},
// 重置音频播放状态
@@ -224,6 +309,17 @@ Page<IPageData, IPageInstance>({
sliderValue: 0,
audioDuration: 0
})
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({
isPlaying: false,
currentTime: 0,
sliderValue: 0,
audioDuration: 0,
playIconSrc: '/static/play.png'
})
},
// 切换到上一个例句
@@ -363,11 +459,19 @@ Page<IPageData, IPageInstance>({
// 停止录音
stopRecording() {
// 若未处于录音中,直接返回,避免调用 stop 后不触发 onStop
if (!this.data.isRecording) {
return
}
const duration = Date.now() - this.data.recordStartTime
if (this.recordTimer) {
clearInterval(this.recordTimer)
}
// 写入最终录音时长,确保 onStop 回调中可准确判断
this.setData({ recordDuration: duration })
if (duration < 3000) { // 小于3秒
wx.showToast({
title: '说话时间太短',
@@ -375,9 +479,9 @@ Page<IPageData, IPageInstance>({
})
recorderManager.stop()
this.setData({
isRecording: false,
remainingTime: 30 // 重置倒计时
})
isRecording: false,
remainingTime: 30 // 重置倒计时
})
return
}
@@ -459,7 +563,11 @@ Page<IPageData, IPageInstance>({
if (isPlaying) {
this.audioContext?.pause()
this.setData({ isPlaying: false })
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({ isPlaying: false, playIconSrc: '/static/play.png' })
return
}
@@ -478,11 +586,25 @@ Page<IPageData, IPageInstance>({
})
this.audioContext.onPlay(() => {
const duration = this.audioContext!.duration
// 停止可能存在的动画计时器
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
// 切换为播放中的图标并启动动画
this.setData({
isPlaying: true,
currentTime: this.audioContext!.currentTime,
audioDuration: duration && duration > 0 ? duration : 0
audioDuration: duration && duration > 0 ? duration : 0,
playIconSrc: '/static/play.png'
})
// 每500ms在 play、play-2、play-1 三个图标之间循环切换顺序play -> play-2 -> play-1
const iconOrder = ['/static/play.png', '/static/play-2.png', '/static/play-1.png']
let i = 0
this.playIconTimer = setInterval(() => {
i = (i + 1) % iconOrder.length
this.setData({ playIconSrc: iconOrder[i] })
}, 500) as any
})
this.audioContext.onTimeUpdate(() => {
const currentTime = this.audioContext!.currentTime
@@ -571,6 +693,125 @@ Page<IPageData, IPageInstance>({
}
},
// 新增:播放评分结果音频(使用当前句子的 file_id
playAssessmentVoice() {
const { currentSentence, isPlaying } = this.data
const fileId = currentSentence?.file_id
const audioUrl = fileId ? `${FILE_BASE_URL}/${fileId}` : ''
if (!audioUrl) {
wx.showToast({ title: '暂无评分音频', icon: 'none' })
return
}
// 如果当前正在播放且音源一致,则切换为暂停
if (isPlaying && this.audioContext?.src === audioUrl) {
this.audioContext.pause()
// 清除播放图标动画并复位
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({ isPlaying: false, playIconSrc: '/static/play.png' })
return
}
// 如果还没有音频上下文,则创建并绑定事件
if (!this.audioContext) {
this.audioContext = wx.createInnerAudioContext()
this.audioContext.onCanplay(() => {
setTimeout(() => {
const duration = this.audioContext!.duration
if (duration && duration > 0) {
this.setData({
audioDuration: duration,
currentTime: this.audioContext!.currentTime
})
}
}, 100)
})
this.audioContext.onPlay(() => {
const duration = this.audioContext!.duration
// 停止可能存在的动画计时器
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
// 切换为播放中的图标并启动动画
this.setData({
isPlaying: true,
currentTime: this.audioContext!.currentTime,
audioDuration: duration && duration > 0 ? duration : 0,
playIconSrc: '/static/play.png'
})
// 每500ms在 play、play-2、play-1 三个图标之间循环切换顺序play -> play-2 -> play-1
const iconOrder = ['/static/play.png', '/static/play-2.png', '/static/play-1.png']
let i = 0
this.playIconTimer = setInterval(() => {
i = (i + 1) % iconOrder.length
this.setData({ playIconSrc: iconOrder[i] })
}, 500) as any
})
this.audioContext.onTimeUpdate(() => {
const currentTime = this.audioContext!.currentTime
const duration = this.audioContext!.duration
if (duration && duration > 0) {
this.setData({
currentTime,
audioDuration: duration,
sliderValue: (currentTime / duration) * 100
})
} else {
this.setData({
currentTime: 0,
audioDuration: 0,
sliderValue: 0
})
}
})
this.audioContext.onEnded(() => {
// 结束时清除动画并复位图标
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({
isPlaying: false,
currentTime: 0,
sliderValue: 0,
playIconSrc: '/static/play.png'
})
})
this.audioContext.onError(() => {
// 出错时清除动画并复位图标
if (this.playIconTimer) {
clearInterval(this.playIconTimer)
this.playIconTimer = undefined
}
this.setData({
isPlaying: false,
currentTime: 0,
sliderValue: 0,
playIconSrc: '/static/play.png'
})
wx.showToast({ title: '播放失败', icon: 'none' })
})
}
// 切换音源并播放
if (this.audioContext.src !== audioUrl) {
this.setData({ currentTime: 0, sliderValue: 0, audioDuration: 0 })
this.audioContext.src = audioUrl
}
try {
this.audioContext.play()
this.setData({ isPlaying: true })
} catch (error) {
wx.showToast({ title: '播放失败', icon: 'none' })
}
},
// 处理进度条拖动
handleSliderChange(e: any) {
const value = e.detail.value
@@ -587,8 +828,42 @@ Page<IPageData, IPageInstance>({
},
async handleWordClick(e: any) {
const { word } = e.currentTarget.dataset
if (!word) return
const { word, index } = e.currentTarget.dataset
// 若例句未被选中,优先选中例句并阻止单词选择
const { selectedSentenceIndex, justSelectedByWord } = this.data
if (typeof index === 'number' && index !== selectedSentenceIndex) {
// 选中目标例句,并更新相关状态(由点击单词触发)
const sentences = this.data.sentences
if (!sentences || !sentences[index]) return
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 < 0 && pronAccuracy < 0 && pronCompletion < 0 && pronFluency < 0
this.resetAudioState()
this.setData({
selectedSentenceIndex: index,
currentSentence,
hasScoreInfo: !allNegative && !!currentSentence.details,
totalScore: suggestedScore > 0 ? suggestedScore : 0,
accuracyScore: pronAccuracy > 0 ? pronAccuracy : 0,
completenessScore: pronCompletion > 0 ? pronCompletion : 0,
fluencyScore: pronFluency > 0 ? pronFluency : 0,
justSelectedByWord: true
})
this.updateCircleProgress()
return
}
// 若是刚通过点击单词选中句子,则本次点击不弹词典,重置标记位
if (justSelectedByWord) {
this.setData({ justSelectedByWord: false })
// 第二次点击,允许继续执行查询词典逻辑
}
// 清理单词,移除标点符号
const cleanedWord = word.replace(/[.,?!*;:]/g, '').trim()
@@ -619,8 +894,59 @@ Page<IPageData, IPageInstance>({
}
},
// 选中例句
handleSentenceSelect(e: any) {
const { index } = e.currentTarget.dataset
if (typeof index !== 'number') return
const { sentences } = this.data
if (!sentences || !sentences[index]) return
// 切换选中时,重置音频播放状态
this.resetAudioState()
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)
const wordScores = assessmentResult?.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
})) || []
})) || []
this.setData({
selectedSentenceIndex: index,
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,
wordScores,
justSelectedByWord: false
})
// 预加载并绑定新的标准语音
if (currentSentence.id) {
this.getStandardVoice(currentSentence.id)
}
},
onLoad(options: Record<string, string>) {
//如果图片ID调用接口获取文本和评分信息
// 如果图片ID调用接口获取文本和评分信息
if (options.index) {
this.setData({ currentIndex: Number(options.index) })
}
@@ -666,6 +992,7 @@ Page<IPageData, IPageInstance>({
processedSentences,
currentSentence,
currentIndex: index,
selectedSentenceIndex: index,
hasScoreInfo: !allNegative && !!currentSentence.details,
totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0,
accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0,
@@ -696,6 +1023,79 @@ Page<IPageData, IPageInstance>({
// 初始化圆形进度条
this.updateCircleProgress()
}
// 录音事件监听(提前绑定,避免事件丢失)
if (!recorderHandlersBound) {
recorderManager.onStop((res) => {
if (this.data.recordDuration >= 3000) { // 只有录音时长大于3秒才提示确认
wx.showModal({
title: '提示',
content: '录音完成,是否确认提交?',
success: (result) => {
if (result.confirm) {
wx.showLoading({ title: '正在解析...' })
console.log('录音文件路径:', res.tempFilePath)
apiManager.uploadFile(res.tempFilePath).then((fileId) => {
apiManager.getAssessmentResult(fileId, this.data.currentSentence.id).then((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)
const { sentences, currentIndex } = this.data
sentences[currentIndex].details = {
assessment: { result: assessmentResult }
}
const wordScores = assessmentResult.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
}))
})) || []
this.setData({
sentences,
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,
wordScores
})
this.updateCircleProgress()
wx.hideLoading()
}).catch(err => {
console.error('获取评估结果失败:', err)
wx.showToast({ title: '评估失败', icon: 'error' })
wx.hideLoading()
})
}).catch(err => {
console.error('上传录音失败:', err)
wx.showToast({ title: '上传失败', icon: 'error' })
wx.hideLoading()
})
}
}
})
}
})
recorderManager.onError((res) => {
wx.showToast({ title: '录音失败', icon: 'none' })
})
recorderHandlersBound = true
}
},
onUnload() {
@@ -706,93 +1106,7 @@ Page<IPageData, IPageInstance>({
},
onReady() {
// 监听录音结束事件
recorderManager.onStop((res) => {
if (this.data.recordDuration >= 3000) { // 只有录音时长大于3秒才提示确认
wx.showModal({
title: '提示',
content: '录音完成,是否确认提交?',
success: (result) => {
if (result.confirm) {
wx.showLoading({ title: '正在解析...' })
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)
// 更新当前例句的评分信息
const { sentences, currentIndex } = this.data
sentences[currentIndex].details = {
assessment: {
result: assessmentResult
}
}
// 处理单词评分数据
const wordScores = assessmentResult.Words?.map((word: any) => ({
word: word.Word,
pronAccuracy: Number(word.PronAccuracy.toFixed(2)),
pronFluency: Number(word.PronFluency.toFixed(2)),
matchTag: word.MatchTag || 0,
phoneInfos: word.PhoneInfos?.map((phone: any) => ({
phone: phone.Phone,
pronAccuracy: Number(phone.PronAccuracy.toFixed(2)),
matchTag: phone.MatchTag || 0
}))
})) || []
// 更新评分信息
this.setData({
sentences, // 保存更新后的例句数组
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,
wordScores
})
// 更新圆形进度条
this.updateCircleProgress()
wx.hideLoading()
})
.catch(err => {
console.error('获取评估结果失败:', err)
wx.showToast({
title: '评估失败',
icon: 'error'
})
wx.hideLoading()
})
})
.catch(err => {
console.error('上传录音失败:', err)
wx.showToast({
title: '上传失败',
icon: 'error'
})
wx.hideLoading()
})
}
}
})
}
})
// 监听录音错误事件
recorderManager.onError((res) => {
wx.showToast({
title: '录音失败',
icon: 'none'
})
})
// 录音事件监听已在 onLoad 提前绑定,避免未绑定时触发 stop 事件导致丢失
},
// 点击“more”按钮展开/收起扩展内容
@@ -805,6 +1119,7 @@ Page<IPageData, IPageInstance>({
// 点击“x”按钮关闭弹窗
handleDictClose() {
this.setData({
showDictExtended: false,
showDictPopup: false
})
},
@@ -850,16 +1165,26 @@ Page<IPageData, IPageInstance>({
wordAudioContext.play()
},
onSearchTap() {
console.log('Search button tapped')
// Navigate to search page or show search modal
onScoreTap() {
console.log('Score button tapped')
// 当当前选中例句无评分信息时,不响应点击
if (!this.data.hasScoreInfo) {
return
}
// 有评分信息时,打开评分结果弹窗
this.setData({
isScoreModalOpen: true
})
// 弹窗打开后绘制进度环(确保 canvas 已渲染)
setTimeout(() => {
this.updateCircleProgress()
}, 0)
},
onHistoryTap() {
console.log('History button tapped')
// Navigate to history page
onCloseScoreModal() {
// 关闭评分弹窗时重置音频播放状态,避免残留播放
this.resetAudioState()
this.setData({ isScoreModalOpen: false })
},
onTransTap() {
console.log('User button tapped')
// Cycle through translation display modes
@@ -898,6 +1223,22 @@ Page<IPageData, IPageInstance>({
return 'file-teams'
}
},
onPageScroll(e: any) {
if (!e || typeof e.scrollTop !== 'number') {
// 忽略无效滚动事件,避免误判为 0 导致回到大图
return
}
const scrollTop = e.scrollTop
const shrinkThreshold = 120
// 一旦缩小,保持小图;不再在下滑时恢复为大图
if (!this.data.imageSmall && scrollTop >= shrinkThreshold) {
this.setData({
imageSmall: true,
imageMode: 'aspectFit'
})
}
},
// 处理句子数据,分割单词和音标
processSentences(sentences: any[]) {

View File

@@ -2,28 +2,28 @@
<view class="assessment-container">
<!-- 顶部图片区域 -->
<view class="image-section" bindtap="handleImagePreview">
<image class="assessment-image" src="{{imagePath}}" mode="aspectFill" />
<image class="assessment-image {{imageSmall ? 'small' : ''}}" src="{{imagePath}}" mode="{{imageMode}}" />
</view>
<!-- 中间例句区域 -->
<view class="sentence-section">
<view class="sentence-container">
<!-- 例句内容 -->
<view class="sentence-content">
<view class="sentence-wrapper" wx:for="{{processedSentences}}" wx:key="index">
<view class="sentence-wrapper {{selectedSentenceIndex === index ? 'selected' : ''}}" wx:for="{{processedSentences}}" wx:key="index" data-index="{{index}}" bindtap="handleSentenceSelect">
<view class="sentence-content-wrapper">
<view class="word-wrapper" wx:for="{{item.words}}" wx:for-item="word" wx:for-index="windex" wx:key="windex">
<text class="sentence-text" data-word="{{word}}" bindtap="handleWordClick">
<span class="sentence-text" data-word="{{word}}" data-index="{{index}}" catchtap="handleWordClick">
{{word}}
</text>
<text wx:if="{{transDisplayMode === 'en_ipa' && item.ipas && item.ipas[windex]}}" class="sentence-ipa">
</span>
<span wx:if="{{transDisplayMode === 'en_ipa' && item.ipas && item.ipas[windex]}}" class="sentence-ipa">
{{item.ipas[windex]}}
</text>
<text wx:else class="sentence-ipa"></text>
</view>
<view wx:if="{{transDisplayMode === 'en_zh'}}" class="word-zh">
<text>{{item.zh}}</text>
</span>
<span wx:else class="sentence-ipa"></span>
</view>
</view>
<view wx:if="{{transDisplayMode === 'en_zh'}}" class="word-zh">
<span>{{item.zh}}</span>
</view>
</view>
</view>
</view>
@@ -46,97 +46,97 @@
<view class="bottom-button-area">
<view class="button-row">
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" class="bottom-button" size="48rpx" bind:tap="playStandardVoice" />
<t-icon name="play-circle-stroke-add" class="bottom-button" size="48rpx" bind:tap="onSearchTap" />
<t-icon name="microphone-1" class="microphone {{isRecording ? 'recording' : ''}}" size="48rpx" bind:longpress="handleRecordStart" bind:touchend="handleRecordEnd" bind:touchcancel="handleRecordEnd" />
<t-icon name="file-search" class="bottom-button" size="48rpx" bind:tap="onHistoryTap" />
<t-icon name="{{transDisplayMode === 'en' ? 'file-teams' : transDisplayMode === 'en_ipa' ? 'file-search' : 'translate'}}" class="bottom-button" size="48rpx" bind:tap="onTransTap" />
<t-icon name="microphone-1" class="microphone bottom-button {{isRecording ? 'recording' : ''}}" size="48rpx" bind:longpress="handleRecordStart" bind:touchend="handleRecordEnd" bind:touchcancel="handleRecordEnd" />
<t-icon name="fact-check" class="bottom-button {{hasScoreInfo ? '' : 'disabled'}}" size="48rpx" bind:tap="onScoreTap" />
<view class="bottom-button-img-wrap bottom-button" bind:tap="onTransTap">
<image src="{{transDisplayMode === 'en_ipa' ? '/static/英.png' : transDisplayMode === 'en' ? '/static/文-中.png' : '/static/文.png'}}" style="width: 32rpx; height: 32rpx;" mode="aspectFit" />
</view>
</view>
</view>
<!-- 底部评分结果区域 -->
<view wx:if="{{hasScoreInfo}}" class="score-section {{isScoreExpanded ? 'expanded' : ''}}" bindtap="toggleScoreSection">
<view class="score-container">
<view class="score-overview">
<view class="total-score">
<view class="circle-progress" style="{{circleProgressStyle}}">
<text class="total-score-value">{{totalScore}}</text>
<text class="total-score-label">总分</text>
</view>
</view>
<view class="score-details">
<view class="score-item">
<text class="score-label">准确性</text>
<view class="score-content">
<block wx:if="{{accuracyScore >= 0}}">
<view class="progress-bar">
<view class="progress-fill" style="width: {{accuracyScore}}%"></view>
</view>
<text class="score-value">{{accuracyScore}}</text>
</block>
<text wx:else class="no-score-text">暂无评分</text>
</view>
</view>
<view class="score-item">
<text class="score-label">完整性</text>
<view class="score-content">
<block wx:if="{{completenessScore >= 0}}">
<view class="progress-bar">
<view class="progress-fill" style="width: {{completenessScore}}%"></view>
</view>
<text class="score-value">{{completenessScore}}</text>
</block>
<text wx:else class="no-score-text">暂无评分</text>
</view>
</view>
<view class="score-item">
<text class="score-label">流利度</text>
<view class="score-content">
<block wx:if="{{fluencyScore >= 0}}">
<view class="progress-bar">
<view class="progress-fill" style="width: {{fluencyScore}}%"></view>
</view>
<text class="score-value">{{fluencyScore}}</text>
</block>
<text wx:else class="no-score-text">暂无评分</text>
</view>
</view>
<!-- 评分结果弹窗 -->
<view wx:if="{{isScoreModalOpen && hasScoreInfo}}">
<view class="score-modal-mask" bindtap="onCloseScoreModal"></view>
<view class="score-modal-content" catchtouchmove="true">
<view class="score-modal-header">
<view class="score-modal-title">
<!-- <span>口语评分结果</span> -->
</view>
<t-icon name="close" class="score-modal-close" size="40rpx" bind:tap="onCloseScoreModal" />
</view>
<!-- MatchTag 说明 -->
<view class="match-tag-legend">
<view class="legend-header">
<text class="legend-title">匹配说明:</text>
<view class="legend-items">
<view class="legend-item" wx:for="{{matchTagLegend}}" wx:key="tag">
<view class="color-box" style="background-color: {{item.color}}"></view>
<text class="legend-text">{{item.description}}</text>
</view>
</view>
<view class="score-container">
<view class="score-image-container">
<image wx:if="{{currentSentence && currentSentence.file_id}}" src="{{playIconSrc}}" class="score-modal-play" bind:tap="playAssessmentVoice" mode="aspectFit" />
<span class="score-text">{{currentSentence.content}}</span>
</view>
</view>
<!-- 单词评分列表 -->
<view class="word-scores-list">
<view class="word-score-item" wx:for="{{wordScores}}" wx:key="word" style="background-color: {{matchTagLegend[item.matchTag || 0].color}}">
<view class="word-header">
<text class="word-text">{{item.word}}</text>
<view class="word-score-details">
<view class="word-score-row">
<text class="word-score-label">准确性</text>
<text class="word-score-value">{{item.pronAccuracy}}</text>
<view class="score-overview">
<view class="score-circles">
<view class="circle-item">
<view class="circle-progress" style="{{circleProgressStyle}}">
<canvas canvas-id="totalCircleCanvas" class="circle-canvas"></canvas>
<text class="total-score-value">{{totalScore}}</text>
<text class="total-score-label">总分</text>
</view>
<view class="word-score-row">
<text class="word-score-label">流利度</text>
<text class="word-score-value">{{item.pronFluency}}</text>
</view>
<view class="circle-item">
<view class="circle-progress" style="{{accuracyCircleStyle}}">
<canvas canvas-id="accuracyCircleCanvas" class="circle-canvas"></canvas>
<text class="total-score-value">{{accuracyScore}}</text>
<text class="total-score-label">准确性</text>
</view>
</view>
<view class="circle-item">
<view class="circle-progress" style="{{completenessCircleStyle}}">
<canvas canvas-id="completenessCircleCanvas" class="circle-canvas"></canvas>
<text class="total-score-value">{{completenessScore}}</text>
<text class="total-score-label">完整性</text>
</view>
</view>
<view class="circle-item">
<view class="circle-progress" style="{{fluencyCircleStyle}}">
<canvas canvas-id="fluencyCircleCanvas" class="circle-canvas"></canvas>
<text class="total-score-value">{{fluencyScore}}</text>
<text class="total-score-label">流利度</text>
</view>
</view>
</view>
<!-- 音标信息 -->
<view class="phone-infos" wx:if="{{item.phoneInfos && item.phoneInfos.length > 0}}">
<view class="phone-info-item" wx:for="{{item.phoneInfos}}" wx:for-item="phoneInfo" wx:key="phone" style="background-color: {{matchTagLegend[phoneInfo.matchTag || 0].color}}">
<text class="phone-text">[{{phoneInfo.phone}}]</text>
<text class="phone-score">{{phoneInfo.pronAccuracy}}</text>
</view>
<!-- MatchTag 说明 -->
<view class="match-tag-legend">
<view class="legend-header">
<text class="legend-title">匹配说明:</text>
<view class="legend-items">
<view class="legend-item" wx:for="{{matchTagLegend}}" wx:key="tag">
<view class="color-box" style="background-color: {{item.color}}"></view>
<text class="legend-text">{{item.description}}</text>
</view>
</view>
</view>
</view>
<!-- 单词评分列表 -->
<scroll-view scroll-y class="word-scores-list">
<view class="word-score-item" wx:for="{{wordScores}}" wx:key="word" style="background-color: {{matchTagLegend[item.matchTag || 0].color}}">
<view class="word-header">
<text class="word-text">{{item.word}}</text>
<view class="word-score-details">
<view class="word-score-row">
<text class="word-score-label">准确性</text>
<text class="word-score-value">{{item.pronAccuracy}}</text>
</view>
<view class="word-score-row">
<text class="word-score-label">流利度</text>
<text class="word-score-value">{{item.pronFluency}}</text>
</view>
</view>
</view>
<!-- 音标信息 -->
<view class="phone-infos" wx:if="{{item.phoneInfos && item.phoneInfos.length > 0}}">
<view class="phone-info-item" wx:for="{{item.phoneInfos}}" wx:for-item="phoneInfo" wx:key="phone" style="background-color: {{matchTagLegend[phoneInfo.matchTag || 0].color}}">
<text class="phone-text">[{{phoneInfo.phone}}]</text>
<text class="phone-score">{{phoneInfo.pronAccuracy}}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
@@ -146,12 +146,12 @@
<view class="popup-header">
<!-- 频率标签 -->
<view class="frequency-tags">
<t-tag wx:if="{{wordDict.ec.exam_type && wordDict.ec.exam_type.length > 0}}" wx:for="{{wordDict.ec.exam_type}}" wx:key="index" variant="light" theme="success" style="margin-right: 12rpx;">
<t-tag wx:if="{{wordDict.ec.exam_type && wordDict.ec.exam_type.length > 0}}" wx:for="{{wordDict.ec.exam_type}}" wx:key="index" variant="light" theme="success">
{{item}}
</t-tag>
</view>
<view class="close-btn" bindtap="handleDictClose">
<t-icon name="close" size="48rpx" />
<t-icon name="close" size="36rpx" />
</view>
</view>
<view class="popup-header">
@@ -159,9 +159,9 @@
<text class="word-title" wx:if="{{prototypeWord}}">{{prototypeWord}}</text>
<view class="more-btn" bindtap="handleDictMore">
<text wx:if="{{!showDictExtended}}">More</text>
<text wx:else>Less</text>
<!-- <text wx:if="{{!showDictExtended}}">Less</text> -->
<t-icon wx:if="{{!showDictExtended}}" name="chevron-up" size="48rpx"></t-icon>
<t-icon wx:else name="chevron-down" size="32rpx"></t-icon>
<!-- <t-icon wx:else name="chevron-down" size="32rpx"></t-icon> -->
</view>
</view>
<!-- 发音区域 -->
@@ -192,7 +192,7 @@
</block>
</view>
<!-- 扩展内容(默认隐藏,点击 more 后显示) -->
<t-tabs animation="{{ { duration: 0.1 } }}" defaultValue="{{3}}" bind:change="onTabsChange" bind:click="onTabsClick" wx:if="{{showDictExtended}}">
<t-tabs class="t-tabs" animation="{{ { duration: 0.1 } }}" defaultValue="{{3}}" bind:change="onTabsChange" bind:click="onTabsClick" wx:if="{{showDictExtended}}">
<t-tab-panel label="词典" value="3" wx:if="{{wordDict.ee && wordDict.ee.word && wordDict.ee.word.trs && wordDict.ee.word.trs.length > 0}}">
<view class="dictionary">
<view class="dictionary-content" wx:for="{{wordDict.ee.word.trs}}" wx:key="index">
@@ -257,6 +257,4 @@
</t-tab-panel>
</t-tabs>
</view>
<!-- 遮罩层 -->
<view class="mask" wx:if="{{isScoreExpanded}}" catchtap="toggleScoreSection"></view>
</view>

View File

@@ -1,10 +1,10 @@
.assessment-container {
min-height: 100vh;
/* min-height: 100vh; */
/* background-color: #f5f5f5; */
/* background-color: #fef6e4; */
background-color: #faeee7;
display: flex;
flex-direction: column;
padding-bottom: 200rpx;
}
/* 顶部图片区域 */
@@ -16,26 +16,33 @@
.assessment-image {
display: block;
width: 200rpx;
height: 200rpx;
width: 60%;
height: auto;
margin: 40rpx auto 0;
border-radius: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
transition: width 500ms ease, height 500ms ease, margin 500ms ease, border-radius 500ms ease;
}
.assessment-image.small {
width: 200rpx;
height: 200rpx;
margin: 20rpx auto 0; /* 缩小时减少上边距,视觉更紧凑 */
}
/* 中间例句区域 */
.sentence-section {
flex: 1;
padding: 0 32rpx;
display: flex;
flex-direction: column;
min-height: calc(100vh - 500rpx);
}
.sentence-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
margin-top: 30rpx;
}
.arrow-btn {
@@ -74,13 +81,18 @@
flex-direction: column;
align-items: center;
width: 100%;
/* margin-top: 30rpx; */
padding: 24rpx 0;
border-bottom: 4rpx solid #f5f5f5;
}
.sentence-content-wrapper {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
justify-content: flex-start;
gap: 10rpx;
color: #666;
}
.word-wrapper {
@@ -91,13 +103,21 @@
.sentence-text {
font-size: 32rpx;
color: #181818;
line-height: 32rpx;
}
.sentence-ipa {
color: #666666;
font-size: 24rpx;
line-height: 24rpx;
line-height: 30rpx;
padding-top: 10rpx;
}
.word-zh {
width: 100%;
text-align: left;
font-size: 26rpx;
padding-top: 12rpx;
}
.page-indicator {
@@ -172,19 +192,12 @@
}
.microphone {
width: 80rpx;
height: 80rpx;
padding: 20rpx;
background: #001858;
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;
background: #ff3b30 !important;
box-shadow: 0 4rpx 16rpx rgba(255, 59, 48, 0.3);
animation: pulse 1.5s infinite;
}
@@ -260,10 +273,15 @@
.score-container {
display: flex;
flex-direction: column;
width: 100%;
padding: 20rpx 40rpx;
transition: all 0.3s ease-in-out;
}
.score-image-container {
display: flex;
align-items: center;
}
.score-overview {
display: flex;
align-items: flex-start;
@@ -295,6 +313,16 @@
flex-shrink: 0;
}
.circle-progress {
position: relative;
}
.circle-canvas {
position: absolute;
width: 150rpx;
height: 150rpx;
top: 0;
left: 0;
}
.circle-progress {
width: 180rpx;
height: 180rpx;
@@ -317,19 +345,21 @@
}
.total-score-value {
font-size: 48rpx;
font-size: 28rpx;
color: #001858;
font-weight: bold;
position: relative;
z-index: 1;
line-height: 28rpx;
}
.total-score-label {
font-size: 24rpx;
font-size: 22rpx;
color: #666666;
position: relative;
z-index: 1;
margin-top: 4rpx;
line-height: 22rpx;
}
.score-details {
@@ -363,7 +393,7 @@
font-size: 24rpx;
color: #001858;
font-weight: bold;
min-width: 48rpx;
width: 80rpx;
text-align: right;
}
@@ -401,7 +431,8 @@
/* 单词评分列表样式 */
.match-tag-legend {
padding: 16rpx;
margin-top: 40rpx;
padding: 16rpx 0;
border-top: 1rpx solid #f0f0f0;
}
@@ -445,9 +476,8 @@
display: flex;
flex-direction: column;
gap: 16rpx;
max-height: calc(80vh - 400rpx);
height: 60vh;
overflow-y: auto;
padding: 16rpx;
}
.word-score-item {
@@ -535,7 +565,7 @@
border-top-left-radius: 12rpx;
border-top-right-radius: 12rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
padding: 20rpx;
padding: 0 20rpx;
box-sizing: border-box;
transition: all 0.3s ease;
z-index: 100;
@@ -552,9 +582,16 @@
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-top: 20rpx;
position: relative;
}
.t-sticky {
position: sticky !important;
top: 0;
z-index: 2 !important;
}
.word-title {
font-size: 40rpx;
font-weight: bold;
@@ -571,8 +608,8 @@
/* 关闭按钮 */
.close-btn {
margin-left: 20rpx;
flex-shrink: 0;
margin-left: 20rpx;
}
.more-btn {
@@ -658,8 +695,7 @@
}
.progress-bar {
height: 100%;
background: #007AFF;
background: #d3d0d0;
}
.example-sentence {
@@ -851,6 +887,10 @@
/* 底部按钮区域 */
.bottom-button-area {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 20rpx 0;
background: #ffffff;
border-top: 1rpx solid #e0e0e0;
@@ -869,11 +909,23 @@
color: #666;
}
.bottom-button.disabled {
opacity: 0.4;
pointer-events: none;
}
.bottom-button:active {
background: #e0e0e0;
color: #333;
}
.bottom-button-img-wrap {
width: 46rpx;
height: 46rpx;
display: flex;
align-items: center;
justify-content: center;
}
/* 词典样式 */
.dictionary {
padding: 20rpx;
@@ -923,3 +975,590 @@
margin-right: 10rpx;
margin-bottom: 10rpx;
}
.sentence-wrapper.selected {
/* border-color: #001858; */
border-bottom: 2rpx solid #001858;
/* background-color: #f0f7ff; */
}
.sentence-wrapper.selected .sentence-text {
color: #001858;
font-weight: 600;
}
/* 评分弹窗头部新增播放按钮样式 */
.score-modal-title {
display: flex;
align-items: center;
gap: 16rpx;
}
.score-modal-play {
flex-shrink: 0;
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
}
.score-modal-close {
position: absolute;
top: 16rpx;
right: 16rpx;
color: #666;
}
/* 弹窗内部的滚动与布局优化 */
.score-modal-content .score-container {
/* max-height: calc(86vh - 120rpx); */
overflow-y: auto;
}
.score-modal-content {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
z-index: 999;
}
.score-modal-header {
padding: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.score-overview {
padding-top: 40rpx;
}
.score-circles {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24rpx;
}
.circle-item {
flex: 1;
display: flex;
justify-content: center;
}
/* 统一圆形大小 */
.circle-item .circle-progress {
width: 150rpx;
height: 150rpx;
}
.circle-item .circle-progress::before {
width: 120rpx;
height: 120rpx;
}
.circle-progress::before {
content: '';
position: absolute;
width: 150rpx;
height: 150rpx;
border-radius: 50%;
background: #ffffff;
}
.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: #001858;
font-weight: bold;
width: 80rpx;
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: #001858;
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;
}
.legend-header {
display: flex;
align-items: center;
gap: 16rpx;
}
.legend-title {
font-size: 24rpx;
color: #333;
white-space: nowrap;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.legend-item {
display: flex;
align-items: center;
gap: 8rpx;
}
.color-box {
width: 20rpx;
height: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 4rpx;
}
.legend-text {
font-size: 22rpx;
color: #666;
}
.word-score-item {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 16rpx;
border: 1rpx solid #e0e0e0;
border-radius: 8rpx;
transition: background-color 0.3s ease;
}
.word-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.phone-infos {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 8rpx;
padding-top: 8rpx;
border-top: 1rpx dashed #e0e0e0;
}
.phone-info-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 4rpx 8rpx;
border-radius: 4rpx;
border: 1rpx solid #e0e0e0;
}
.phone-text {
font-size: 24rpx;
color: #333;
}
.phone-score {
font-size: 22rpx;
color: #666;
}
.word-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
.word-score-details {
display: flex;
gap: 24rpx;
}
.word-score-row {
display: flex;
align-items: center;
gap: 8rpx;
justify-content: flex-end;
}
.word-score-label {
font-size: 22rpx;
color: #666;
}
.word-score-value {
font-size: 22rpx;
color: #333;
font-weight: 500;
text-align: right;
}
/* 弹窗整体样式 */
.word-popup {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-top-left-radius: 12rpx;
border-top-right-radius: 12rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
padding: 0 20rpx;
box-sizing: border-box;
transition: all 0.3s ease;
z-index: 100;
overflow-y: scroll;
max-height: 100%;
}
.expanded {
height: 100vh;
}
/* 弹窗头部 */
.popup-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-top: 20rpx;
position: relative;
}
.t-sticky {
position: sticky !important;
top: 0;
z-index: 2 !important;
}
.word-title {
font-size: 40rpx;
font-weight: bold;
}
/* 频率标签容器 */
.frequency-tags {
flex: 1;
display: flex;
flex-wrap: wrap;
gap: 12rpx;
min-height: 48rpx; /* 确保即使没有标签也有最小高度 */
}
/* 关闭按钮 */
.close-btn {
flex-shrink: 0;
margin-left: 20rpx;
}
.more-btn {
display: flex;
font-size: 30rpx;
}
/* 发音区域 */
.pronounce {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20rpx;
}
.pron-item {
display: flex;
align-items: center;
margin-right: 20rpx;
font-size: 24rpx;
line-height: 30rpx;
}
.pron-item-text {
padding-right: 8rpx;
}
.sound-btn {
background: transparent;
border: none;
margin-left: 10rpx;
}
/* 基础词性释义 */
.word-meanings {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
font-size: 24rpx;
}
/* 扩展内容区域 */
.extended-content {
margin-top: 20rpx;
}
.tab-bar {
display: flex;
margin-bottom: 20rpx;
}
.tab {
padding: 10rpx 20rpx;
margin-right: 10rpx;
font-size: 28rpx;
}
.tab.active {
color: #007AFF;
border-bottom: 2rpx solid #007AFF;
}
.syllabus-title, .example-title {
font-weight: bold;
margin-bottom: 10rpx;
display: block;
}
.syllabus-stats {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
}
.progress-item {
margin-bottom: 10rpx;
}
.progress {
height: 10rpx;
background: #eee;
margin-top: 10rpx;
}
.progress-bar {
background: #d3d0d0;
}
.example-sentence {
display: flex;
flex-direction: column;
}
/* 真题例句样式 */
.exam-sents {
padding: 20rpx;
}
.exam-sent {
margin-bottom: 30rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
border-radius: 10rpx;
}
.exam-sent-en {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 6rpx;
}
.exam-sent-zh {
font-size: 28rpx;
color: #999;
margin-bottom: 6rpx;
}
.exam-sent-source {
font-size: 24rpx;
color: #ccc;
text-align: right;
}
.no-exam-sents {
text-align: center;
font-size: 28rpx;
color: #999;
padding: 40rpx 0;
}
/* 同义词样式 */
.syno {
margin-bottom: 30rpx;
}
.syno-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.syno-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.syno-pos {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.syno-tran {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
}
.syno-item {
display: inline-block;
padding: 10rpx 20rpx;
background: #e3f2fd;
border-radius: 30rpx;
margin-right: 15rpx;
margin-bottom: 15rpx;
font-size: 26rpx;
color: #001858;
}
/* 相关单词样式 */
.rel-word {
margin-bottom: 30rpx;
}
.rel-word-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.rel-word-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.rel-word-pos {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.rel-word-item {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 1rpx solid #eee;
}
.rel-word-item:last-child {
border-bottom: none;
}
.rel-word-tran {
font-size: 28rpx;
color: #333;
flex: 1;
}
.rel-word-word {
font-size: 28rpx;
color: #001858;
font-weight: 500;
margin-left: 20rpx;
}
/* 区分单词样式 */
.discriminate {
margin-bottom: 30rpx;
}
.discriminate-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.discriminate-list {
padding: 20rpx;
background: #f9f9f9;
border-radius: 10rpx;
}
.discriminate-item {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
border-bottom: 1rpx solid #eee;
}
.discriminate-item:last-child {
border-bottom: none;
}
.discriminate-tran {
font-size: 28rpx;
color: #333;
flex: 1;
}
.button-row {
display: flex;
justify-content: space-around;
align-items: center;
}

View File

@@ -320,7 +320,10 @@ Page({
imageId: recognitionResult.image_id,
// bubbleList,
animationStage: 'result',
showResultArea: true
// showResultArea: true
})
wx.navigateTo({
url: '/pages/assessment/assessment?imageId=' + recognitionResult.image_id
})
}, 1000)

View File

@@ -1,18 +1,20 @@
{
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-steps": "tdesign-miniprogram/steps/steps",
"t-step-item": "tdesign-miniprogram/step-item/step-item",
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet",
"t-grid": "tdesign-miniprogram/grid/grid",
"t-grid-item": "tdesign-miniprogram/grid-item/grid-item",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton"
},
"navigationBarTitleText": "",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#fffffe",
"backgroundColor": "#fffffe",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false,
"onReachBottomDistance": 50
{
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-steps": "tdesign-miniprogram/steps/steps",
"t-step-item": "tdesign-miniprogram/step-item/step-item",
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet",
"t-grid": "tdesign-miniprogram/grid/grid",
"t-grid-item": "tdesign-miniprogram/grid-item/grid-item",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"navigation-bar": "/components/navigation-bar/navigation-bar"
},
"navigationBarTitleText": "",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#fffffe",
"navigationStyle": "custom",
"backgroundColor": "#fffffe",
"backgroundTextStyle": "light",
"enablePullDownRefresh": true,
"onReachBottomDistance": 50
}

View File

@@ -192,7 +192,7 @@ Page({
this.setData({
groupedHistory: groupedArray,
page: result.page,
page: page,
total: result.total,
hasMore: newData.length < result.total,
isLoading: false,
@@ -227,20 +227,12 @@ Page({
// 图片点击事件
onImageTap(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: '取消',
} as ActionSheet.ShowOption);
const { imageId } = e.currentTarget.dataset;
if (imageId) {
wx.navigateTo({
url: `/pages/assessment/assessment?imageId=${imageId}`
})
}
},
onImageCardTap(e: any) {
@@ -255,9 +247,11 @@ Page({
theme: ActionSheetTheme.Grid,
selector: '#t-images-sheet',
context: this,
align: 'center',
description: '请选择图片',
items,
cancelText: '取消'
} as ActionSheet.ShowOption);
});
},
// 检查登录状态
@@ -293,11 +287,8 @@ Page({
// 更新登录状态
updateLoginStatus() {
this.setData({
isLoggedIn: app.globalData.isLoggedIn,
showLoginView: !app.globalData.isLoggedIn,
userInfo: app.globalData.userInfo
})
const isLoggedIn = !!app.globalData.token
this.setData({ isLoggedIn, showLoginView: !isLoggedIn })
},
// 处理图片选择(合并拍照和相册选择功能)
@@ -307,6 +298,9 @@ Page({
ActionSheet.show({
theme: ActionSheetTheme.List,
selector: '#t-action-sheet',
context: this,
align: 'center',
description: '请选择操作',
items: [
{
label: '拍照识别',
@@ -317,10 +311,8 @@ Page({
icon: 'image'
}
],
cancelText: '取消',
align: 'center',
description: ''
} as ActionSheet.ShowOption);
cancelText: '取消'
});
},
handleSelected(e: any) {
@@ -429,6 +421,13 @@ Page({
}
},
// 跳转到个人主页
goProfile() {
wx.navigateTo({
url: '/pages/profile/profile'
})
}
// 退出登录
// handleLogout() {
// wx.showModal({

View File

@@ -1,4 +1,9 @@
<!-- upload.wxml - 主功能页面 -->
<navigation-bar title="" back="{{false}}" color="{{day_type === 'night' ? 'white' : 'black'}}" background="{{day_type === 'night' ? '#232946' : '#fffffe'}}">
<view slot="left">
<t-icon class="user-home" name="user-circle" size="46rpx" bind:tap="goProfile" />
</view>
</navigation-bar>
<view class="{{['upload-container', day_type]}}">
<!-- 登录检查界面 -->
<view wx:if="{{showLoginView}}">

View File

@@ -16,6 +16,7 @@
background-color: #232946;
padding: 0;
position: relative;
padding-top: calc(var(--height) + 50px);
}
/* 主要内容区域 */
@@ -111,6 +112,7 @@
.images-list {
margin-top: 20rpx;
width: 100%;
height: 100rpx;
white-space: nowrap;
display: flex;
gap: 16rpx;
@@ -147,7 +149,7 @@
}
.star-icon {
position: fixed;
top: 140px;
top: 198px;
right: 40px;
font-size: 50rpx;
color: #FFD700;
@@ -274,7 +276,7 @@
.cloud-icon {
position: fixed;
top: 440rpx;
top: 430rpx;
left: -100rpx;
color: #eff0f3;
opacity: 0.8;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
miniprogram/static/文.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
miniprogram/static/英.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

View File

@@ -1419,6 +1419,7 @@ class ApiManager {
id: string
content: string,
ipa: string,
file_id: string,
details: {
assessment: {
code: number
@@ -1452,6 +1453,7 @@ class ApiManager {
id: string
content: string,
ipa: string,
file_id: string,
details: {
assessment: {
code: number