[lisa]feat: improve ui
@@ -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": "需要使用相机拍照识别图片"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {}
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,11 @@ Component({
|
||||
})
|
||||
}
|
||||
this.triggerEvent('back', { delta: data.delta }, {})
|
||||
},
|
||||
goProfile() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/profile/profile'
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "口语评估",
|
||||
"navigationBarTitleText": "识别结果",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#faeee7",
|
||||
"backgroundColor": "#f8f9fa",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"backgroundTextStyle": "light",
|
||||
"enablePullDownRefresh": false,
|
||||
"onReachBottomDistance": 50,
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -166,7 +166,7 @@ Page({
|
||||
|
||||
this.setData({
|
||||
groupedHistory: groupedArray,
|
||||
page: result.page,
|
||||
page: page,
|
||||
total: result.total,
|
||||
hasMore: newData.length < result.total,
|
||||
isLoading: false,
|
||||
@@ -201,20 +201,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) {
|
||||
@@ -229,9 +221,11 @@ Page({
|
||||
theme: ActionSheetTheme.Grid,
|
||||
selector: '#t-images-sheet',
|
||||
context: this,
|
||||
align: 'center',
|
||||
description: '请选择图片',
|
||||
items,
|
||||
cancelText: '取消'
|
||||
} as ActionSheet.ShowOption);
|
||||
});
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
@@ -267,11 +261,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 })
|
||||
},
|
||||
|
||||
// 处理图片选择(合并拍照和相册选择功能)
|
||||
@@ -281,6 +272,9 @@ Page({
|
||||
ActionSheet.show({
|
||||
theme: ActionSheetTheme.List,
|
||||
selector: '#t-action-sheet',
|
||||
context: this,
|
||||
align: 'center',
|
||||
description: '请选择操作',
|
||||
items: [
|
||||
{
|
||||
label: '拍照识别',
|
||||
@@ -291,10 +285,8 @@ Page({
|
||||
icon: 'image'
|
||||
}
|
||||
],
|
||||
cancelText: '取消',
|
||||
align: 'center',
|
||||
description: ''
|
||||
} as ActionSheet.ShowOption);
|
||||
cancelText: '取消'
|
||||
});
|
||||
},
|
||||
|
||||
handleSelected(e: any) {
|
||||
@@ -403,6 +395,13 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到个人主页
|
||||
goProfile() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/profile/profile'
|
||||
})
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
// handleLogout() {
|
||||
// wx.showModal({
|
||||
|
||||
@@ -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}}">
|
||||
|
||||
@@ -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;
|
||||
@@ -272,7 +274,7 @@
|
||||
|
||||
.cloud-icon {
|
||||
position: fixed;
|
||||
top: 440rpx;
|
||||
top: 430rpx;
|
||||
left: -100rpx;
|
||||
color: #eff0f3;
|
||||
opacity: 0.8;
|
||||
|
||||
BIN
miniprogram/static/.DS_Store
vendored
BIN
miniprogram/static/play-1.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
miniprogram/static/play-2.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
miniprogram/static/文-1.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
miniprogram/static/文-中-1.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
miniprogram/static/文-中.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
miniprogram/static/文.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
miniprogram/static/英-1.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
miniprogram/static/英.png
Normal file
|
After Width: | Height: | Size: 317 KiB |
@@ -1385,6 +1385,7 @@ class ApiManager {
|
||||
id: string
|
||||
content: string,
|
||||
ipa: string,
|
||||
file_id: string,
|
||||
details: {
|
||||
assessment: {
|
||||
code: number
|
||||
@@ -1418,6 +1419,7 @@ class ApiManager {
|
||||
id: string
|
||||
content: string,
|
||||
ipa: string,
|
||||
file_id: string,
|
||||
details: {
|
||||
assessment: {
|
||||
code: number
|
||||
|
||||