fix ui
This commit is contained in:
@@ -16,15 +16,16 @@ Component({
|
||||
type: Array,
|
||||
value: [
|
||||
{ tag: 0, description: '匹配', color: '#ffffff' },
|
||||
{ tag: 1, description: '新增', color: '#e6f0ff' },
|
||||
{ tag: 2, description: '缺少', color: '#eef7ff' },
|
||||
{ tag: 3, description: '错读', color: '#eaf3ff' },
|
||||
{ tag: 4, description: '未录入', color: '#f5f7fa' }
|
||||
{ tag: 1, description: '新增', color: '#ffebee' },
|
||||
{ tag: 2, description: '缺少', color: '#e3f2fd' },
|
||||
{ tag: 3, description: '错读', color: '#fff3e0' },
|
||||
{ tag: 4, description: '未录入', color: '#f5f5f5' }
|
||||
]
|
||||
},
|
||||
playIconName: { type: String, value: 'sound-low' }
|
||||
},
|
||||
methods: {
|
||||
noop() {},
|
||||
onClose() {
|
||||
const self = this as any
|
||||
self.triggerEvent('close')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<view wx:if="{{hasScoreInfo}}">
|
||||
<view class="score-modal-content {{visible ? 'show' : ''}}">
|
||||
<view class="score-modal-content {{visible ? 'show' : ''}}" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop" catchtap="noop">
|
||||
<view class="score-modal-header">
|
||||
<view class="score-modal-title"></view>
|
||||
<t-icon name="close" class="score-modal-close" size="40rpx" bindtap="onClose" />
|
||||
</view>
|
||||
<view class="score-container">
|
||||
<scroll-view class="score-container" scroll-y="true">
|
||||
<view class="score-image-container">
|
||||
<t-icon wx:if="{{sentence && sentence.file_id}}" name="{{playIconName}}" class="score-modal-play" size="60rpx" bindtap="onPlay"></t-icon>
|
||||
<view class="score-text">{{sentence.content}}</view>
|
||||
@@ -47,7 +47,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="word-scores-list">
|
||||
<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>
|
||||
@@ -69,7 +69,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
.score-modal-title { display: flex; align-items: center; gap: 16rpx; }
|
||||
.score-modal-close { position: absolute; top: 12rpx; right: 12rpx; color: #666; }
|
||||
.score-container { overflow-y: auto; }
|
||||
.score-container { height: 100vh; }
|
||||
.score-image-container { display: flex; align-items: center; padding: 0 0 0 12rpx; min-height: 120rpx; }
|
||||
.score-text { flex: 1; font-size: 30rpx; line-height: 42rpx; color: #001858; font-weight: 600; word-break: break-word; }
|
||||
.score-modal-play { flex-shrink: 0; margin-right: 20rpx; }
|
||||
@@ -49,4 +49,3 @@
|
||||
.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; }
|
||||
|
||||
|
||||
@@ -122,9 +122,12 @@ Component({
|
||||
const self = this as any
|
||||
self.triggerEvent('back')
|
||||
},
|
||||
onWordTap() {
|
||||
onWordTap(e: any) {
|
||||
const self = this as any
|
||||
const word = (self.data && self.data.prototypeWord) ? self.data.prototypeWord : ''
|
||||
const dsWord = e && e.currentTarget && e.currentTarget.dataset ? e.currentTarget.dataset.word : ''
|
||||
const propWord = (self.data && self.data.prototypeWord) ? self.data.prototypeWord : ''
|
||||
const word = dsWord || propWord || ''
|
||||
if (!word) return
|
||||
self.triggerEvent('wordTap', { word })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
class="overlay-word {{highlightShow ? 'show' : ''}} {{highlightZoom ? 'zoom' : ''}}"
|
||||
style="left: {{item.left}}px; top: {{item.top}}px; width: {{item.width}}px; height: {{item.height}}px; {{item.transform ? ('transform: ' + item.transform) : ''}}">
|
||||
<view class="overlay-text {{analizing ? 'loading-shimmer' : ''}}" style="animation-delay: {{index * 200}}ms;">{{item.text}}</view>
|
||||
<view class="overlay-text {{analizing ? 'loading-shimmer' : ''}}" style="animation-delay: {{index * 200}}ms; {{index === 0 ? 'text-transform: capitalize;' : ''}}">{{item.text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isRecording}}" class="recording-mask" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop"></view>
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
<!--coupon.wxml-->
|
||||
<view class="coupon-container">
|
||||
<!-- <view class="points_box">
|
||||
<view class="points-title">
|
||||
<t-icon class="points-label" name="filter-3" size="28rpx"></t-icon>
|
||||
当前积分
|
||||
</view>
|
||||
<view class="points-row">
|
||||
<view class="points-value">{{displayUserPoints}}</view>
|
||||
</view>
|
||||
<view class="vip-pill" wx:if="{{vipLevel > 0}}">
|
||||
<text class="vip-star">★</text>
|
||||
<text class="vip-text">VIP Member Level {{vipLevel}}</text>
|
||||
</view>
|
||||
<view class="vip-pill" wx:else>
|
||||
<text class="vip-star">★</text>
|
||||
<text class="vip-text">Member</text>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="coupon_title">
|
||||
获取更多积分
|
||||
</view>
|
||||
<view class='coupon_box {{item.one_time ? "one_time" : ""}}' wx:for="{{products}}" wx:key="id" wx:for-item="item">
|
||||
<!-- <view class='coupon_box {{item.one_time ? "one_time" : ""}}' wx:for="{{products}}" wx:key="id" wx:for-item="item">
|
||||
<view class='content' bindtap="handleCouponTap" data-id="{{item.id}}" data-points="{{item.points}}">
|
||||
<view class='title'>{{item.title}}</view>
|
||||
<view class='how_much'>{{item.points}}</view>
|
||||
</view>
|
||||
<view class='btn'> ¥{{item.amountYuan}}</view>
|
||||
</view> -->
|
||||
|
||||
<view class="card-box">
|
||||
<view class="card" wx:for="{{products}}" wx:key="id" wx:for-item="item" bindtap="handleCouponTap" data-id="{{item.id}}" data-points="{{item.points}}">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
<view class="card-points">{{item.points}}</view>
|
||||
<view class="card-credits">{{item.description ? item.description : ' '}}</view>
|
||||
<view class="card-price">¥{{item.amountYuan}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tips_box">
|
||||
<view class="tips-header">
|
||||
<view class="tips-icon"><t-icon name="info-circle" size="32rpx"></t-icon></view>
|
||||
|
||||
@@ -55,95 +55,65 @@
|
||||
line-height: 38rpx;
|
||||
}
|
||||
|
||||
.points_box {
|
||||
width: 100%;
|
||||
margin: 40rpx 2%;
|
||||
padding: 36rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
background: linear-gradient(135deg, #6a5af9 0%, #a855f7 60%, #9333ea 100%);
|
||||
box-shadow: 0 12rpx 30rpx rgba(123, 67, 255, 0.35);
|
||||
position: relative;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.points_box::before,
|
||||
.points_box::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 50%;
|
||||
filter: blur(12rpx);
|
||||
pointer-events: none;
|
||||
}
|
||||
.points_box::before {
|
||||
bottom: -80rpx;
|
||||
left: -80rpx;
|
||||
background: radial-gradient(closest-side, rgba(255,255,255,0.20), rgba(255,255,255,0));
|
||||
}
|
||||
.points_box::after {
|
||||
top: -80rpx;
|
||||
right: -80rpx;
|
||||
background: radial-gradient(closest-side, rgba(255,255,255,0.16), rgba(255,255,255,0));
|
||||
}
|
||||
|
||||
.points-title {
|
||||
font-size: 26rpx;
|
||||
opacity: 0.92;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.points-title .points-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.points-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 16rpx;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.points-value {
|
||||
font-size: 96rpx;
|
||||
line-height: 96rpx;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 8rpx 22rpx rgba(0,0,0,0.18);
|
||||
}
|
||||
|
||||
.points-label {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.vip-pill {
|
||||
margin-top: 18rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 14rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255,255,255,0.18);
|
||||
box-shadow: inset 0 0 0 2rpx rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.vip-star {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.vip-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.coupon_title {
|
||||
width: 100%;
|
||||
padding: 0 2%;
|
||||
padding: 24rpx 2% 0 24rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-box {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 0 8rpx 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 26%;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 28rpx 2% 24rpx 2%;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #4b5565;
|
||||
margin-bottom: 12rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-points {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
line-height: 60rpx;
|
||||
}
|
||||
|
||||
.card-credits {
|
||||
margin-top: 4rpx;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 2rpx;
|
||||
color: #6b7280;
|
||||
min-height: 32rpx;
|
||||
}
|
||||
|
||||
.card-price {
|
||||
margin-top: 18rpx;
|
||||
background: #eef2f7;
|
||||
color: #111827;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 9999rpx;
|
||||
}
|
||||
|
||||
.coupon_box{
|
||||
background: linear-gradient(to right, #5da0f9, #3071ee);
|
||||
width: 46%;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "场景句子",
|
||||
"navigationBarTitleText": "场景句型",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
|
||||
@@ -42,6 +42,10 @@ interface IData {
|
||||
accuracyScore: number
|
||||
completenessScore: number
|
||||
fluencyScore: number
|
||||
displayTotalScore: number
|
||||
displayAccuracyScore: number
|
||||
displayCompletenessScore: number
|
||||
displayFluencyScore: number
|
||||
circleProgressStyle: string
|
||||
accuracyCircleStyle: string
|
||||
completenessCircleStyle: string
|
||||
@@ -56,6 +60,17 @@ interface IData {
|
||||
dictDefaultTabValue?: string
|
||||
standardAudioMap?: Record<string, string>
|
||||
standardAudioLocalMap?: Record<string, string>
|
||||
assessmentAudioLocalMap?: Record<string, string>
|
||||
assessmentCache?: Record<string, {
|
||||
hasScoreInfo: boolean
|
||||
totalScore: number
|
||||
accuracyScore: number
|
||||
completenessScore: number
|
||||
fluencyScore: number
|
||||
wordScores: Array<any>
|
||||
file_id?: string
|
||||
content?: string
|
||||
}>
|
||||
wordAudioPlaying?: boolean
|
||||
wordAudioIconName?: string
|
||||
activeWordAudioType?: string
|
||||
@@ -73,6 +88,8 @@ interface IData {
|
||||
analizing?: boolean
|
||||
cachedHighlightWords?: Array<any>
|
||||
cachedSentenceIndex?: number
|
||||
loadingDots?: string
|
||||
loadingLabel?: string
|
||||
}
|
||||
|
||||
interface IPageInstance {
|
||||
@@ -80,7 +97,9 @@ interface IPageInstance {
|
||||
audioCtx?: any
|
||||
wordAudioContext?: WechatMiniprogram.InnerAudioContext
|
||||
wordAudioIconTimer?: number
|
||||
circleAnimTimer?: number
|
||||
recordTimer?: number
|
||||
loadingDotsTimer?: number
|
||||
fetchSceneSentence: (imageId: string) => Promise<void>
|
||||
startPolling: (taskId: string, imageId: string) => void
|
||||
onTransTap: () => void
|
||||
@@ -97,12 +116,16 @@ interface IPageInstance {
|
||||
handleDictMore: () => void
|
||||
processCollinsData: (collinsData: any) => any
|
||||
getStandardVoice: (sentenceId: string) => Promise<void>
|
||||
updateCircleProgress: () => void
|
||||
playAssessmentVoice: () => void
|
||||
noop: () => void
|
||||
startRecording: () => void
|
||||
stopRecording: () => void
|
||||
onMicHighlight: () => void
|
||||
computeHighlightLayout: () => void
|
||||
fetchRecordResultForSentence: (textId: string) => Promise<void>
|
||||
startLoadingDots: () => void
|
||||
stopLoadingDots: () => void
|
||||
}
|
||||
|
||||
Page<IData, IPageInstance>({
|
||||
@@ -124,6 +147,10 @@ Page<IData, IPageInstance>({
|
||||
accuracyScore: 0,
|
||||
completenessScore: 0,
|
||||
fluencyScore: 0,
|
||||
displayTotalScore: 0,
|
||||
displayAccuracyScore: 0,
|
||||
displayCompletenessScore: 0,
|
||||
displayFluencyScore: 0,
|
||||
circleProgressStyle: '',
|
||||
accuracyCircleStyle: '',
|
||||
completenessCircleStyle: '',
|
||||
@@ -138,6 +165,8 @@ Page<IData, IPageInstance>({
|
||||
dictDefaultTabValue: '0',
|
||||
standardAudioMap: {},
|
||||
standardAudioLocalMap: {},
|
||||
assessmentAudioLocalMap: {},
|
||||
assessmentCache: {},
|
||||
wordAudioPlaying: false,
|
||||
wordAudioIconName: 'sound',
|
||||
activeWordAudioType: '',
|
||||
@@ -154,7 +183,9 @@ Page<IData, IPageInstance>({
|
||||
highlightZoom: false,
|
||||
analizing: false,
|
||||
cachedHighlightWords: [],
|
||||
cachedSentenceIndex: -1
|
||||
cachedSentenceIndex: -1,
|
||||
loadingDots: '',
|
||||
loadingLabel: '场景分析中'
|
||||
},
|
||||
noop() {},
|
||||
|
||||
@@ -166,6 +197,7 @@ Page<IData, IPageInstance>({
|
||||
return
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true, statusText: '加载中...', currentIndex: 0, transDisplayMode: 'en_zh', contentVisible: false, isPlaying: false, isRecording: false, hasScoreInfo: false, isScoreModalOpen: false, scoreModalVisible: false })
|
||||
this.startLoadingDots()
|
||||
if (!this.audioCtx) {
|
||||
this.audioCtx = wx.createInnerAudioContext()
|
||||
this.audioCtx.onEnded(() => { this.setData({ isPlaying: false }) })
|
||||
@@ -215,16 +247,37 @@ Page<IData, IPageInstance>({
|
||||
completenessScore: pronCompletion >= 0 ? Number(pronCompletion.toFixed(2)) : 0,
|
||||
fluencyScore: pronFluency >= 0 ? Number(pronFluency.toFixed(2)) : 0,
|
||||
wordScores,
|
||||
currentSentence: {
|
||||
content: String(this.data.currentSentence?.content || ''),
|
||||
id: this.data.currentSentence?.id,
|
||||
file_id: fileId
|
||||
},
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
this.onScoreTap()
|
||||
}).catch(() => {
|
||||
this.setData({
|
||||
analizing: false
|
||||
})
|
||||
const cache = this.data.assessmentCache || {}
|
||||
this.setData({
|
||||
assessmentCache: {
|
||||
...cache,
|
||||
[imageTextId]: {
|
||||
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,
|
||||
file_id: fileId,
|
||||
content: String(this.data.currentSentence?.content || '')
|
||||
}
|
||||
}
|
||||
})
|
||||
this.onScoreTap()
|
||||
}).catch(() => {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
@@ -310,20 +363,21 @@ Page<IData, IPageInstance>({
|
||||
'These mint leaves look crisp and fresh.(强调口感)'
|
||||
]
|
||||
const responsePairs = (cur as any)?.responsePairs || []
|
||||
this.setData({
|
||||
scene,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '已获取数据',
|
||||
contentVisible: true,
|
||||
this.setData({
|
||||
scene,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '已获取数据',
|
||||
contentVisible: true,
|
||||
currentIndex: idx,
|
||||
currentSentence,
|
||||
englishWords,
|
||||
buttonsVisible: true,
|
||||
commonMistakes,
|
||||
pragmaticAlternative,
|
||||
responsePairs,
|
||||
hasScoreInfo: false
|
||||
})
|
||||
responsePairs,
|
||||
hasScoreInfo: false
|
||||
})
|
||||
this.stopLoadingDots()
|
||||
const sid = cur && cur.imageTextId
|
||||
if (sid) {
|
||||
try { this.fetchRecordResultForSentence(String(sid)) } catch (e) {}
|
||||
@@ -331,12 +385,34 @@ Page<IData, IPageInstance>({
|
||||
} else {
|
||||
const { task_id } = await apiManager.createScene(imageId, 'scene_sentence')
|
||||
this.setData({ taskId: task_id, statusText: '解析中...' })
|
||||
this.startLoadingDots()
|
||||
this.startPolling(task_id, imageId)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('获取场景句子失败:', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
const msg = (e as any)?.message || ''
|
||||
if (typeof msg === 'string' && msg.indexOf('积分') !== -1) {
|
||||
wx.showModal({
|
||||
title: '积分不足',
|
||||
content: '您的积分不足,是否前往购买?',
|
||||
confirmText: '获取',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.redirectTo({ url: '/pages/coupon/coupon' })
|
||||
} else {
|
||||
wx.navigateBack({ delta: 1 })
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: msg || '上传失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
this.setData({ loadingMaskVisible: false })
|
||||
this.stopLoadingDots()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -346,6 +422,7 @@ Page<IData, IPageInstance>({
|
||||
this.pollTimer = undefined
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
this.startLoadingDots()
|
||||
this.pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiManager.getSceneTask(taskId)
|
||||
@@ -358,12 +435,29 @@ Page<IData, IPageInstance>({
|
||||
this.pollTimer = undefined
|
||||
wx.showToast({ title: '任务失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '任务失败' })
|
||||
this.stopLoadingDots()
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('轮询任务状态失败:', err)
|
||||
}
|
||||
}, 3000) as any
|
||||
},
|
||||
startLoadingDots() {
|
||||
if (this.loadingDotsTimer) { clearInterval(this.loadingDotsTimer) }
|
||||
let n = 0
|
||||
this.loadingDotsTimer = setInterval(() => {
|
||||
n = (n % 3) + 1
|
||||
const dots = '.'.repeat(n)
|
||||
this.setData({ loadingDots: dots })
|
||||
}, 500) as any
|
||||
},
|
||||
stopLoadingDots() {
|
||||
if (this.loadingDotsTimer) {
|
||||
clearInterval(this.loadingDotsTimer)
|
||||
this.loadingDotsTimer = undefined
|
||||
}
|
||||
this.setData({ loadingDots: '' })
|
||||
},
|
||||
|
||||
onTransTap() {
|
||||
const mode = this.data.transDisplayMode === 'en' ? 'en_zh' : 'en'
|
||||
@@ -411,14 +505,53 @@ Page<IData, IPageInstance>({
|
||||
})
|
||||
const sid = cur && cur.imageTextId
|
||||
if (sid) {
|
||||
wx.nextTick(() => { this.getStandardVoice(String(sid)) })
|
||||
wx.nextTick(() => { this.fetchRecordResultForSentence(String(sid)) })
|
||||
const sidStr = String(sid)
|
||||
const cachedAssess = (this.data.assessmentCache || {})[sidStr]
|
||||
if (cachedAssess && cachedAssess.hasScoreInfo) {
|
||||
this.setData({
|
||||
hasScoreInfo: true,
|
||||
totalScore: cachedAssess.totalScore,
|
||||
accuracyScore: cachedAssess.accuracyScore,
|
||||
completenessScore: cachedAssess.completenessScore,
|
||||
fluencyScore: cachedAssess.fluencyScore,
|
||||
wordScores: cachedAssess.wordScores || [],
|
||||
currentSentence: {
|
||||
content: String(currentSentence?.content || cachedAssess.content || ''),
|
||||
id: sidStr,
|
||||
file_id: cachedAssess.file_id || currentSentence?.file_id
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.nextTick(() => { this.fetchRecordResultForSentence(sidStr) })
|
||||
}
|
||||
const hasStd = !!((this.data.standardAudioMap || {})[sidStr])
|
||||
if (!hasStd) {
|
||||
wx.nextTick(() => { this.getStandardVoice(sidStr) })
|
||||
}
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
|
||||
async fetchRecordResultForSentence(textId: string) {
|
||||
try {
|
||||
const cached = (this.data.assessmentCache || {})[textId]
|
||||
if (cached && cached.hasScoreInfo) {
|
||||
const curSentence = this.data.currentSentence || { content: '' }
|
||||
this.setData({
|
||||
hasScoreInfo: true,
|
||||
totalScore: cached.totalScore,
|
||||
accuracyScore: cached.accuracyScore,
|
||||
completenessScore: cached.completenessScore,
|
||||
fluencyScore: cached.fluencyScore,
|
||||
wordScores: cached.wordScores || [],
|
||||
currentSentence: {
|
||||
content: String(curSentence.content || cached.content || ''),
|
||||
id: curSentence.id || textId,
|
||||
file_id: cached.file_id || curSentence.file_id
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const rec: any = await apiManager.getRecordResult(textId)
|
||||
const hasDetails = !!(rec && rec.details && Object.keys(rec.details || {}).length > 0)
|
||||
if (!hasDetails) {
|
||||
@@ -452,6 +585,22 @@ Page<IData, IPageInstance>({
|
||||
wordScores,
|
||||
currentSentence: { ...curSentence, file_id: rec?.file_id || curSentence.file_id }
|
||||
})
|
||||
const cache = this.data.assessmentCache || {}
|
||||
this.setData({
|
||||
assessmentCache: {
|
||||
...cache,
|
||||
[textId]: {
|
||||
hasScoreInfo: true,
|
||||
totalScore: Number((suggestedScore || 0).toFixed(2)),
|
||||
accuracyScore: Number((pronAccuracy || 0).toFixed(2)),
|
||||
completenessScore: Number((pronCompletion || 0).toFixed(2)),
|
||||
fluencyScore: Number((pronFluency || 0).toFixed(2)),
|
||||
wordScores,
|
||||
file_id: rec?.file_id || curSentence.file_id,
|
||||
content: String(curSentence.content || '')
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
this.setData({ hasScoreInfo: false })
|
||||
}
|
||||
@@ -523,6 +672,56 @@ Page<IData, IPageInstance>({
|
||||
})
|
||||
}
|
||||
},
|
||||
playAssessmentVoice() {
|
||||
if (this.data.isRecording) return
|
||||
const fileId = this.data.currentSentence?.file_id
|
||||
if (!fileId) {
|
||||
wx.showToast({ title: '暂无评分音频', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const cachedLocal = (this.data.assessmentAudioLocalMap || {})[fileId]
|
||||
const playWithPath = (filePath: string) => {
|
||||
if (this.data.isPlaying && this.audioCtx?.src === filePath) {
|
||||
try { this.audioCtx.pause() } catch (e) {}
|
||||
try { this.audioCtx.seek(0) } catch (e) {}
|
||||
this.setData({ isPlaying: false })
|
||||
return
|
||||
}
|
||||
if (!this.audioCtx) {
|
||||
this.audioCtx = wx.createInnerAudioContext()
|
||||
try { (this.audioCtx as any).obeyMuteSwitch = false } catch (e) {}
|
||||
try { (this.audioCtx as any).autoplay = false } catch (e) {}
|
||||
this.audioCtx.onEnded(() => { this.setData({ isPlaying: false }) })
|
||||
this.audioCtx.onStop(() => { this.setData({ isPlaying: false }) })
|
||||
this.audioCtx.onError(() => { this.setData({ isPlaying: false }) })
|
||||
}
|
||||
if (this.audioCtx.src !== filePath) {
|
||||
try { this.audioCtx.pause() } catch (e) {}
|
||||
try { this.audioCtx.seek(0) } catch (e) {}
|
||||
this.audioCtx.src = filePath
|
||||
} else {
|
||||
try { this.audioCtx.seek(0) } catch (e) {}
|
||||
}
|
||||
try {
|
||||
this.audioCtx.play()
|
||||
this.setData({ isPlaying: true })
|
||||
} catch (error) {
|
||||
wx.showToast({ title: '音频播放失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
if (cachedLocal) {
|
||||
playWithPath(cachedLocal)
|
||||
} else {
|
||||
apiManager.downloadFile(fileId).then((filePath) => {
|
||||
const map = this.data.assessmentAudioLocalMap || {}
|
||||
this.setData({ assessmentAudioLocalMap: { ...map, [fileId]: filePath } })
|
||||
playWithPath(filePath)
|
||||
}).catch((error) => {
|
||||
logger.error('下载音频失败:', error)
|
||||
wx.showToast({ title: '音频下载失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleRecordStart() {
|
||||
if (this.data.isRecording) return
|
||||
@@ -536,12 +735,39 @@ Page<IData, IPageInstance>({
|
||||
|
||||
onScoreTap() {
|
||||
if (!this.data.hasScoreInfo) return
|
||||
this.setData({ isScoreModalOpen: true })
|
||||
setTimeout(() => this.setData({ scoreModalVisible: true }), 50)
|
||||
this.setData({
|
||||
isScoreModalOpen: true,
|
||||
scoreModalVisible: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
analizing: false
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.setData({ scoreModalVisible: true })
|
||||
setTimeout(() => {
|
||||
try { this.updateCircleProgress() } catch (e) {}
|
||||
}, 100)
|
||||
}, 0)
|
||||
},
|
||||
|
||||
onCloseScoreModal() {
|
||||
this.setData({ isScoreModalOpen: false, scoreModalVisible: false })
|
||||
if (this.circleAnimTimer) {
|
||||
clearInterval(this.circleAnimTimer)
|
||||
this.circleAnimTimer = undefined
|
||||
}
|
||||
this.setData({ scoreModalVisible: false })
|
||||
this.setData({
|
||||
circleProgressStyle: 'background: conic-gradient(transparent 0)',
|
||||
accuracyCircleStyle: 'background: conic-gradient(transparent 0)',
|
||||
completenessCircleStyle: 'background: conic-gradient(transparent 0)',
|
||||
fluencyCircleStyle: 'background: conic-gradient(transparent 0)',
|
||||
displayTotalScore: 0,
|
||||
displayAccuracyScore: 0,
|
||||
displayCompletenessScore: 0,
|
||||
displayFluencyScore: 0,
|
||||
isScoreModalOpen: false
|
||||
})
|
||||
},
|
||||
|
||||
async onWordTap(e: any) {
|
||||
@@ -596,6 +822,77 @@ Page<IData, IPageInstance>({
|
||||
this.setData({ dictLoading: false })
|
||||
}
|
||||
},
|
||||
updateCircleProgress() {
|
||||
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)
|
||||
const pickColor = (pct: number) => {
|
||||
if (pct >= 70) return 'rgb(39, 174, 96)'
|
||||
if (pct >= 40) return 'rgb(241, 196, 15)'
|
||||
return 'rgb(231, 76, 60)'
|
||||
}
|
||||
const totalColor = pickColor(totalPct)
|
||||
const accColor = pickColor(accPct)
|
||||
const compColor = pickColor(compPct)
|
||||
const fluColor = pickColor(fluPct)
|
||||
if (this.circleAnimTimer) {
|
||||
clearInterval(this.circleAnimTimer)
|
||||
this.circleAnimTimer = undefined
|
||||
}
|
||||
const duration = 800
|
||||
const start = Date.now()
|
||||
const compose = (pct: number, color: string) => {
|
||||
if (!hasScoreInfo || pct <= 0) {
|
||||
return 'background: conic-gradient(transparent 0)'
|
||||
}
|
||||
return `background: conic-gradient(${color} ${pct}%, transparent 0)`
|
||||
}
|
||||
const tick = () => {
|
||||
const elapsed = Date.now() - start
|
||||
const progressPct = Math.min(100, Math.round((elapsed / duration) * 100))
|
||||
const curTotal = Math.min(progressPct, totalPct)
|
||||
const curAcc = Math.min(progressPct, accPct)
|
||||
const curComp = Math.min(progressPct, compPct)
|
||||
const curFlu = Math.min(progressPct, fluPct)
|
||||
const ratio = (target: number, current: number) => (target === 0 ? 0 : (current / target))
|
||||
const round2 = (v: number) => Math.round(v * 100) / 100
|
||||
this.setData({
|
||||
circleProgressStyle: compose(curTotal, totalColor),
|
||||
accuracyCircleStyle: compose(curAcc, accColor),
|
||||
completenessCircleStyle: compose(curComp, compColor),
|
||||
fluencyCircleStyle: compose(curFlu, fluColor),
|
||||
displayTotalScore: round2(totalScore * ratio(totalPct, curTotal)),
|
||||
displayAccuracyScore: round2(accuracyScore * ratio(accPct, curAcc)),
|
||||
displayCompletenessScore: round2(completenessScore * ratio(compPct, curComp)),
|
||||
displayFluencyScore: round2(fluencyScore * ratio(fluPct, curFlu)),
|
||||
})
|
||||
if (elapsed >= duration) {
|
||||
if (this.circleAnimTimer) {
|
||||
clearInterval(this.circleAnimTimer)
|
||||
this.circleAnimTimer = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setData({
|
||||
circleProgressStyle: compose(0, totalColor),
|
||||
accuracyCircleStyle: compose(0, accColor),
|
||||
completenessCircleStyle: compose(0, compColor),
|
||||
fluencyCircleStyle: compose(0, fluColor),
|
||||
displayTotalScore: 0,
|
||||
displayAccuracyScore: 0,
|
||||
displayCompletenessScore: 0,
|
||||
displayFluencyScore: 0,
|
||||
})
|
||||
this.circleAnimTimer = setInterval(tick, 16) as any
|
||||
},
|
||||
|
||||
handleDictClose() {
|
||||
this.setData({ showDictPopup: false, showDictExtended: false })
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<view class="scene-sentence-container">
|
||||
<view wx:if="{{loadingMaskVisible}}" class="page-loading-mask">
|
||||
<view class="scanner scanner-visible">
|
||||
<view class="star star1"></view>
|
||||
<view class="star star2"></view>
|
||||
<view class="star star3"></view>
|
||||
<view class="loading-center">
|
||||
<view class="scanner scanner-visible">
|
||||
<view class="star star1"></view>
|
||||
<view class="star star2"></view>
|
||||
<view class="star star3"></view>
|
||||
</view>
|
||||
<view class="status-text">{{loadingLabel}}{{loadingDots}}</view>
|
||||
</view>
|
||||
<view class="status-text">场景分析中...</view>
|
||||
</view>
|
||||
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{scene && scene.list && scene.list.length > 0}}">
|
||||
<block wx:if="{{scene.list[currentIndex]}}">
|
||||
@@ -103,7 +105,7 @@
|
||||
<view wx:for="{{highlightWords}}" wx:key="index"
|
||||
class="overlay-word {{highlightShow ? 'show' : ''}} {{highlightZoom ? 'zoom' : ''}}"
|
||||
style="left: {{item.left}}px; top: {{item.top}}px; width: {{item.width}}px; height: {{item.height}}px; {{item.transform ? ('transform: ' + item.transform) : ''}}">
|
||||
<view class="overlay-text">{{item.text}}</view>
|
||||
<view class="overlay-text {{analizing ? 'loading-shimmer' : ''}}" style="animation-delay: {{index * 200}}ms; {{index === 0 ? 'text-transform: capitalize;' : ''}}">{{item.text}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{isRecording}}" class="recording-mask" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop"></view>
|
||||
@@ -124,17 +126,17 @@
|
||||
visible="{{scoreModalVisible}}"
|
||||
hasScoreInfo="{{hasScoreInfo}}"
|
||||
sentence="{{currentSentence}}"
|
||||
totalScore="{{totalScore}}"
|
||||
accuracyScore="{{accuracyScore}}"
|
||||
completenessScore="{{completenessScore}}"
|
||||
fluencyScore="{{fluencyScore}}"
|
||||
totalScore="{{displayTotalScore}}"
|
||||
accuracyScore="{{displayAccuracyScore}}"
|
||||
completenessScore="{{displayCompletenessScore}}"
|
||||
fluencyScore="{{displayFluencyScore}}"
|
||||
circleProgressStyle="{{circleProgressStyle}}"
|
||||
accuracyCircleStyle="{{accuracyCircleStyle}}"
|
||||
completenessCircleStyle="{{completenessCircleStyle}}"
|
||||
fluencyCircleStyle="{{fluencyCircleStyle}}"
|
||||
wordScores="{{wordScores}}"
|
||||
bind:close="onCloseScoreModal"
|
||||
bind:play="playStandardVoice"
|
||||
bind:play="playAssessmentVoice"
|
||||
/>
|
||||
<word-dictionary
|
||||
visible="{{showDictPopup}}"
|
||||
|
||||
@@ -18,6 +18,16 @@
|
||||
background: transparent;
|
||||
z-index: 1000;
|
||||
}
|
||||
.loading-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
/* 遮罩层样式 */
|
||||
.word-popup-mask {
|
||||
position: fixed;
|
||||
@@ -117,6 +127,24 @@
|
||||
}
|
||||
.overlay-word.show { opacity: 1; }
|
||||
.overlay-word.zoom { transform-origin: center center; transition: transform 500ms ease; }
|
||||
.loading-shimmer {
|
||||
animation: loading-shimmer 3s ease infinite;
|
||||
}
|
||||
@keyframes loading-shimmer {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
color: #ffffff;
|
||||
}
|
||||
15% {
|
||||
transform: translate(0, -2rpx) scale(1.2);
|
||||
color: #eee;
|
||||
}
|
||||
30%, 100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.recording-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -387,12 +415,9 @@ background-color: #eef3fb;
|
||||
}
|
||||
|
||||
.scanner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: relative;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
pointer-events: none;
|
||||
border-radius: 12rpx;
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
|
||||
@@ -49,6 +49,8 @@ Page({
|
||||
scrollTop: 0,
|
||||
takePhoto: false,
|
||||
photoPath: '',
|
||||
photoImageLoaded: false,
|
||||
photoExpandLoaded: false,
|
||||
photoExpandTransform: '',
|
||||
photoExpandTransition: '',
|
||||
showExpandLayer: false,
|
||||
@@ -648,7 +650,7 @@ Page({
|
||||
const imagePath = await imageManager.takePhoto({})
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ photoPath: imagePath, takePhoto: true })
|
||||
this.setData({ photoPath: String(imagePath || '').trim(), takePhoto: true })
|
||||
logger.info('拍照成功:', imagePath)
|
||||
// try { wx.nextTick(() => { this.scheduleExpandTransform() }) } catch (err) {}
|
||||
|
||||
@@ -683,7 +685,7 @@ Page({
|
||||
const imagePath = await imageManager.chooseFromAlbum()
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ photoPath: imagePath, takePhoto: true })
|
||||
this.setData({ photoPath: String(imagePath || '').trim(), takePhoto: true })
|
||||
|
||||
logger.info('选择图片成功:', imagePath)
|
||||
// try { wx.nextTick(() => { this.scheduleExpandTransform() }) } catch (err) {}
|
||||
@@ -704,8 +706,68 @@ Page({
|
||||
}
|
||||
}
|
||||
},
|
||||
async onPhotoImageLoad() {
|
||||
try {
|
||||
this.setData({ photoImageLoaded: true })
|
||||
if (this.data.takePhoto && !this.data.showExpandLayer) {
|
||||
try { wx.nextTick(() => { this.scheduleExpandTransform() }) } catch {}
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
async onPhotoImageError() {
|
||||
try {
|
||||
const p = String(this.data.photoPath || '').trim()
|
||||
if (!p) return
|
||||
const fs = wx.getFileSystemManager()
|
||||
let persistent = ''
|
||||
try {
|
||||
persistent = fs.saveFileSync(p)
|
||||
} catch (e) {}
|
||||
if (persistent) {
|
||||
this.setData({ photoPath: persistent })
|
||||
} else {
|
||||
try {
|
||||
await wx.getImageInfo({ src: p } as any)
|
||||
} catch {
|
||||
wx.showToast({ title: '图片加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
async onPhotoExpandLoaded() {
|
||||
try {
|
||||
this.setData({ photoExpandLoaded: true })
|
||||
} catch {}
|
||||
},
|
||||
async onPhotoExpandError() {
|
||||
try {
|
||||
const p = String(this.data.photoPath || '').trim()
|
||||
if (!p) return
|
||||
const fs = wx.getFileSystemManager()
|
||||
let persistent = ''
|
||||
try {
|
||||
persistent = fs.saveFileSync(p)
|
||||
} catch (e) {}
|
||||
if (persistent) {
|
||||
this.setData({ photoPath: persistent })
|
||||
} else {
|
||||
try {
|
||||
await wx.getImageInfo({ src: p } as any)
|
||||
} catch {
|
||||
wx.showToast({ title: '图片加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
scheduleExpandTransform() {
|
||||
if (!this.data.photoImageLoaded) {
|
||||
const timer = setTimeout(() => {
|
||||
clearTimeout(timer)
|
||||
if (this.data.photoImageLoaded) this.scheduleExpandTransform()
|
||||
}, 200)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const win: any = (wx as any).getWindowInfo ? (wx as any).getWindowInfo() : sys
|
||||
@@ -759,6 +821,8 @@ Page({
|
||||
this.setData({
|
||||
takePhoto: false,
|
||||
photoPath: '',
|
||||
photoImageLoaded: false,
|
||||
photoExpandLoaded: false,
|
||||
showExpandLayer: false,
|
||||
photoExpandTransform: '',
|
||||
photoExpandTransition: '',
|
||||
@@ -803,13 +867,13 @@ Page({
|
||||
wx.showModal({
|
||||
title: '积分不足',
|
||||
content: '您的积分不足,是否前往购买?',
|
||||
confirmText: '购买',
|
||||
confirmText: '获取',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/coupon/coupon' })
|
||||
wx.redirectTo({ url: '/pages/coupon/coupon' })
|
||||
} else {
|
||||
// wx.showToast({ title: '已取消', icon: 'none' })
|
||||
// wx.navigateBack({ delta: 1 })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -932,7 +996,7 @@ Page({
|
||||
const y = Number(parts[0]);
|
||||
const m = Number(parts[1]);
|
||||
const gridCols = images.length <= 1 ? 1 : images.length <= 4 ? 2 : 3;
|
||||
const useWaterfall = images.length > 3;
|
||||
const useWaterfall = images.length > 2;
|
||||
let waterfallLeft: any[] = [], waterfallRight: any[] = [];
|
||||
if (useWaterfall) {
|
||||
images.forEach((img, idx) => { (idx % 2 === 0 ? waterfallLeft : waterfallRight).push(img); });
|
||||
|
||||
@@ -36,14 +36,14 @@
|
||||
<view class="photo-wrapper" wx:if="{{(!isLoading && todaySummary.length == 0 && groupedHistory.length == 0) || takePhoto}}">
|
||||
<view class="photo">
|
||||
<view class="photo-inner">
|
||||
<image class="photo-image" src="{{takePhoto ? photoPath : photoSvgData}}" mode="{{takePhoto ? 'widthFix' : 'aspectFit'}}"></image>
|
||||
<image class="photo-image" src="{{takePhoto ? photoPath : photoSvgData}}" mode="{{takePhoto ? 'widthFix' : 'aspectFit'}}" bindload="onPhotoImageLoad" binderror="onPhotoImageError"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{takePhoto && showExpandLayer }}" class="photo-expand-layer" style="{{photoExpandTransform}} {{photoExpandTransition}}">
|
||||
<view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}}">
|
||||
<!-- <view class="expand-border-bg" style="{{expandBorderStyle}}"></view> -->
|
||||
<image class="photo-expand-image" src="{{takePhoto ? photoPath : photoSvgData}}" mode="widthFix"></image>
|
||||
<image class="photo-expand-image" src="{{takePhoto ? photoPath : photoSvgData}}" mode="widthFix" bindload="onPhotoExpandLoaded" binderror="onPhotoExpandError"></image>
|
||||
<view class="scanner {{scannerVisible ? 'scanner-visible' : 'scanner-hidden'}}">
|
||||
<view class="star star1"></view>
|
||||
<view class="star star2"></view>
|
||||
|
||||
Reference in New Issue
Block a user