add sentence
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
"pages/privacy/privacy",
|
||||
"pages/analyze/analyze",
|
||||
"pages/coupon/coupon",
|
||||
"pages/order/order"
|
||||
"pages/order/order",
|
||||
"pages/scene_sentence/scene_sentence"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
|
||||
@@ -78,7 +78,7 @@ App<IAppOption>({
|
||||
this.globalData.token = authInfo.token
|
||||
this.globalData.userInfo = authInfo.userInfo
|
||||
// 初始化词典等级
|
||||
this.globalData.dictLevel = authInfo.dictLevel || 'LEVEL1'
|
||||
this.globalData.dictLevel = authInfo.dictLevel || 'level1'
|
||||
console.log('登录状态有效,自动登录')
|
||||
} else {
|
||||
console.log('Token 已过期,清理本地数据')
|
||||
@@ -101,7 +101,7 @@ App<IAppOption>({
|
||||
this.globalData.token = loginData.access_token
|
||||
this.globalData.userInfo = loginData.userInfo
|
||||
// 更新词典等级
|
||||
this.globalData.dictLevel = loginData.dict_level || 'LEVEL1'
|
||||
this.globalData.dictLevel = loginData.dict_level || 'level1'
|
||||
|
||||
// 存储到本地
|
||||
wx.setStorageSync('token', loginData.access_token)
|
||||
|
||||
9
miniprogram/components/score-modal/score-modal.json
Normal file
9
miniprogram/components/score-modal/score-modal.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
}
|
||||
}
|
||||
|
||||
37
miniprogram/components/score-modal/score-modal.ts
Normal file
37
miniprogram/components/score-modal/score-modal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
Component({
|
||||
properties: {
|
||||
visible: { type: Boolean, value: false },
|
||||
hasScoreInfo: { type: Boolean, value: false },
|
||||
sentence: { type: Object, value: null },
|
||||
totalScore: { type: Number, value: 0 },
|
||||
accuracyScore: { type: Number, value: 0 },
|
||||
completenessScore: { type: Number, value: 0 },
|
||||
fluencyScore: { type: Number, value: 0 },
|
||||
circleProgressStyle: { type: String, value: '' },
|
||||
accuracyCircleStyle: { type: String, value: '' },
|
||||
completenessCircleStyle: { type: String, value: '' },
|
||||
fluencyCircleStyle: { type: String, value: '' },
|
||||
wordScores: { type: Array, value: [] },
|
||||
matchTagLegend: {
|
||||
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' }
|
||||
]
|
||||
},
|
||||
playIconName: { type: String, value: 'sound-low' }
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
const self = this as any
|
||||
self.triggerEvent('close')
|
||||
},
|
||||
onPlay() {
|
||||
const self = this as any
|
||||
self.triggerEvent('play')
|
||||
}
|
||||
}
|
||||
})
|
||||
75
miniprogram/components/score-modal/score-modal.wxml
Normal file
75
miniprogram/components/score-modal/score-modal.wxml
Normal file
@@ -0,0 +1,75 @@
|
||||
<view wx:if="{{hasScoreInfo}}">
|
||||
<view class="score-modal-content {{visible ? 'show' : ''}}">
|
||||
<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">
|
||||
<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>
|
||||
</view>
|
||||
<view class="score-overview">
|
||||
<view class="score-circles">
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{circleProgressStyle}}">
|
||||
<text class="total-score-value">{{totalScore}}</text>
|
||||
<text class="total-score-label">总分</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{accuracyCircleStyle}}">
|
||||
<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}}">
|
||||
<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}}">
|
||||
<text class="total-score-value">{{fluencyScore}}</text>
|
||||
<text class="total-score-label">流利度</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="match-tag-legend">
|
||||
<view class="legend-header">
|
||||
<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>
|
||||
52
miniprogram/components/score-modal/score-modal.wxss
Normal file
52
miniprogram/components/score-modal/score-modal.wxss
Normal file
@@ -0,0 +1,52 @@
|
||||
.score-modal-content {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #f8fafc;
|
||||
z-index: 999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0ms ease-out;
|
||||
}
|
||||
.score-modal-content.show { transform: translateY(0); }
|
||||
.score-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 48rpx;
|
||||
}
|
||||
.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-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; }
|
||||
.score-overview {}
|
||||
.score-circles { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 24rpx; }
|
||||
.circle-item { display: flex; justify-content: center; background-color: #eee; border-radius: 50%; }
|
||||
.circle-progress { width: 120rpx; height: 120rpx; border-radius: 50%; background: #f7f7f7; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; }
|
||||
.circle-progress::before { content: ''; position: absolute; width: 100rpx; height: 100rpx; top: 10rpx; left: 10rpx; border-radius: 50%; background: #ffffff; }
|
||||
.total-score-value { font-size: 28rpx; color: #001858; font-weight: 600; position: relative; z-index: 1; line-height: 28rpx; }
|
||||
.total-score-label { font-size: 20rpx; color: #666666; position: relative; z-index: 1; margin-top: 8rpx; line-height: 20rpx; }
|
||||
.match-tag-legend { padding: 24rpx; }
|
||||
.legend-header { display: flex; align-items: center; gap: 16rpx; }
|
||||
.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-scores-list { display: flex; flex-direction: column; gap: 16rpx; overflow-y: auto; border-radius: 24rpx; }
|
||||
.word-score-item { display: flex; flex-direction: column; gap: 12rpx; padding: 20rpx; border-bottom: 2rpx solid #eee; }
|
||||
.word-score-item:last-child { border-bottom: none; }
|
||||
.word-header { display: flex; justify-content: space-between; align-items: center; }
|
||||
.phone-infos { display: flex; flex-wrap: wrap; gap: 8rpx; }
|
||||
.phone-info-item { display: flex; align-items: center; gap: 8rpx; padding: 6rpx 10rpx; border-radius: 8rpx; border: 1rpx solid rgba(0,0,0,0.06); }
|
||||
.phone-text { font-size: 24rpx; line-height: 24rpx; color: #333; }
|
||||
.phone-score { font-size: 24rpx; line-height: 24rpx; 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; }
|
||||
|
||||
12
miniprogram/components/word-dictionary/word-dictionary.json
Normal file
12
miniprogram/components/word-dictionary/word-dictionary.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-tabs": "tdesign-miniprogram/tabs/tabs",
|
||||
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton"
|
||||
}
|
||||
}
|
||||
131
miniprogram/components/word-dictionary/word-dictionary.ts
Normal file
131
miniprogram/components/word-dictionary/word-dictionary.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
Component({
|
||||
properties: {
|
||||
visible: { type: Boolean, value: false },
|
||||
expanded: { type: Boolean, value: false },
|
||||
loading: { type: Boolean, value: false },
|
||||
wordDict: { type: Object, value: {} },
|
||||
showBackIcon: { type: Boolean, value: false },
|
||||
prototypeWord: { type: String, value: '' },
|
||||
isWordEmptyResult: { type: Boolean, value: false },
|
||||
dictDefaultTabValue: { type: String, value: '0' },
|
||||
activeWordAudioType: { type: String, value: '' },
|
||||
wordAudioPlaying: { type: Boolean, value: false },
|
||||
wordAudioIconName: { type: String, value: 'sound' }
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
const self = this as any
|
||||
try { self.wordAudioContext && self.wordAudioContext.stop() } catch (e) {}
|
||||
try {
|
||||
if (self.wordAudioContext) {
|
||||
self.wordAudioContext.offPlay()
|
||||
self.wordAudioContext.offEnded()
|
||||
self.wordAudioContext.offError()
|
||||
self.wordAudioContext.destroy()
|
||||
self.wordAudioContext = undefined
|
||||
}
|
||||
} catch (e) {}
|
||||
if (self.wordAudioIconTimer) {
|
||||
clearInterval(self.wordAudioIconTimer)
|
||||
self.wordAudioIconTimer = undefined
|
||||
}
|
||||
self.setData({ wordAudioPlaying: false, wordAudioIconName: 'sound', activeWordAudioType: '' })
|
||||
self.triggerEvent('close')
|
||||
},
|
||||
onMore() {
|
||||
const self = this as any
|
||||
self.triggerEvent('more')
|
||||
},
|
||||
onTabsChange(e: any) {
|
||||
const self = this as any
|
||||
self.triggerEvent('tabsChange', { value: e?.detail?.value })
|
||||
},
|
||||
onTabsClick(e: any) {
|
||||
const self = this as any
|
||||
self.triggerEvent('tabsClick', { value: e?.detail?.value })
|
||||
},
|
||||
onPlayWordAudio(e: any) {
|
||||
const dataset = e?.currentTarget?.dataset || {}
|
||||
const type = dataset.type
|
||||
const audio = dataset.audio
|
||||
const self = this as any
|
||||
if (!audio) return
|
||||
if (self.data.wordAudioPlaying && self.data.activeWordAudioType === type) {
|
||||
try { self.wordAudioContext && self.wordAudioContext.stop() } catch (err) {}
|
||||
try {
|
||||
if (self.wordAudioContext) {
|
||||
self.wordAudioContext.offPlay()
|
||||
self.wordAudioContext.offEnded()
|
||||
self.wordAudioContext.offError()
|
||||
self.wordAudioContext.destroy()
|
||||
self.wordAudioContext = undefined
|
||||
}
|
||||
} catch (err) {}
|
||||
if (self.wordAudioIconTimer) {
|
||||
clearInterval(self.wordAudioIconTimer)
|
||||
self.wordAudioIconTimer = undefined
|
||||
}
|
||||
self.setData({ wordAudioPlaying: false, wordAudioIconName: 'sound', activeWordAudioType: '' })
|
||||
return
|
||||
}
|
||||
try { self.wordAudioContext && self.wordAudioContext.stop() } catch (err) {}
|
||||
try {
|
||||
if (self.wordAudioContext) {
|
||||
self.wordAudioContext.offPlay()
|
||||
self.wordAudioContext.offEnded()
|
||||
self.wordAudioContext.offError()
|
||||
self.wordAudioContext.destroy()
|
||||
self.wordAudioContext = undefined
|
||||
}
|
||||
} catch (err) {}
|
||||
if (self.wordAudioIconTimer) {
|
||||
clearInterval(self.wordAudioIconTimer)
|
||||
self.wordAudioIconTimer = undefined
|
||||
}
|
||||
const audioUrl = `https://dict.youdao.com/dictvoice?audio=${audio}`
|
||||
self.wordAudioContext = wx.createInnerAudioContext()
|
||||
try { (self.wordAudioContext as any).autoplay = false } catch (err) {}
|
||||
self.wordAudioContext.onPlay(() => {
|
||||
const seq = ['sound', 'sound-low']
|
||||
let i = 0
|
||||
self.setData({ wordAudioPlaying: true, activeWordAudioType: type, wordAudioIconName: seq[0] })
|
||||
self.wordAudioIconTimer = setInterval(() => {
|
||||
i = (i + 1) % seq.length
|
||||
self.setData({ wordAudioIconName: seq[i] })
|
||||
}, 400) as any
|
||||
})
|
||||
const finalize = () => {
|
||||
if (self.wordAudioIconTimer) {
|
||||
clearInterval(self.wordAudioIconTimer)
|
||||
self.wordAudioIconTimer = undefined
|
||||
}
|
||||
self.setData({ wordAudioPlaying: false, wordAudioIconName: 'sound', activeWordAudioType: '' })
|
||||
try {
|
||||
if (self.wordAudioContext) {
|
||||
self.wordAudioContext.offPlay()
|
||||
self.wordAudioContext.offEnded()
|
||||
self.wordAudioContext.offError()
|
||||
self.wordAudioContext.destroy()
|
||||
self.wordAudioContext = undefined
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
self.wordAudioContext.onEnded(() => { finalize() })
|
||||
self.wordAudioContext.onError(() => {
|
||||
wx.showToast({ title: '播放失败', icon: 'none' })
|
||||
finalize()
|
||||
})
|
||||
self.wordAudioContext.src = audioUrl
|
||||
self.wordAudioContext.play()
|
||||
},
|
||||
onBack() {
|
||||
const self = this as any
|
||||
self.triggerEvent('back')
|
||||
},
|
||||
onWordTap() {
|
||||
const self = this as any
|
||||
const word = (self.data && self.data.prototypeWord) ? self.data.prototypeWord : ''
|
||||
self.triggerEvent('wordTap', { word })
|
||||
}
|
||||
}
|
||||
})
|
||||
136
miniprogram/components/word-dictionary/word-dictionary.wxml
Normal file
136
miniprogram/components/word-dictionary/word-dictionary.wxml
Normal file
@@ -0,0 +1,136 @@
|
||||
<view class="word-popup {{expanded ? 'expanded' : ''}}" wx:if="{{visible}}">
|
||||
<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">
|
||||
{{item}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="close-btn" bindtap="onClose">
|
||||
<t-icon name="close" size="36rpx" />
|
||||
</view>
|
||||
</view>
|
||||
<t-skeleton wx:if="{{loading}}" theme="paragraph" animation="gradient" loading="{{true}}"></t-skeleton>
|
||||
<view wx:if="{{!loading}}" class="popup-header">
|
||||
<view wx:if="{{wordDict.simple.query}}" class="word-title">
|
||||
<t-icon wx:if="{{showBackIcon}}" name="chevron-left" size="40rpx" bindtap="onBack"></t-icon>
|
||||
{{wordDict.simple.query}}
|
||||
</view>
|
||||
<view class="word-source" wx:if="{{prototypeWord}}">词源: <t-tag variant="light" theme="primary" data-word="{{prototypeWord}}" bindtap="onWordTap">{{prototypeWord}}</t-tag></view>
|
||||
<view class="more-btn" wx:if="{{!isWordEmptyResult}}" bindtap="onMore">
|
||||
<text wx:if="{{!expanded}}">More</text>
|
||||
<t-icon wx:if="{{!expanded}}" name="chevron-up" size="48rpx"></t-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pronounce" wx:if="{{!loading}}">
|
||||
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
|
||||
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].ukphone}}">
|
||||
UK:[{{wordDict.simple.word[0].ukphone}}]
|
||||
</span>
|
||||
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].ukspeech}}" bind:tap="onPlayWordAudio" data-type="uk" data-audio="{{wordDict.simple.word[0].ukspeech}}" name="{{(activeWordAudioType === 'uk' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
|
||||
</view>
|
||||
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
|
||||
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].usphone}}">
|
||||
US:[{{wordDict.simple.word[0].usphone}}]
|
||||
</span>
|
||||
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].usspeech}}" bind:tap="onPlayWordAudio" data-type="us" data-audio="{{wordDict.simple.word[0].usspeech}}" name="{{(activeWordAudioType === 'us' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!loading}}" class="word-meanings">
|
||||
<block wx:if="{{wordDict.ec && wordDict.ec.word && wordDict.ec.word.length > 0}}">
|
||||
<block wx:for="{{wordDict.ec.word[0].trs}}" wx:key="index">
|
||||
<block wx:for="{{item.tr}}" wx:key="index">
|
||||
<block wx:for="{{item.l.i}}" wx:key="index">
|
||||
<text>{{item}}</text>
|
||||
</block>
|
||||
</block>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<t-tabs class="t-tabs" animation="{{ { duration: 0.1 } }}" defaultValue="{{dictDefaultTabValue}}" bind:change="onTabsChange" bind:click="onTabsClick" wx:if="{{expanded}}">
|
||||
<t-tab-panel label="使用" value="0" wx:if="{{wordDict.collins && wordDict.collins.collins_entries && wordDict.collins.collins_entries.length > 0 && wordDict.collins.collins_entries[0].entries && wordDict.collins.collins_entries[0].entries.entry}}">
|
||||
<view class="dictionary">
|
||||
<block wx:for="{{wordDict.collins.collins_entries[0].entries.entry}}" wx:key="index">
|
||||
<block wx:if="{{item.tran_entry && item.tran_entry.length > 0}}">
|
||||
<view class="dictionary-content" wx:for="{{item.tran_entry}}" wx:key="tindex">
|
||||
<view class="dictionary-pos" wx:if="{{item.pos_entry && item.pos_entry.pos && item.pos_entry.pos_tips}}">
|
||||
<t-tag variant="light" theme="primary">
|
||||
{{item.pos_entry.pos}} {{item.pos_entry.pos_tips}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="dictionary-tran" wx:if="{{item.tranParts && item.tranParts.length > 0}}">
|
||||
<block wx:for="{{item.tranParts}}" wx:key="pindex">
|
||||
<text wx:if="{{!item.bold}}" class="normal-text">{{item.text}}</text>
|
||||
<text wx:if="{{item.bold}}" class="dictionary-tran-bold">{{item.text}}</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="dictionary-tran" wx:elif="{{item.tran}}">{{item.tran}}</view>
|
||||
<view class="dictionary-list" wx:if="{{item.exam_sents && item.exam_sents.sent && item.exam_sents.sent.length > 0}}">
|
||||
<block wx:for="{{item.exam_sents.sent}}" wx:key="index">
|
||||
<view class="word-sent-en" wx:if="{{item.eng_sent}}">{{item.eng_sent}}</view>
|
||||
<view class="word-sent-zh" wx:if="{{item.chn_sent}}">{{item.chn_sent}}</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.phrs && wordDict.phrs.phrs && wordDict.phrs.phrs.length > 0}}" label="短语" value="1">
|
||||
<block wx:for="{{wordDict.phrs.phrs}}" wx:key="index">
|
||||
<t-cell title="{{item.phr.headword.l.i}}" description="{{item.phr.trs[0].tr.l.i}}" />
|
||||
</block>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.individual && wordDict.individual.pastExamSents && wordDict.individual.pastExamSents.length > 0}}" label="真题" value="2">
|
||||
<view wx:for="{{wordDict.individual.pastExamSents}}" wx:key="index" class="exam-sent">
|
||||
<view class="exam-sent-en">{{item.en}}</view>
|
||||
<view class="exam-sent-zh">{{item.zh}}</view>
|
||||
<view class="exam-sent-source">--摘自《{{item.source}}》</view>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0}}" label="相关" value="3">
|
||||
<view wx:if="{{(wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0) || (wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0) || (wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0)}}">
|
||||
<view class="syno" wx:if="{{wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0}}">
|
||||
<view class="syno-title">同义词</view>
|
||||
<view class="syno-list" wx:for="{{wordDict.syno.synos}}" wx:key="index">
|
||||
<view class="syno-pos">
|
||||
<t-tag wx:if="{{item.syno.pos}}" variant="light" theme="primary">
|
||||
{{item.syno.pos}}
|
||||
</t-tag>
|
||||
<view class="syno-tran">{{item.syno.tran}}</view>
|
||||
</view>
|
||||
<t-tag class="syno-item" wx:for="{{item.syno.ws}}" wx:key="w" variant="light">
|
||||
{{item.w}}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rel-word" wx:if="{{wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0}}">
|
||||
<view class="rel-word-title">相关单词</view>
|
||||
<view class="rel-word-list" wx:for="{{wordDict.relWord.rels}}" wx:key="index">
|
||||
<view class="rel-word-pos">
|
||||
<t-tag wx:if="{{item.rel.pos}}" variant="light" theme="primary">
|
||||
{{item.rel.pos}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="rel-word-item" wx:for="{{item.rel.words}}" wx:key="word">
|
||||
<view class="rel-word-tran">{{item.tran}}</view>
|
||||
<view class="rel-word-word">{{item.word}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="discriminate" wx:if="{{wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0}}">
|
||||
<view class="discriminate-title">区分单词</view>
|
||||
<view class="discriminate-list" wx:for="{{wordDict.discriminate.data}}" wx:key="index">
|
||||
<view class="discriminate-item" wx:for="{{item.usages}}" wx:key="headword">
|
||||
<view class="discriminate-tran">{{item.usage}}</view>
|
||||
<view class="discriminate-headword">{{item.headword}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="no-related-words">
|
||||
<text class="no-related-words-text">暂无相关单词信息</text>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
</t-tabs>
|
||||
<view class="no-word" wx:if="{{isWordEmptyResult}}">没有更多内容</view>
|
||||
</view>
|
||||
197
miniprogram/components/word-dictionary/word-dictionary.wxss
Normal file
197
miniprogram/components/word-dictionary/word-dictionary.wxss
Normal file
@@ -0,0 +1,197 @@
|
||||
.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;
|
||||
min-height: 600rpx;
|
||||
}
|
||||
.word-popup.expanded {
|
||||
height: 100vh;
|
||||
border-top-left-radius: 0rpx;
|
||||
border-top-right-radius: 0rpx;
|
||||
}
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 20rpx;
|
||||
position: relative;
|
||||
}
|
||||
.word-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.word-source {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.word-meanings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.dictionary {
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
.dictionary-content {
|
||||
margin-bottom: 20rpx;
|
||||
font-size: 28rpx;
|
||||
border-bottom: 1rpx solid #f5f5f5;
|
||||
}
|
||||
.dictionary-pos {
|
||||
display: inline;
|
||||
color: #33272a;
|
||||
}
|
||||
.dictionary-tran {
|
||||
display: inline;
|
||||
padding-left: 16rpx;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.dictionary-tran-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.dictionary-list {
|
||||
padding: 16rpx 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.exam-sent {
|
||||
padding: 10rpx 0;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
}
|
||||
.exam-sent-en {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.exam-sent-zh {
|
||||
font-size: 28rpx;
|
||||
color: #909090;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.exam-sent-source {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
text-align: right;
|
||||
}
|
||||
.syno {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.syno-title {
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
.syno-list {
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.syno-pos {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.syno-tran {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin: 12rpx 0;
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
.syno-item {
|
||||
display: inline-block;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
.rel-word {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
.rel-word-title {
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.rel-word-list {
|
||||
padding: 20rpx;
|
||||
background: #f9f9f9;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.rel-word-pos {
|
||||
font-size: 28rpx;
|
||||
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;
|
||||
}
|
||||
.no-related-words-text {
|
||||
font-size: 28rpx;
|
||||
color: #909090;
|
||||
}
|
||||
.no-word {
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #909090;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
@@ -6,12 +6,14 @@
|
||||
"backgroundTextStyle": "light",
|
||||
"enablePullDownRefresh": false,
|
||||
"onReachBottomDistance": 50,
|
||||
"usingComponents": {
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-tabs": "tdesign-miniprogram/tabs/tabs",
|
||||
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton"
|
||||
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
|
||||
"score-modal": "/components/score-modal/score-modal",
|
||||
"word-dictionary": "/components/word-dictionary/word-dictionary"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ let recorderHandlersBound = false
|
||||
|
||||
interface IPageData {
|
||||
imagePath: string
|
||||
imageId?: string
|
||||
imageSmall: boolean
|
||||
imageMode: 'widthFix' | 'aspectFit'
|
||||
imageMode: 'widthFix' | 'aspectFit' | 'aspectFill'
|
||||
imageScale: number
|
||||
imageTranslateY: number
|
||||
currentSentence: any
|
||||
sentences: any[]
|
||||
currentIndex: number
|
||||
@@ -136,6 +139,7 @@ interface IPageData {
|
||||
sentenceMarginTop: number,
|
||||
standardAudioLocalMap: { [key: string]: string }, // 标准语音文件ID映射
|
||||
assessmentAudioLocalMap: { [key: string]: string } // 评估语音文件ID映射
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
type IPageMethods = {
|
||||
@@ -167,9 +171,11 @@ type IPageMethods = {
|
||||
onPageScroll: (e: any) => void
|
||||
processCollinsData: (collinsData: any) => any // Add this line
|
||||
computeHighlightLayout:() => void
|
||||
computeDynamicLayout: () => void
|
||||
onMicHighlight: () => void
|
||||
ensureRecordPermission: () => void
|
||||
onMoreTap: () => void
|
||||
onSceneSentenceTap: () => void
|
||||
noop: () => void
|
||||
}
|
||||
|
||||
@@ -192,6 +198,7 @@ interface IPageInstance extends IPageMethods {
|
||||
Page<IPageData, IPageInstance>({
|
||||
data: {
|
||||
imagePath: '', // 图片路径
|
||||
imageId: '',
|
||||
imageSmall: false, // 图片是否缩小
|
||||
imageMode: 'aspectFill', // 初始完整展示模式
|
||||
imageScale: 1,
|
||||
@@ -1145,6 +1152,7 @@ Page<IPageData, IPageInstance>({
|
||||
this.setData({ currentIndex: Number(options.index) })
|
||||
}
|
||||
if (options.imageId) {
|
||||
this.setData({ imageId: options.imageId })
|
||||
// wx.showLoading({ title: '加载中...' })
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
apiManager.getImageTextInit(options.imageId)
|
||||
@@ -1396,6 +1404,18 @@ Page<IPageData, IPageInstance>({
|
||||
recorderHandlersBound = true
|
||||
}
|
||||
},
|
||||
|
||||
onSceneSentenceTap() {
|
||||
const imageId = this.data.imageId || ''
|
||||
if (!imageId) {
|
||||
wx.showToast({ title: '缺少图片ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isMoreMenuOpen: false, isMoreMenuClosing: false })
|
||||
wx.navigateTo({
|
||||
url: `/pages/scene_sentence/scene_sentence?image_id=${encodeURIComponent(imageId)}`
|
||||
})
|
||||
},
|
||||
|
||||
noop() {},
|
||||
|
||||
|
||||
@@ -69,239 +69,50 @@
|
||||
</view>
|
||||
<view class="bottom-more-area {{isMoreMenuOpen ? 'open' : (isMoreMenuClosing ? 'close' : '')}}" wx:if="{{isMoreMenuOpen || isMoreMenuClosing}}">
|
||||
<view class="more-items">
|
||||
<view class="more-item">更多功能</view>
|
||||
<view class="more-item" bindtap="onSceneSentenceTap">场景句型</view>
|
||||
<!-- <view class="more-item">功能2</view> -->
|
||||
<!-- <view class="more-item">功能3</view> -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-mask"></view>
|
||||
<!-- 评分结果弹窗 -->
|
||||
<view wx:if="{{isScoreModalOpen && hasScoreInfo}}">
|
||||
<!-- <view class="score-modal-mask" bindtap="onCloseScoreModal"></view> -->
|
||||
<view class="score-modal-content {{scoreModalVisible ? 'show' : ''}}">
|
||||
<view class="score-modal-header">
|
||||
<view class="score-modal-title">
|
||||
<!-- <span>口语评分结果</span> -->
|
||||
</view>
|
||||
<t-icon name="close" class="score-modal-close" size="40rpx" bindtap="onCloseScoreModal" />
|
||||
</view>
|
||||
<view class="score-container" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop">
|
||||
<view class="score-image-container">
|
||||
<t-icon wx:if="{{currentSentence && currentSentence.file_id}}" name="{{playIconName}}" class="score-modal-play" size="60rpx" bindtap="playAssessmentVoice"></t-icon>
|
||||
<!-- <image wx:if="{{currentSentence && currentSentence.file_id}}" src="{{playIconSrc}}" class="score-modal-play" bind:tap="playAssessmentVoice" mode="aspectFit" /> -->
|
||||
<view class="score-text">{{currentSentence.content}}</view>
|
||||
</view>
|
||||
<view class="score-overview">
|
||||
<view class="score-circles">
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{circleProgressStyle}}">
|
||||
<text class="total-score-value">{{displayTotalScore}}</text>
|
||||
<text class="total-score-label">总分</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{accuracyCircleStyle}}">
|
||||
<text class="total-score-value">{{displayAccuracyScore}}</text>
|
||||
<text class="total-score-label">准确性</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{completenessCircleStyle}}">
|
||||
<text class="total-score-value">{{displayCompletenessScore}}</text>
|
||||
<text class="total-score-label">完整性</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="circle-item">
|
||||
<view class="circle-progress" style="{{fluencyCircleStyle}}">
|
||||
<text class="total-score-value">{{displayFluencyScore}}</text>
|
||||
<text class="total-score-label">流利度</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</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>
|
||||
<!-- 单词查询展示区域 -->
|
||||
<view class="word-popup {{showDictExtended ? 'expanded' : ''}}" wx:if="{{showDictPopup}}">
|
||||
<!-- 弹窗头部:单词 + more 按钮 -->
|
||||
<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">
|
||||
{{item}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="close-btn" bindtap="handleDictClose">
|
||||
<t-icon name="close" size="36rpx" />
|
||||
</view>
|
||||
</view>
|
||||
<t-skeleton wx:if="{{dictLoading}}" theme="paragraph" animation="gradient" loading="{{true}}"></t-skeleton>
|
||||
<view wx:if="{{!dictLoading}}" class="popup-header">
|
||||
<view wx:if="{{wordDict.simple.query}}" class="word-title">
|
||||
<t-icon wx:if="{{showBackIcon}}" name="chevron-left" size="40rpx" bindtap="handleBackToPreviousWord"></t-icon>
|
||||
{{wordDict.simple.query}}
|
||||
</view>
|
||||
<view class="word-source" wx:if="{{prototypeWord}}">词源: <t-tag variant="light" theme="primary" data-word="{{prototypeWord}}" bindtap="handleWordClick">{{prototypeWord}}</t-tag></view>
|
||||
<view class="more-btn" wx:if="{{!isWordEmptyResult}}" bindtap="handleDictMore">
|
||||
<text wx:if="{{!showDictExtended}}">More</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> -->
|
||||
</view>
|
||||
</view>
|
||||
<!-- 发音区域 -->
|
||||
<view class="pronounce" wx:if="{{!dictLoading}}">
|
||||
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
|
||||
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].ukphone}}">
|
||||
UK:[{{wordDict.simple.word[0].ukphone}}]
|
||||
</span>
|
||||
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].ukspeech}}" bind:click="playWordAudio" data-type="uk" data-audio="{{wordDict.simple.word[0].ukspeech}}" name="{{(activeWordAudioType === 'uk' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
|
||||
</view>
|
||||
<view class="pron-item" wx:if="{{wordDict.simple && wordDict.simple.word && wordDict.simple.word.length > 0}}">
|
||||
<span class="pron-item-text" wx:if="{{wordDict.simple.word[0].usphone}}">
|
||||
US:[{{wordDict.simple.word[0].usphone}}]
|
||||
</span>
|
||||
<t-icon class="ipa-audio" wx:if="{{wordDict.simple.word[0].usspeech}}" bind:click="playWordAudio" data-type="us" data-audio="{{wordDict.simple.word[0].usspeech}}" name="{{(activeWordAudioType === 'us' && wordAudioPlaying) ? wordAudioIconName : 'sound'}}" size="30rpx" />
|
||||
</view>
|
||||
</view>
|
||||
<!-- 基础词性释义 -->
|
||||
<view wx:if="{{!dictLoading}}" class="word-meanings">
|
||||
<block wx:if="{{wordDict.ec && wordDict.ec.word && wordDict.ec.word.length > 0}}">
|
||||
<block wx:for="{{wordDict.ec.word[0].trs}}" wx:key="index">
|
||||
<block wx:for="{{item.tr}}" wx:key="index">
|
||||
<block wx:for="{{item.l.i}}" wx:key="index">
|
||||
<text>{{item}}</text>
|
||||
</block>
|
||||
</block>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
<!-- 扩展内容(默认隐藏,点击 more 后显示) -->
|
||||
<t-tabs class="t-tabs" animation="{{ { duration: 0.1 } }}" defaultValue="{{dictDefaultTabValue}}" bind:change="onTabsChange" bind:click="onTabsClick" wx:if="{{showDictExtended}}">
|
||||
<t-tab-panel label="使用" value="0" wx:if="{{wordDict.collins && wordDict.collins.collins_entries && wordDict.collins.collins_entries.length > 0 && wordDict.collins.collins_entries[0].entries && wordDict.collins.collins_entries[0].entries.entry}}">
|
||||
<view class="dictionary">
|
||||
<block wx:for="{{wordDict.collins.collins_entries[0].entries.entry}}" wx:key="index">
|
||||
<block wx:if="{{item.tran_entry && item.tran_entry.length > 0}}">
|
||||
<view class="dictionary-content" wx:for="{{item.tran_entry}}" wx:key="tindex">
|
||||
<view class="dictionary-pos" wx:if="{{item.pos_entry && item.pos_entry.pos && item.pos_entry.pos_tips}}">
|
||||
<t-tag variant="light" theme="primary">
|
||||
{{item.pos_entry.pos}} {{item.pos_entry.pos_tips}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="dictionary-tran" wx:if="{{item.tranParts && item.tranParts.length > 0}}">
|
||||
<block wx:for="{{item.tranParts}}" wx:key="pindex">
|
||||
<text wx:if="{{!item.bold}}" class="normal-text">{{item.text}}</text>
|
||||
<text wx:if="{{item.bold}}" class="dictionary-tran-bold">{{item.text}}</text>
|
||||
</block>
|
||||
</view>
|
||||
<view class="dictionary-tran" wx:elif="{{item.tran}}">{{item.tran}}</view>
|
||||
<view class="dictionary-list" wx:if="{{item.exam_sents && item.exam_sents.sent && item.exam_sents.sent.length > 0}}">
|
||||
<block wx:for="{{item.exam_sents.sent}}" wx:key="index">
|
||||
<view class="word-sent-en" wx:if="{{item.eng_sent}}">{{item.eng_sent}}</view>
|
||||
<view class="word-sent-zh" wx:if="{{item.chn_sent}}">{{item.chn_sent}}</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.phrs && wordDict.phrs.phrs && wordDict.phrs.phrs.length > 0}}" label="短语" value="1">
|
||||
<block wx:for="{{wordDict.phrs.phrs}}" wx:key="index">
|
||||
<t-cell title="{{item.phr.headword.l.i}}" description="{{item.phr.trs[0].tr.l.i}}" />
|
||||
</block>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.individual && wordDict.individual.pastExamSents && wordDict.individual.pastExamSents.length > 0}}" label="真题" value="2">
|
||||
<view wx:for="{{wordDict.individual.pastExamSents}}" wx:key="index" class="exam-sent">
|
||||
<view class="exam-sent-en">{{item.en}}</view>
|
||||
<view class="exam-sent-zh">{{item.zh}}</view>
|
||||
<view class="exam-sent-source">--摘自《{{item.source}}》</view>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
<t-tab-panel wx:if="{{wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0}}" label="相关" value="3">
|
||||
<view wx:if="{{(wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0) || (wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0) || (wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0)}}">
|
||||
<view class="syno" wx:if="{{wordDict.syno && wordDict.syno.synos && wordDict.syno.synos.length > 0}}">
|
||||
<view class="syno-title">同义词</view>
|
||||
<view class="syno-list" wx:for="{{wordDict.syno.synos}}" wx:key="index">
|
||||
<view class="syno-pos">
|
||||
<t-tag wx:if="{{item.syno.pos}}" variant="light" theme="primary">
|
||||
{{item.syno.pos}}
|
||||
</t-tag>
|
||||
<view class="syno-tran">{{item.syno.tran}}</view>
|
||||
</view>
|
||||
<t-tag class="syno-item" wx:for="{{item.syno.ws}}" wx:key="w" variant="light">
|
||||
{{item.w}}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rel-word" wx:if="{{wordDict.relWord && wordDict.relWord.rels && wordDict.relWord.rels.length > 0}}">
|
||||
<view class="rel-word-title">相关单词</view>
|
||||
<view class="rel-word-list" wx:for="{{wordDict.relWord.rels}}" wx:key="index">
|
||||
<view class="rel-word-pos">
|
||||
<t-tag wx:if="{{item.rel.pos}}" variant="light" theme="primary">
|
||||
{{item.rel.pos}}
|
||||
</t-tag>
|
||||
</view>
|
||||
<view class="rel-word-item" wx:for="{{item.rel.words}}" wx:key="word">
|
||||
<view class="rel-word-tran">{{item.tran}}</view>
|
||||
<view class="rel-word-word">{{item.word}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="discriminate" wx:if="{{wordDict.discriminate && wordDict.discriminate.data && wordDict.discriminate.data.length > 0}}">
|
||||
<view class="discriminate-title">区分单词</view>
|
||||
<view class="discriminate-list" wx:for="{{wordDict.discriminate.data}}" wx:key="index">
|
||||
<view class="discriminate-item" wx:for="{{item.usages}}" wx:key="headword">
|
||||
<view class="discriminate-tran">{{item.usage}}</view>
|
||||
<view class="discriminate-headword">{{item.headword}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:else class="no-related-words">
|
||||
<text class="no-related-words-text">暂无相关单词信息</text>
|
||||
</view>
|
||||
</t-tab-panel>
|
||||
</t-tabs>
|
||||
<view class="no-word" wx:if="{{isWordEmptyResult}}">没有更多内容</view>
|
||||
</view>
|
||||
<score-modal
|
||||
wx:if="{{isScoreModalOpen && hasScoreInfo}}"
|
||||
visible="{{scoreModalVisible}}"
|
||||
hasScoreInfo="{{hasScoreInfo}}"
|
||||
sentence="{{currentSentence}}"
|
||||
totalScore="{{displayTotalScore}}"
|
||||
accuracyScore="{{displayAccuracyScore}}"
|
||||
completenessScore="{{displayCompletenessScore}}"
|
||||
fluencyScore="{{displayFluencyScore}}"
|
||||
circleProgressStyle="{{circleProgressStyle}}"
|
||||
accuracyCircleStyle="{{accuracyCircleStyle}}"
|
||||
completenessCircleStyle="{{completenessCircleStyle}}"
|
||||
fluencyCircleStyle="{{fluencyCircleStyle}}"
|
||||
wordScores="{{wordScores}}"
|
||||
matchTagLegend="{{matchTagLegend}}"
|
||||
playIconName="{{playIconName}}"
|
||||
bind:close="onCloseScoreModal"
|
||||
bind:play="playAssessmentVoice"
|
||||
/>
|
||||
<word-dictionary
|
||||
visible="{{showDictPopup}}"
|
||||
expanded="{{showDictExtended}}"
|
||||
loading="{{dictLoading}}"
|
||||
wordDict="{{wordDict}}"
|
||||
showBackIcon="{{showBackIcon}}"
|
||||
prototypeWord="{{prototypeWord}}"
|
||||
isWordEmptyResult="{{isWordEmptyResult}}"
|
||||
dictDefaultTabValue="{{dictDefaultTabValue}}"
|
||||
activeWordAudioType="{{activeWordAudioType}}"
|
||||
wordAudioPlaying="{{wordAudioPlaying}}"
|
||||
wordAudioIconName="{{wordAudioIconName}}"
|
||||
bind:close="handleDictClose"
|
||||
bind:more="handleDictMore"
|
||||
bind:tabsChange="onTabsChange"
|
||||
bind:tabsClick="onTabsClick"
|
||||
bind:play="playWordAudio"
|
||||
bind:back="handleBackToPreviousWord"
|
||||
bind:wordTap="handleWordClick"
|
||||
/>
|
||||
<view class="word-popup-mask" wx:if="{{showDictPopup}}" bindtap="handleDictClose"></view>
|
||||
</view>
|
||||
|
||||
@@ -133,7 +133,7 @@ Page({
|
||||
const basicData = {
|
||||
isLoggedIn: app.globalData.isLoggedIn || false,
|
||||
userInfo: app.globalData.userInfo || null,
|
||||
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||||
}
|
||||
|
||||
// 设置基础统计数据
|
||||
@@ -218,7 +218,7 @@ Page({
|
||||
const userData = {
|
||||
isLoggedIn: app.globalData.isLoggedIn || false,
|
||||
userInfo: app.globalData.userInfo || null,
|
||||
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
dictLevel: app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||||
}
|
||||
|
||||
logger.info('更新用户信息:', userData)
|
||||
@@ -265,7 +265,7 @@ Page({
|
||||
logger.info('开始加载词典等级配置')
|
||||
const startTime = Date.now()
|
||||
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||||
|
||||
const endTime = Date.now()
|
||||
logger.info('词典等级配置加载完成,耗时:', endTime - startTime, 'ms', dictLevel)
|
||||
@@ -274,7 +274,7 @@ Page({
|
||||
} catch (error) {
|
||||
logger.error('加载词典等级配置失败:', error)
|
||||
// 使用默认设置
|
||||
this.setData({ dictLevel: 'LEVEL1' })
|
||||
this.setData({ dictLevel: 'level1' })
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
15
miniprogram/pages/scene_sentence/scene_sentence.json
Normal file
15
miniprogram/pages/scene_sentence/scene_sentence.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"navigationBarTitleText": "场景句子",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"backgroundTextStyle": "light",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"score-modal": "/components/score-modal/score-modal",
|
||||
"word-dictionary": "/components/word-dictionary/word-dictionary"
|
||||
}
|
||||
}
|
||||
840
miniprogram/pages/scene_sentence/scene_sentence.ts
Normal file
840
miniprogram/pages/scene_sentence/scene_sentence.ts
Normal file
@@ -0,0 +1,840 @@
|
||||
import apiManager from '../../utils/api'
|
||||
import logger from '../../utils/logger'
|
||||
const recorderManager = wx.getRecorderManager()
|
||||
let recorderHandlersBound = false
|
||||
|
||||
interface IData {
|
||||
loadingMaskVisible: boolean
|
||||
imageId: string
|
||||
taskId?: string
|
||||
statusText: string
|
||||
scene?: {
|
||||
id: string
|
||||
image_id: string
|
||||
list: Array<{
|
||||
collocations: string[]
|
||||
coreVocab: string[]
|
||||
coreVocabDesc?: string[]
|
||||
functionTags: string[]
|
||||
pronunciationTip: string
|
||||
pronunciationUrl?: string | null
|
||||
imageTextId?: string
|
||||
sceneExplanation: string
|
||||
sentenceEn: string
|
||||
sentenceZh: string
|
||||
avoidScenarios?: string
|
||||
}>
|
||||
}
|
||||
currentIndex: number
|
||||
transDisplayMode: 'en' | 'en_zh'
|
||||
contentVisible: boolean
|
||||
isPlaying: boolean
|
||||
isRecording: boolean
|
||||
hasScoreInfo: boolean
|
||||
isScoreModalOpen: boolean
|
||||
scoreModalVisible: boolean
|
||||
currentSentence?: {
|
||||
content: string
|
||||
file_id?: string
|
||||
id?: string
|
||||
}
|
||||
totalScore: number
|
||||
accuracyScore: number
|
||||
completenessScore: number
|
||||
fluencyScore: number
|
||||
circleProgressStyle: string
|
||||
accuracyCircleStyle: string
|
||||
completenessCircleStyle: string
|
||||
fluencyCircleStyle: string
|
||||
wordScores: Array<any>
|
||||
englishWords?: Array<string>
|
||||
showDictPopup?: boolean
|
||||
showDictExtended?: boolean
|
||||
dictLoading?: boolean
|
||||
wordDict?: any
|
||||
isWordEmptyResult?: boolean
|
||||
dictDefaultTabValue?: string
|
||||
standardAudioMap?: Record<string, string>
|
||||
standardAudioLocalMap?: Record<string, string>
|
||||
wordAudioPlaying?: boolean
|
||||
wordAudioIconName?: string
|
||||
activeWordAudioType?: string
|
||||
buttonsVisible?: boolean
|
||||
commonMistakes?: string[]
|
||||
pragmaticAlternative?: string[]
|
||||
responsePairs?: string[]
|
||||
recordStartTime?: number
|
||||
recordDuration?: number
|
||||
remainingTime?: number
|
||||
overlayVisible?: boolean
|
||||
highlightWords?: Array<any>
|
||||
highlightShow?: boolean
|
||||
highlightZoom?: boolean
|
||||
analizing?: boolean
|
||||
cachedHighlightWords?: Array<any>
|
||||
cachedSentenceIndex?: number
|
||||
}
|
||||
|
||||
interface IPageInstance {
|
||||
pollTimer?: number
|
||||
audioCtx?: any
|
||||
wordAudioContext?: WechatMiniprogram.InnerAudioContext
|
||||
wordAudioIconTimer?: number
|
||||
recordTimer?: number
|
||||
fetchSceneSentence: (imageId: string) => Promise<void>
|
||||
startPolling: (taskId: string, imageId: string) => void
|
||||
onTransTap: () => void
|
||||
onPrevTap: () => void
|
||||
onNextTap: () => void
|
||||
switchSentence: (delta: number) => void
|
||||
playStandardVoice: () => void
|
||||
handleRecordStart: () => void
|
||||
handleRecordEnd: () => void
|
||||
onScoreTap: () => void
|
||||
onCloseScoreModal: () => void
|
||||
onWordTap: (e: any) => void
|
||||
handleDictClose: () => void
|
||||
handleDictMore: () => void
|
||||
processCollinsData: (collinsData: any) => any
|
||||
getStandardVoice: (sentenceId: string) => Promise<void>
|
||||
noop: () => void
|
||||
startRecording: () => void
|
||||
stopRecording: () => void
|
||||
onMicHighlight: () => void
|
||||
computeHighlightLayout: () => void
|
||||
fetchRecordResultForSentence: (textId: string) => Promise<void>
|
||||
}
|
||||
|
||||
Page<IData, IPageInstance>({
|
||||
data: {
|
||||
loadingMaskVisible: false,
|
||||
imageId: '',
|
||||
statusText: '加载中...',
|
||||
scene: undefined,
|
||||
currentIndex: 0,
|
||||
transDisplayMode: 'en_zh',
|
||||
contentVisible: false,
|
||||
isPlaying: false,
|
||||
isRecording: false,
|
||||
hasScoreInfo: false,
|
||||
isScoreModalOpen: false,
|
||||
scoreModalVisible: false,
|
||||
currentSentence: undefined,
|
||||
totalScore: 0,
|
||||
accuracyScore: 0,
|
||||
completenessScore: 0,
|
||||
fluencyScore: 0,
|
||||
circleProgressStyle: '',
|
||||
accuracyCircleStyle: '',
|
||||
completenessCircleStyle: '',
|
||||
fluencyCircleStyle: '',
|
||||
wordScores: [],
|
||||
englishWords: [],
|
||||
showDictPopup: false,
|
||||
showDictExtended: false,
|
||||
dictLoading: false,
|
||||
wordDict: {},
|
||||
isWordEmptyResult: false,
|
||||
dictDefaultTabValue: '0',
|
||||
standardAudioMap: {},
|
||||
standardAudioLocalMap: {},
|
||||
wordAudioPlaying: false,
|
||||
wordAudioIconName: 'sound',
|
||||
activeWordAudioType: '',
|
||||
buttonsVisible: false,
|
||||
commonMistakes: [],
|
||||
pragmaticAlternative: [],
|
||||
responsePairs: [],
|
||||
recordStartTime: 0,
|
||||
recordDuration: 0,
|
||||
remainingTime: 30,
|
||||
overlayVisible: false,
|
||||
highlightWords: ["this", "dish"],
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
analizing: false,
|
||||
cachedHighlightWords: [],
|
||||
cachedSentenceIndex: -1
|
||||
},
|
||||
noop() {},
|
||||
|
||||
async onLoad(options: Record<string, string>) {
|
||||
const imageId = options.image_id || options.imageId || ''
|
||||
this.setData({ imageId })
|
||||
if (!imageId) {
|
||||
wx.showToast({ title: '缺少 image_id', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true, statusText: '加载中...', currentIndex: 0, transDisplayMode: 'en_zh', contentVisible: false, isPlaying: false, isRecording: false, hasScoreInfo: false, isScoreModalOpen: false, scoreModalVisible: false })
|
||||
if (!this.audioCtx) {
|
||||
this.audioCtx = wx.createInnerAudioContext()
|
||||
this.audioCtx.onEnded(() => { this.setData({ isPlaying: false }) })
|
||||
this.audioCtx.onStop(() => { this.setData({ isPlaying: false }) })
|
||||
this.audioCtx.onError(() => { this.setData({ isPlaying: false }) })
|
||||
}
|
||||
if (!recorderHandlersBound) {
|
||||
recorderManager.onStop((res) => {
|
||||
const ms = Date.now() - (this.data.recordStartTime || 0)
|
||||
if (ms >= 3000) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '录音完成,是否确认提交?',
|
||||
success: (r) => {
|
||||
if (r.confirm) {
|
||||
this.setData({ analizing: true })
|
||||
apiManager.uploadFile(res.tempFilePath).then((fileId) => {
|
||||
const cur = this.data.scene?.list?.[this.data.currentIndex]
|
||||
const imageTextId = cur && cur.imageTextId ? String(cur.imageTextId) : ''
|
||||
if (!imageTextId) {
|
||||
this.setData({ analizing: false })
|
||||
wx.showToast({ title: '缺少句子ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
apiManager.getAssessmentResult(fileId, imageTextId).then((result) => {
|
||||
const assessmentResult = result.assessment_result?.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((s: number) => s < 0)
|
||||
const wordScores = (assessmentResult.Words || []).map((w: any) => ({
|
||||
word: w.Word,
|
||||
pronAccuracy: Number((w.PronAccuracy || 0).toFixed(2)),
|
||||
pronFluency: Number((w.PronFluency || 0).toFixed(2)),
|
||||
matchTag: w.MatchTag || 0,
|
||||
phoneInfos: (w.PhoneInfos || []).map((p: any) => ({
|
||||
phone: p.Phone,
|
||||
pronAccuracy: Number((p.PronAccuracy || 0).toFixed(2)),
|
||||
matchTag: p.MatchTag || 0
|
||||
}))
|
||||
}))
|
||||
this.setData({
|
||||
hasScoreInfo: !allNegative && !!assessmentResult,
|
||||
totalScore: suggestedScore >= 0 ? Number(suggestedScore.toFixed(2)) : 0,
|
||||
accuracyScore: pronAccuracy >= 0 ? Number(pronAccuracy.toFixed(2)) : 0,
|
||||
completenessScore: pronCompletion >= 0 ? Number(pronCompletion.toFixed(2)) : 0,
|
||||
fluencyScore: pronFluency >= 0 ? Number(pronFluency.toFixed(2)) : 0,
|
||||
wordScores,
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
this.onScoreTap()
|
||||
}).catch(() => {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
wx.showToast({ title: '评估失败', icon: 'none' })
|
||||
}).finally(() => {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
})
|
||||
}).catch(() => {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
wx.showToast({ title: '上传失败', icon: 'none' })
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
analizing: false
|
||||
})
|
||||
wx.showToast({ title: '说话时间太短', icon: 'none' })
|
||||
}
|
||||
})
|
||||
recorderManager.onError(() => {
|
||||
wx.showToast({ title: '录音失败', icon: 'none' })
|
||||
// this.setData({ overlayVisible: false, isRecording: false })
|
||||
const timer = setTimeout(() => {
|
||||
// this.setData({ highlightShow: false, highlightZoom: false, highlightWords: [], analizing: false })
|
||||
clearTimeout(timer)
|
||||
}, 320)
|
||||
})
|
||||
recorderHandlersBound = true
|
||||
}
|
||||
await this.fetchSceneSentence(imageId)
|
||||
},
|
||||
|
||||
async fetchSceneSentence(imageId: string) {
|
||||
try {
|
||||
const resp = await apiManager.getSceneSentence(imageId)
|
||||
if (resp && Object.keys(resp || {}).length > 0) {
|
||||
logger.info('场景句子数据:', resp)
|
||||
const scene = resp.data || resp
|
||||
const idx = 0
|
||||
const cur = scene && scene.list && scene.list[idx] ? scene.list[idx] : undefined
|
||||
const currentSentence = cur ? { content: cur.sentenceEn, file_id: cur.pronunciationUrl || undefined, id: cur.imageTextId } : undefined
|
||||
const englishWords = cur && cur.sentenceEn ? cur.sentenceEn.split(' ') : []
|
||||
const commonMistakes = (cur as any)?.commonMistakes || [
|
||||
'1. 漏冠词a(×bowl mint leaves)',
|
||||
'2. refreshing误读为/ˈrefrʌʃɪŋ/'
|
||||
]
|
||||
const pragmaticAlternative = (cur as any)?.pragmaticAlternative || [
|
||||
'This bowl of mint leaves appears very refreshing.(更书面)',
|
||||
'These mint leaves look crisp and fresh.(强调口感)'
|
||||
]
|
||||
const responsePairs = (cur as any)?.responsePairs || []
|
||||
this.setData({
|
||||
scene,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '已获取数据',
|
||||
contentVisible: true,
|
||||
currentIndex: idx,
|
||||
currentSentence,
|
||||
englishWords,
|
||||
buttonsVisible: true,
|
||||
commonMistakes,
|
||||
pragmaticAlternative,
|
||||
responsePairs,
|
||||
hasScoreInfo: false
|
||||
})
|
||||
const sid = cur && cur.imageTextId
|
||||
if (sid) {
|
||||
try { this.fetchRecordResultForSentence(String(sid)) } catch (e) {}
|
||||
}
|
||||
} else {
|
||||
const { task_id } = await apiManager.createScene(imageId, 'scene_sentence')
|
||||
this.setData({ taskId: task_id, statusText: '解析中...' })
|
||||
this.startPolling(task_id, imageId)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('获取场景句子失败:', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false })
|
||||
}
|
||||
},
|
||||
|
||||
startPolling(taskId: string, imageId: string) {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = undefined
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
this.pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiManager.getSceneTask(taskId)
|
||||
if (res.status === 'completed') {
|
||||
clearInterval(this.pollTimer!)
|
||||
this.pollTimer = undefined
|
||||
await this.fetchSceneSentence(imageId)
|
||||
} else if (res.status === 'failed') {
|
||||
clearInterval(this.pollTimer!)
|
||||
this.pollTimer = undefined
|
||||
wx.showToast({ title: '任务失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '任务失败' })
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('轮询任务状态失败:', err)
|
||||
}
|
||||
}, 3000) as any
|
||||
},
|
||||
|
||||
onTransTap() {
|
||||
const mode = this.data.transDisplayMode === 'en' ? 'en_zh' : 'en'
|
||||
this.setData({ transDisplayMode: mode })
|
||||
},
|
||||
|
||||
onPrevTap() {
|
||||
this.switchSentence(-1)
|
||||
},
|
||||
|
||||
onNextTap() {
|
||||
this.switchSentence(1)
|
||||
},
|
||||
|
||||
switchSentence(delta: number) {
|
||||
const list = this.data.scene?.list || []
|
||||
const count = list.length
|
||||
if (count === 0) return
|
||||
const nextIndex = this.data.currentIndex + delta
|
||||
if (nextIndex < 0 || nextIndex >= count) return
|
||||
if (this.data.isPlaying) {
|
||||
this.setData({ isPlaying: false })
|
||||
}
|
||||
this.setData({ contentVisible: false })
|
||||
setTimeout(() => {
|
||||
const list = this.data.scene?.list || []
|
||||
const cur = list && list[nextIndex] ? list[nextIndex] : undefined
|
||||
const currentSentence = cur ? { content: cur.sentenceEn, file_id: cur.pronunciationUrl || undefined, id: cur.imageTextId } : undefined
|
||||
const englishWords = cur && cur.sentenceEn ? cur.sentenceEn.split(' ') : []
|
||||
const commonMistakes = (cur as any)?.commonMistakes || [
|
||||
'1. 漏冠词a(×bowl mint leaves)',
|
||||
'2. refreshing误读为/ˈrefrʌʃɪŋ/'
|
||||
]
|
||||
const pragmaticAlternative = (cur as any)?.pragmaticAlternative || []
|
||||
const responsePairs = (cur as any)?.responsePairs || []
|
||||
this.setData({
|
||||
currentIndex: nextIndex,
|
||||
currentSentence,
|
||||
englishWords,
|
||||
commonMistakes,
|
||||
pragmaticAlternative,
|
||||
responsePairs,
|
||||
contentVisible: true,
|
||||
hasScoreInfo: false
|
||||
})
|
||||
const sid = cur && cur.imageTextId
|
||||
if (sid) {
|
||||
wx.nextTick(() => { this.getStandardVoice(String(sid)) })
|
||||
wx.nextTick(() => { this.fetchRecordResultForSentence(String(sid)) })
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
|
||||
async fetchRecordResultForSentence(textId: string) {
|
||||
try {
|
||||
const rec: any = await apiManager.getRecordResult(textId)
|
||||
const hasDetails = !!(rec && rec.details && Object.keys(rec.details || {}).length > 0)
|
||||
if (!hasDetails) {
|
||||
this.setData({ hasScoreInfo: false })
|
||||
return
|
||||
}
|
||||
const assessment = rec.details?.assessment || {}
|
||||
const result = assessment?.result || {}
|
||||
const suggestedScore = result?.SuggestedScore ?? 0
|
||||
const pronAccuracy = result?.PronAccuracy ?? 0
|
||||
const pronCompletion = result?.PronCompletion ?? 0
|
||||
const pronFluency = result?.PronFluency ?? 0
|
||||
const wordScores = (result?.Words || []).map((w: any) => ({
|
||||
word: w.Word,
|
||||
pronAccuracy: Number((w.PronAccuracy || 0).toFixed(2)),
|
||||
pronFluency: Number((w.PronFluency || 0).toFixed(2)),
|
||||
matchTag: w.MatchTag || 0,
|
||||
phoneInfos: (w.PhoneInfos || []).map((p: any) => ({
|
||||
phone: p.Phone,
|
||||
pronAccuracy: Number((p.PronAccuracy || 0).toFixed(2)),
|
||||
matchTag: p.MatchTag || 0
|
||||
}))
|
||||
}))
|
||||
const curSentence = this.data.currentSentence || { content: '' }
|
||||
this.setData({
|
||||
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,
|
||||
currentSentence: { ...curSentence, file_id: rec?.file_id || curSentence.file_id }
|
||||
})
|
||||
} catch (e) {
|
||||
this.setData({ hasScoreInfo: false })
|
||||
}
|
||||
},
|
||||
|
||||
playStandardVoice() {
|
||||
const sid = this.data.currentSentence?.id
|
||||
if (!sid) { wx.showToast({ title: '缺少 imageTextId', icon: 'none' }); return }
|
||||
const audioUrl = (this.data.standardAudioMap || {})[sid]
|
||||
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 (!audioUrl) {
|
||||
wx.showLoading({ title: '正在获取...' })
|
||||
this.getStandardVoice(sid)
|
||||
.then(() => {
|
||||
wx.hideLoading()
|
||||
this.playStandardVoice()
|
||||
})
|
||||
.catch(() => {
|
||||
wx.hideLoading()
|
||||
})
|
||||
return
|
||||
}
|
||||
const cachedLocal = (this.data.standardAudioLocalMap || {})[sid]
|
||||
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.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(audioUrl).then((filePath) => {
|
||||
const map = this.data.standardAudioLocalMap || {}
|
||||
this.setData({ standardAudioLocalMap: { ...map, [sid]: filePath } })
|
||||
playWithPath(filePath)
|
||||
}).catch((error) => {
|
||||
logger.error('下载音频失败:', error)
|
||||
wx.showToast({ title: '音频下载失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleRecordStart() {
|
||||
if (this.data.isRecording) return
|
||||
this.startRecording()
|
||||
try { this.onMicHighlight() } catch (e) {}
|
||||
},
|
||||
|
||||
handleRecordEnd() {
|
||||
this.stopRecording()
|
||||
},
|
||||
|
||||
onScoreTap() {
|
||||
if (!this.data.hasScoreInfo) return
|
||||
this.setData({ isScoreModalOpen: true })
|
||||
setTimeout(() => this.setData({ scoreModalVisible: true }), 50)
|
||||
},
|
||||
|
||||
onCloseScoreModal() {
|
||||
this.setData({ isScoreModalOpen: false, scoreModalVisible: false })
|
||||
},
|
||||
|
||||
async onWordTap(e: any) {
|
||||
const rawWord = (e && e.currentTarget && e.currentTarget.dataset && e.currentTarget.dataset.word)
|
||||
|| (e && e.target && e.target.dataset && e.target.dataset.word)
|
||||
|| (e && e.detail && e.detail.word)
|
||||
|| ''
|
||||
console.log('rawWord:', rawWord)
|
||||
if (!rawWord) return
|
||||
const cleanedWord = String(rawWord).replace(/[.,?!*;:'"()]/g, '').trim()
|
||||
if (!cleanedWord) return
|
||||
try {
|
||||
this.setData({ dictLoading: true, showDictPopup: true })
|
||||
const wordDetail: any = await apiManager.getWordDetail(cleanedWord)
|
||||
const processedCollins = wordDetail['collins'] ? this.processCollinsData(wordDetail['collins']) : wordDetail['collins']
|
||||
const rawRelWord = wordDetail['rel_word']
|
||||
let sanitizedRelWord = rawRelWord
|
||||
if (rawRelWord && rawRelWord.rels && Array.isArray(rawRelWord.rels)) {
|
||||
sanitizedRelWord = {
|
||||
...rawRelWord,
|
||||
rels: rawRelWord.rels.map((entry: any) => {
|
||||
const rel = entry.rel || {}
|
||||
const pos = rel.pos
|
||||
const cleanPos = typeof pos === 'string' ? pos.replace(/\[|\]/g, '') : pos
|
||||
return { ...entry, rel: { ...rel, pos: cleanPos } }
|
||||
})
|
||||
}
|
||||
}
|
||||
const hasEE = !!wordDetail['ee']
|
||||
const hasEC = !!(wordDetail['ec'] && wordDetail['ec'].word && wordDetail['ec'].word.length > 0)
|
||||
const hasCollins = !!(processedCollins && processedCollins.collins_entries && processedCollins.collins_entries.length > 0)
|
||||
const isWordEmptyResult = !(hasEE || hasEC || hasCollins)
|
||||
const hasPhrs = !!(wordDetail['phrs'] && wordDetail['phrs'].phrs && wordDetail['phrs'].phrs.length > 0)
|
||||
const hasPastExam = !!(wordDetail['individual'] && wordDetail['individual'].pastExamSents && wordDetail['individual'].pastExamSents.length > 0)
|
||||
const dictDefaultTabValue = hasCollins ? '0' : (hasPhrs ? '1' : (hasPastExam ? '2' : '3'))
|
||||
this.setData({
|
||||
showDictPopup: true,
|
||||
wordDict: {
|
||||
ee: wordDetail['ee'], ec: wordDetail['ec'], expandEc: wordDetail['expand_ec'],
|
||||
simple: wordDetail['simple'], phrs: wordDetail['phrs'], etym: wordDetail['etym'],
|
||||
individual: wordDetail['individual'], collins: processedCollins,
|
||||
relWord: sanitizedRelWord, syno: wordDetail['syno'],
|
||||
discriminate: wordDetail['discriminate']
|
||||
},
|
||||
isWordEmptyResult,
|
||||
dictDefaultTabValue
|
||||
})
|
||||
this.setData({ dictLoading: false })
|
||||
} catch (err) {
|
||||
logger.error('获取单词详情失败:', err)
|
||||
wx.showToast({ title: '获取单词详情失败', icon: 'none' })
|
||||
this.setData({ dictLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
handleDictClose() {
|
||||
this.setData({ showDictPopup: false, showDictExtended: false })
|
||||
try { this.wordAudioContext && this.wordAudioContext.stop() } catch (e) {}
|
||||
try {
|
||||
if (this.wordAudioContext) {
|
||||
this.wordAudioContext.offPlay()
|
||||
this.wordAudioContext.offEnded()
|
||||
this.wordAudioContext.offError()
|
||||
this.wordAudioContext.destroy()
|
||||
this.wordAudioContext = undefined
|
||||
}
|
||||
} catch (e) {}
|
||||
if (this.wordAudioIconTimer) {
|
||||
clearInterval(this.wordAudioIconTimer)
|
||||
this.wordAudioIconTimer = undefined
|
||||
}
|
||||
this.setData({ wordAudioPlaying: false, wordAudioIconName: 'sound', activeWordAudioType: '' })
|
||||
},
|
||||
|
||||
handleDictMore() {
|
||||
this.setData({ showDictExtended: !this.data.showDictExtended })
|
||||
},
|
||||
|
||||
processCollinsData(collinsData: any) {
|
||||
if (!collinsData || !collinsData.collins_entries) return collinsData
|
||||
const processedData = JSON.parse(JSON.stringify(collinsData))
|
||||
processedData.collins_entries.forEach((entry: any) => {
|
||||
if (entry.entries && entry.entries.entry) {
|
||||
entry.entries.entry.forEach((entryItem: any) => {
|
||||
if (entryItem.tran_entry) {
|
||||
entryItem.tran_entry.forEach((tranEntry: any) => {
|
||||
if (tranEntry.tran) {
|
||||
const parts = tranEntry.tran.split(/(<b>.*?<\/b>)/g).filter(Boolean)
|
||||
const processedParts: Array<{ text: string; bold: boolean }> = []
|
||||
parts.forEach((part: string) => {
|
||||
if (part.startsWith('<b>') && part.endsWith('</b>')) {
|
||||
const text = part.substring(3, part.length - 4)
|
||||
processedParts.push({ text, bold: true })
|
||||
} else {
|
||||
processedParts.push({ text: part, bold: false })
|
||||
}
|
||||
})
|
||||
tranEntry.tranParts = processedParts
|
||||
tranEntry.originalTran = tranEntry.tran
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return processedData
|
||||
},
|
||||
startRecording() {
|
||||
const options: WechatMiniprogram.RecorderManagerStartOption = {
|
||||
duration: 30000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 48000,
|
||||
format: 'mp3' as 'mp3'
|
||||
}
|
||||
this.recordTimer && clearInterval(this.recordTimer)
|
||||
this.setData({ recordDuration: 0, remainingTime: 30 })
|
||||
recorderManager.start(options)
|
||||
this.setData({ isRecording: true, recordStartTime: Date.now() })
|
||||
this.recordTimer = setInterval(() => {
|
||||
const duration = Date.now() - (this.data.recordStartTime || 0)
|
||||
const remaining = Math.max(0, 30 - Math.floor(duration / 1000))
|
||||
this.setData({ recordDuration: duration, remainingTime: remaining })
|
||||
if (remaining === 0) {
|
||||
this.stopRecording()
|
||||
}
|
||||
}, 100) as any
|
||||
},
|
||||
stopRecording() {
|
||||
if (!this.data.isRecording) return
|
||||
const duration = Date.now() - (this.data.recordStartTime || 0)
|
||||
if (this.recordTimer) { clearInterval(this.recordTimer) }
|
||||
if (duration < 3000) {
|
||||
try { recorderManager.stop() } catch (e) {}
|
||||
this.setData({
|
||||
isRecording: false,
|
||||
remainingTime: 30,
|
||||
overlayVisible: false,
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
highlightWords: [],
|
||||
cachedHighlightWords: [],
|
||||
cachedSentenceIndex: -1
|
||||
})
|
||||
return
|
||||
}
|
||||
try { recorderManager.stop() } catch (e) {}
|
||||
},
|
||||
onMicHighlight() {
|
||||
this.computeHighlightLayout()
|
||||
const startAnim = () => {
|
||||
const words = this.data.cachedHighlightWords || []
|
||||
if (!words || words.length === 0) return
|
||||
this.setData({
|
||||
overlayVisible: true,
|
||||
highlightWords: words.map((w: any) => ({ ...w, transform: 'translate(0px, 0px) scale(1)' })),
|
||||
highlightShow: false,
|
||||
highlightZoom: false
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.setData({ highlightShow: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ highlightZoom: true })
|
||||
try {
|
||||
wx.nextTick(() => {
|
||||
const updated = (this.data.highlightWords || []).map((w: any) => ({ ...w, transform: w.targetTransform }))
|
||||
this.setData({ highlightWords: updated })
|
||||
})
|
||||
} catch (e) {
|
||||
setTimeout(() => {
|
||||
const updated = (this.data.highlightWords || []).map((w: any) => ({ ...w, transform: w.targetTransform }))
|
||||
this.setData({ highlightWords: updated })
|
||||
}, 0)
|
||||
}
|
||||
}, 500)
|
||||
}, 50)
|
||||
}
|
||||
try {
|
||||
wx.nextTick(() => {
|
||||
setTimeout(() => startAnim(), 80)
|
||||
})
|
||||
} catch (e) {
|
||||
setTimeout(() => {
|
||||
setTimeout(() => startAnim(), 80)
|
||||
}, 0)
|
||||
}
|
||||
},
|
||||
computeHighlightLayout() {
|
||||
const list = this.data.scene?.list || []
|
||||
const cur = list[this.data.currentIndex]
|
||||
const words = (cur && cur.sentenceEn ? cur.sentenceEn.split(' ') : [])
|
||||
if (!words || words.length === 0) return
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const windowWidth = sys.windowWidth || 375
|
||||
const windowHeight = sys.windowHeight || 667
|
||||
const bottomOffset = 120
|
||||
const scale = 1.6
|
||||
const centerX = windowWidth / 2
|
||||
const centerY = (windowHeight - bottomOffset) / 2
|
||||
const query = wx.createSelectorQuery().in(this as any)
|
||||
query.selectAll('.sentence-en .sentence-word').boundingClientRect()
|
||||
query.exec((res: any) => {
|
||||
const rects = (res && res[0]) || []
|
||||
if (!rects || rects.length === 0) return
|
||||
const sidePadding = 24
|
||||
const wordSpacing = 12
|
||||
const rowSpacing = 16
|
||||
const availWidth = Math.max(windowWidth - sidePadding * 2, 100)
|
||||
const scaledHeights = rects.map((r: any) => r.height * scale)
|
||||
const rowHeight = Math.max(...scaledHeights)
|
||||
const scaledWidths = rects.map((r: any) => Math.max(r.width * scale, 10))
|
||||
const rows: { idxs: number[], width: number }[] = []
|
||||
let current: { idxs: number[], width: number } = { idxs: [], width: 0 }
|
||||
scaledWidths.forEach((w: number, i: number) => {
|
||||
const extra = current.idxs.length > 0 ? wordSpacing : 0
|
||||
if (current.width + extra + w <= availWidth) {
|
||||
current.idxs.push(i)
|
||||
current.width += extra + w
|
||||
} else {
|
||||
if (current.idxs.length > 0) rows.push(current)
|
||||
current = { idxs: [i], width: w }
|
||||
}
|
||||
})
|
||||
if (current.idxs.length > 0) rows.push(current)
|
||||
const totalHeight = rows.length * rowHeight + Math.max(rows.length - 1, 0) * rowSpacing
|
||||
const firstRowCenterY = centerY - totalHeight / 2 + rowHeight / 2
|
||||
const targetWords = rects.map((r: any, idx: number) => {
|
||||
const rcx = r.left + r.width / 2
|
||||
const rcy = r.top + r.height / 2
|
||||
let rowIndex = 0
|
||||
let y = firstRowCenterY
|
||||
for (let ri = 0; ri < rows.length; ri++) {
|
||||
if (rows[ri].idxs.includes(idx)) { rowIndex = ri; break }
|
||||
y += rowHeight + rowSpacing
|
||||
}
|
||||
const row = rows[rowIndex]
|
||||
const rowStartX = centerX - row.width / 2
|
||||
let cumX = 0
|
||||
for (const j of row.idxs) {
|
||||
if (j === idx) break
|
||||
cumX += scaledWidths[j] + wordSpacing
|
||||
}
|
||||
const targetCx = rowStartX + cumX + scaledWidths[idx] / 2
|
||||
const targetCy = firstRowCenterY + rowIndex * (rowHeight + rowSpacing)
|
||||
const dx = targetCx - rcx
|
||||
const dy = targetCy - rcy
|
||||
const transform = `translate(${dx}px, ${dy}px) scale(${scale})`
|
||||
return {
|
||||
text: words[idx] || '',
|
||||
left: r.left,
|
||||
top: r.top,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
targetTransform: transform,
|
||||
transform: 'translate(0px, 0px) scale(1)'
|
||||
}
|
||||
})
|
||||
this.setData({ cachedHighlightWords: targetWords, cachedSentenceIndex: this.data.currentIndex })
|
||||
})
|
||||
},
|
||||
|
||||
async getStandardVoice(sentenceId: string) {
|
||||
try {
|
||||
const map = this.data.standardAudioMap || {}
|
||||
if (map[sentenceId]) {
|
||||
if (this.audioCtx && !this.audioCtx.src) {
|
||||
this.audioCtx.src = map[sentenceId]
|
||||
}
|
||||
return
|
||||
}
|
||||
const { audio_id } = await apiManager.getStandardVoice(sentenceId)
|
||||
if (audio_id) {
|
||||
const fileUrl = String(audio_id)
|
||||
this.setData({ standardAudioMap: { ...map, [sentenceId]: fileUrl } })
|
||||
if (this.audioCtx && !this.audioCtx.src) {
|
||||
this.audioCtx.src = fileUrl
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('获取标准语音失败:', err)
|
||||
wx.showToast({ title: '获取语音失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
this.pollTimer = undefined
|
||||
}
|
||||
if (this.audioCtx) {
|
||||
try {
|
||||
this.audioCtx.destroy()
|
||||
} catch {}
|
||||
this.audioCtx = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
151
miniprogram/pages/scene_sentence/scene_sentence.wxml
Normal file
151
miniprogram/pages/scene_sentence/scene_sentence.wxml
Normal file
@@ -0,0 +1,151 @@
|
||||
<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>
|
||||
<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]}}">
|
||||
<view class="sentence-header">
|
||||
<view class="sentence-en sentence-content-wrapper">
|
||||
<view wx:for="{{englishWords}}" wx:for-item="w" wx:for-index="i" wx:key="i" class="sentence-word" style="{{i === 0 ? 'text-transform: capitalize;' : ''}}" data-word="{{w}}" data-index="{{i}}" bindtap="onWordTap">{{w}}</view>
|
||||
</view>
|
||||
<text class="sentence-zh">{{scene.list[currentIndex].sentenceZh}}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="sentence-body" scroll-y="true" scroll-with-animation="true">
|
||||
<view class="tags-wrap">
|
||||
<t-tag wx:for="{{scene.list[currentIndex].functionTags}}" wx:key="idx" variant="light"><text>#</text>{{item}}</t-tag>
|
||||
</view>
|
||||
|
||||
<view class="tip-card">
|
||||
<view class="tip-title">
|
||||
<t-icon name="info-circle" size="40rpx" />
|
||||
<text>发音提示</text>
|
||||
</view>
|
||||
<view class="tip-content">
|
||||
<text>{{scene.list[currentIndex].pronunciationTip}}</text>
|
||||
</view>
|
||||
<view class="fluency-hack" wx:if="{{scene.list[currentIndex].fluencyHacks}}">
|
||||
<view class="hack-title">
|
||||
<t-icon name="chat-bubble-smile" size="32rpx" />
|
||||
<text>技巧</text>
|
||||
</view>
|
||||
<view class="hack-content">
|
||||
<text>{{scene.list[currentIndex].fluencyHacks}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip-card">
|
||||
<view class="tip-title">
|
||||
<t-icon name="chat-bubble-error" size="40rpx" />
|
||||
<text>常见错误</text>
|
||||
</view>
|
||||
<view class="tip-content">
|
||||
<text wx:for="{{commonMistakes}}" wx:for-index="i" wx:key="idx" class="mistake-item ">{{item}}</text>
|
||||
</view>
|
||||
<!-- <view class="fluency-hack" wx:if="{{scene.list[currentIndex].fluencyHacks}}">
|
||||
<view class="hack-title">
|
||||
<t-icon name="notification-error" size="32rpx" />
|
||||
<text>注意事项</text>
|
||||
</view>
|
||||
<view class="hack-content">
|
||||
<text>{{scene.list[currentIndex].avoidScenarios}}</text>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<view class="section-title">
|
||||
<text>相似句型</text>
|
||||
</view>
|
||||
<view class="mistake-card" wx:if="{{pragmaticAlternative && pragmaticAlternative.length > 0}}">
|
||||
<view class="mistake-list">
|
||||
<text wx:for="{{pragmaticAlternative}}" wx:for-index="i" wx:key="idx" class="mistake-item ">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title">
|
||||
<text>搭话</text>
|
||||
</view>
|
||||
<view class="mistake-card" wx:if="{{responsePairs && responsePairs.length > 0}}">
|
||||
<view class="mistake-list">
|
||||
<text wx:for="{{responsePairs}}" wx:for-index="i" wx:key="idx" class="mistake-item ">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section-title">
|
||||
<text>核心词汇</text>
|
||||
<text class="section-count">{{scene.list[currentIndex].coreVocab.length}} words</text>
|
||||
</view>
|
||||
<view class="core-vocab">
|
||||
<t-tag class="core-vocab-tag" wx:for="{{scene.list[currentIndex].coreVocab}}" wx:for-index="i" wx:key="word" variant="light" theme="primary" data-word="{{item}}" bindtap="onWordTap">{{item}} {{scene.list[currentIndex].coreVocabDesc[i]}}</t-tag>
|
||||
</view>
|
||||
|
||||
<view class="section-title">
|
||||
<text>搭配用法</text>
|
||||
</view>
|
||||
<view class="collocations">
|
||||
<t-tag wx:for="{{scene.list[currentIndex].collocations}}" wx:key="idx" variant="light" theme="primary">{{item}}</t-tag>
|
||||
</view>
|
||||
|
||||
<!-- <view class="scene-explain">
|
||||
<text>{{scene.list[currentIndex].sceneExplanation}}</text>
|
||||
</view> -->
|
||||
</scroll-view>
|
||||
</block>
|
||||
|
||||
</view>
|
||||
<view class="highlight-area {{overlayVisible ? 'show' : ''}}" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop">
|
||||
<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>
|
||||
</view>
|
||||
<view wx:if="{{isRecording}}" class="recording-mask" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop"></view>
|
||||
<view class="bottom-bar {{buttonsVisible ? 'show' : ''}}">
|
||||
<t-icon name="chevron-left" class="bottom-btn {{currentIndex <= 0 ? 'disabled' : ''}}" size="48rpx" bind:tap="onPrevTap" />
|
||||
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" class="bottom-btn" size="48rpx" bind:tap="playStandardVoice" />
|
||||
<view class="mic-wrap" bind:longpress="handleRecordStart" bind:touchend="handleRecordEnd" bind:touchcancel="handleRecordEnd">
|
||||
<t-icon name="microphone-1" class="microphone {{isRecording ? 'recording' : 'bottom-btn'}}" size="48rpx" />
|
||||
<view wx:if="{{isRecording}}" class="mic">
|
||||
<view class="mic-shadow"></view>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="fact-check" class="bottom-btn {{(hasScoreInfo && !isRecording) ? '' : 'disabled'}}" size="48rpx" bind:tap="onScoreTap" />
|
||||
<t-icon name="chevron-right" class="bottom-btn {{(scene.list && (currentIndex >= scene.list.length - 1)) ? 'disabled' : ''}}" size="48rpx" bind:tap="onNextTap" />
|
||||
</view>
|
||||
<score-modal
|
||||
wx:if="{{isScoreModalOpen && hasScoreInfo}}"
|
||||
visible="{{scoreModalVisible}}"
|
||||
hasScoreInfo="{{hasScoreInfo}}"
|
||||
sentence="{{currentSentence}}"
|
||||
totalScore="{{totalScore}}"
|
||||
accuracyScore="{{accuracyScore}}"
|
||||
completenessScore="{{completenessScore}}"
|
||||
fluencyScore="{{fluencyScore}}"
|
||||
circleProgressStyle="{{circleProgressStyle}}"
|
||||
accuracyCircleStyle="{{accuracyCircleStyle}}"
|
||||
completenessCircleStyle="{{completenessCircleStyle}}"
|
||||
fluencyCircleStyle="{{fluencyCircleStyle}}"
|
||||
wordScores="{{wordScores}}"
|
||||
bind:close="onCloseScoreModal"
|
||||
bind:play="playStandardVoice"
|
||||
/>
|
||||
<word-dictionary
|
||||
visible="{{showDictPopup}}"
|
||||
expanded="{{showDictExtended}}"
|
||||
loading="{{dictLoading}}"
|
||||
wordDict="{{wordDict}}"
|
||||
isWordEmptyResult="{{isWordEmptyResult}}"
|
||||
dictDefaultTabValue="{{dictDefaultTabValue}}"
|
||||
bind:close="handleDictClose"
|
||||
bind:more="handleDictMore"
|
||||
bind:wordTap="onWordTap"
|
||||
/>
|
||||
<view class="word-popup-mask" wx:if="{{showDictPopup}}" bindtap="handleDictClose"></view>
|
||||
</view>
|
||||
447
miniprogram/pages/scene_sentence/scene_sentence.wxss
Normal file
447
miniprogram/pages/scene_sentence/scene_sentence.wxss
Normal file
@@ -0,0 +1,447 @@
|
||||
.scene-sentence-container {
|
||||
min-height: 100vh;
|
||||
background: #ffffff;
|
||||
/* padding-bottom: calc(220rpx + env(safe-area-inset-bottom)); */
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 28rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.page-loading-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
z-index: 1000;
|
||||
}
|
||||
/* 遮罩层样式 */
|
||||
.word-popup-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 99;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.fade-in { animation: fadeInUp 260ms ease forwards; }
|
||||
.fade-out { animation: fadeOutDown 200ms ease forwards; }
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translate3d(0, 12rpx, 0); }
|
||||
to { opacity: 1; transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
@keyframes fadeOutDown {
|
||||
from { opacity: 1; transform: translate3d(0, 0, 0); }
|
||||
to { opacity: 0; transform: translate3d(0, 12rpx, 0); }
|
||||
}
|
||||
|
||||
.sentence-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sentence-body {
|
||||
flex: 1;
|
||||
margin-top: 24rpx;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
margin-bottom: calc(110rpx + env(safe-area-inset-bottom)) ;
|
||||
}
|
||||
.sentence-content-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
.sentence-word {
|
||||
font-size: 40rpx;
|
||||
line-height: 56rpx;
|
||||
color: #001858;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sentence-en {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #001858;
|
||||
line-height: 56rpx;
|
||||
}
|
||||
.sentence-zh {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
/* 高亮遮罩与单词浮层 */
|
||||
.highlight-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(0,0,0,0.6);
|
||||
z-index: 99;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.highlight-area.show { display: block; opacity: 1; }
|
||||
.overlay-word {
|
||||
position: fixed;
|
||||
color: #ffffff;
|
||||
font-size: 40rpx;
|
||||
line-height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease;
|
||||
font-weight: 600;
|
||||
}
|
||||
.overlay-word .overlay-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
.overlay-word.show { opacity: 1; }
|
||||
.overlay-word.zoom { transform-origin: center center; transition: transform 500ms ease; }
|
||||
.recording-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
z-index: 99;
|
||||
}
|
||||
.mic-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.microphone {
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.microphone.recording {
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
.mic {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mic::before,
|
||||
.mic::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 100%;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 4.8px 4.8px #1c084f;
|
||||
}
|
||||
|
||||
.mic::before {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
background-color: #1a084e;
|
||||
}
|
||||
|
||||
.mic::after {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background-color: #2f1e5f;
|
||||
animation: mic-circle-size 0.8s linear infinite alternate;
|
||||
}
|
||||
|
||||
.mic-shadow {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: 100%;
|
||||
z-index: 1;
|
||||
box-shadow: 3.6rpx -14.4rpx 7.2rpx 3.6rpx #823ca6,
|
||||
9.6rpx -2.4rpx 14.4rpx 3.6rpx #aab3d2,
|
||||
9.6rpx 7.2rpx 26.4rpx 3.6rpx #5acee3,
|
||||
19.2rpx 2.4rpx 7.2rpx 3.6rpx #1b7d8f,
|
||||
2.4rpx 1.2rpx 21.6rpx 3.6rpx #f30bf5;
|
||||
transform: translate(-50%, -50%);
|
||||
transform-origin: 0% 0%;
|
||||
animation: mic-shadow-rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes mic-circle-size {
|
||||
from {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
}
|
||||
to {
|
||||
width: 57.6rpx;
|
||||
height: 57.6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mic-shadow-rotate {
|
||||
from { rotate: 0deg; }
|
||||
to { rotate: 360deg; }
|
||||
}
|
||||
|
||||
.tags-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip-card {
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e6eef9;
|
||||
position: relative;
|
||||
margin-bottom: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.tip-card::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -60rpx;
|
||||
right: -60rpx;
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #eef3fb;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.tip-title {
|
||||
position: relative;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
font-size: 28rpx;
|
||||
color: #001858;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tip-content {
|
||||
position: relative;
|
||||
padding: 0 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 42rpx;
|
||||
z-index: 1;
|
||||
}
|
||||
.fluency-hack {
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
padding: 24rpx 24rpx 28rpx;
|
||||
border-top: 2rpx solid #e6eef9;
|
||||
background: #eef3fb;
|
||||
border-radius: 0 0 20rpx 20rpx;
|
||||
}
|
||||
.hack-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
font-size: 26rpx;
|
||||
color: #001858;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hack-content {
|
||||
margin-top: 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 10rpx;
|
||||
font-size: 30rpx;
|
||||
color: #001858;
|
||||
font-weight: 600;
|
||||
margin: 24rpx 0 12rpx;
|
||||
}
|
||||
.section-count {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 400;
|
||||
}
|
||||
.core-vocab {
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
padding: 12rpx;
|
||||
}
|
||||
|
||||
.core-vocab-tag {
|
||||
margin: 8rpx 12rpx 8rpx 0;
|
||||
}
|
||||
|
||||
.collocations {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.mistake-card {
|
||||
position: relative;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
border: 2rpx solid #e6eef9;
|
||||
margin: 12rpx 0 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mistake-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8rpx;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-bottom-left-radius: 20rpx;
|
||||
background: #eef3fb;
|
||||
}
|
||||
.mistake-title {
|
||||
font-size: 28rpx;
|
||||
color: #001858;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.mistake-item {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 42rpx;
|
||||
}
|
||||
.scene-explain {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
line-height: 40rpx;
|
||||
background: #fafafa;
|
||||
border-radius: 16rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
box-shadow: 0 -8rpx 24rpx rgba(0,0,0,0.08);
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.bottom-bar.show { opacity: 1; }
|
||||
.bottom-btn {
|
||||
padding: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
.bottom-btn.disabled {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
.score-btn {
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 28rpx;
|
||||
background: #f5f5f5;
|
||||
color: #001858;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.scanner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
border-radius: 12rpx;
|
||||
background-color: rgba(255, 255, 255, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
.scanner-visible { animation: scannerFadeIn 1s 1000ms ease forwards; }
|
||||
@keyframes scannerFadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||
|
||||
.star { position: relative; opacity: 0; transform-origin: center center; will-change: transform, opacity; }
|
||||
.star::after {
|
||||
content:"";
|
||||
position:absolute;
|
||||
width: 36rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(337deg, #DE9EFC 7.88%, #31DAFF 107.03%);
|
||||
transform:rotate(45deg) skewX(22.5deg) skewY(22.5deg);
|
||||
}
|
||||
.star::before{
|
||||
content:"";
|
||||
position:absolute;
|
||||
width: 36rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(78.35deg, #DE9EFC 7.88%, #31DAFF 107.03%);
|
||||
transform:rotate(-45deg) skewX(22.5deg) skewY(22.5deg);
|
||||
}
|
||||
.scanner-visible .star { animation: starCycle 3000ms ease-in-out infinite; }
|
||||
.scanner-visible .star1 { animation-delay: 1000ms; }
|
||||
.scanner-visible .star2 { animation-delay: 2000ms; }
|
||||
.scanner-visible .star3 { animation-delay: 3000ms; }
|
||||
@keyframes starCycle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-24rpx, -24rpx) scale(0.5);
|
||||
}
|
||||
34% {
|
||||
opacity: 1;
|
||||
transform: translate(-24rpx, -24rpx) scale(0.5);
|
||||
}
|
||||
67% {
|
||||
opacity: 1;
|
||||
transform: translate(0rpx, 0rpx) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(36rpx, 36rpx) scale(0.5);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,8 @@ Page({
|
||||
dateStripScrollLeft: 0,
|
||||
dateStripTransform: '',
|
||||
isSwitchingDate: false,
|
||||
dateItemWidthRpx: 120,
|
||||
dateItemGapRpx: 24,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -820,6 +822,8 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
noop() {},
|
||||
|
||||
// 跳转到个人主页
|
||||
goProfile() {
|
||||
wx.navigateTo({
|
||||
@@ -860,7 +864,10 @@ Page({
|
||||
const gridCols = 1;
|
||||
const monthTitle = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}`;
|
||||
const stripCentered = true;
|
||||
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall: false, waterfallLeft: [], waterfallRight: [], stripCentered, todayKey: selectedKey });
|
||||
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall: false, waterfallLeft: [], waterfallRight: [], stripCentered, todayKey: selectedKey }, () => {
|
||||
this.computeDateItemDimensions();
|
||||
this.centerDateStrip();
|
||||
});
|
||||
return;
|
||||
}
|
||||
records.sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
@@ -903,6 +910,7 @@ Page({
|
||||
}
|
||||
const stripCentered = strip.length <= 3;
|
||||
this.setData({ dateStripItems: strip, imagesByDate, selectedDateKey: selectedKey, selectedDateImages: selectedImages, gridCols, selectedMonthTitle: monthTitle, useWaterfall, waterfallLeft, waterfallRight, stripCentered, todayKey, animateWaterfall: false }, () => {
|
||||
this.computeDateItemDimensions();
|
||||
this.centerDateStrip();
|
||||
});
|
||||
if (useWaterfall) {
|
||||
@@ -964,15 +972,18 @@ Page({
|
||||
if (cached) {
|
||||
img.thumbnail_url = typeof cached === 'string' ? cached.trim() : cached;
|
||||
img.thumbnail_loading = false;
|
||||
img.thumb_loaded = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
img.thumbnail_url = (await apiManager.getFileDisplayUrl(img.thumbnail_file_id)).trim();
|
||||
this.cacheThumbnail(img.thumbnail_file_id, img.thumbnail_url);
|
||||
img.thumbnail_loading = false;
|
||||
img.thumb_loaded = false;
|
||||
} catch {
|
||||
img.thumbnail_url = '/static/sun-2.png';
|
||||
img.thumbnail_loading = false;
|
||||
img.thumb_loaded = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -987,6 +998,95 @@ Page({
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onThumbLoaded(e: any) {
|
||||
try {
|
||||
const imageId = e?.currentTarget?.dataset?.imageId;
|
||||
if (!imageId) return;
|
||||
const list = (this.data.selectedDateImages || []).map((img: any) => {
|
||||
if (img && img.image_id === imageId) {
|
||||
return { ...img, thumb_loaded: true };
|
||||
}
|
||||
return img;
|
||||
});
|
||||
this.setData({ selectedDateImages: list }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
onWfThumbLoaded(e: any) {
|
||||
try {
|
||||
const imageId = e?.currentTarget?.dataset?.imageId;
|
||||
if (!imageId) return;
|
||||
const list = (this.data.selectedDateImages || []).map((img: any) => {
|
||||
if (img && img.image_id === imageId) {
|
||||
return { ...img, thumb_loaded: true };
|
||||
}
|
||||
return img;
|
||||
});
|
||||
this.setData({ selectedDateImages: list }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
onImageMoreTap(e: any) {
|
||||
try {
|
||||
const imageId = e?.currentTarget?.dataset?.imageId;
|
||||
if (!imageId) return;
|
||||
const list = (this.data.selectedDateImages || []).map((img: any) => {
|
||||
if (img && img.image_id === imageId) {
|
||||
const open = !!img.more_open;
|
||||
if (open) {
|
||||
return { ...img, menu_closing: true, more_open: false };
|
||||
}
|
||||
return { ...img, more_open: true, menu_closing: false };
|
||||
}
|
||||
return { ...img, more_open: false, menu_closing: false };
|
||||
});
|
||||
this.setData({ selectedDateImages: list }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
// 如果是关闭,延迟移除 closing 状态以便播放淡出动画
|
||||
setTimeout(() => {
|
||||
const after = (this.data.selectedDateImages || []).map((img: any) => {
|
||||
if (img && img.image_id === imageId) {
|
||||
return { ...img, menu_closing: false };
|
||||
}
|
||||
return img;
|
||||
});
|
||||
this.setData({ selectedDateImages: after }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
}, 260);
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
onImageSceneSentenceTap(e: any) {
|
||||
try {
|
||||
const imageId = e?.currentTarget?.dataset?.imageId;
|
||||
if (!imageId) return;
|
||||
// 关闭所有更多菜单
|
||||
const list = (this.data.selectedDateImages || []).map((img: any) => ({ ...img, more_open: false, menu_closing: false }));
|
||||
this.setData({ selectedDateImages: list }, () => {
|
||||
if (this.data.useWaterfall) {
|
||||
this.balanceWaterfall(this.data.selectedDateImages || []);
|
||||
}
|
||||
});
|
||||
wx.navigateTo({
|
||||
url: `/pages/scene_sentence/scene_sentence?image_id=${encodeURIComponent(imageId)}`
|
||||
});
|
||||
} catch (err) {}
|
||||
},
|
||||
|
||||
async ensureThumbSize(img: any): Promise<{ w: number; h: number } | null> {
|
||||
if (img && typeof img._thumbW === 'number' && typeof img._thumbH === 'number') {
|
||||
return { w: img._thumbW, h: img._thumbH };
|
||||
@@ -1009,7 +1109,7 @@ Page({
|
||||
},
|
||||
|
||||
async computeWaterfallColumns(images: any[]): Promise<{ left: any[]; right: any[] }> {
|
||||
const windowInfo = wx.getWindowInfo();
|
||||
const windowInfo = (wx as any).getWindowInfo();
|
||||
const ww = windowInfo.windowWidth;
|
||||
const rpx = ww / 750;
|
||||
const padPx = Math.floor(24 * rpx);
|
||||
@@ -1046,29 +1146,65 @@ Page({
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
computeDateItemDimensions() {
|
||||
try {
|
||||
const items = this.data.dateStripItems || [];
|
||||
if (!items || items.length === 0) return;
|
||||
const windowInfo = (wx as any).getWindowInfo();
|
||||
const ww = windowInfo.windowWidth;
|
||||
const rpx = ww / 750;
|
||||
const containerW = ww - Math.floor(40 * rpx);
|
||||
const itemCount = items.length;
|
||||
const minWidthRpx = 96;
|
||||
const maxWidthRpx = 160;
|
||||
const minGapRpx = 12;
|
||||
const maxGapRpx = 24;
|
||||
const tryCompute = (visible: number) => {
|
||||
let gapRpx = maxGapRpx;
|
||||
let gapPx = Math.floor(gapRpx * rpx);
|
||||
let widthPx = Math.floor((containerW - (visible - 1) * gapPx) / visible);
|
||||
let widthRpx = Math.floor(widthPx / rpx);
|
||||
if (widthRpx < minWidthRpx) {
|
||||
gapRpx = minGapRpx;
|
||||
gapPx = Math.floor(gapRpx * rpx);
|
||||
widthPx = Math.floor((containerW - (visible - 1) * gapPx) / visible);
|
||||
widthRpx = Math.floor(widthPx / rpx);
|
||||
}
|
||||
return { widthRpx, gapRpx };
|
||||
};
|
||||
let visible = itemCount >= 6 ? 6 : Math.min(5, itemCount);
|
||||
let { widthRpx, gapRpx } = tryCompute(visible);
|
||||
if (widthRpx < minWidthRpx && visible > 5) {
|
||||
visible = 5;
|
||||
({ widthRpx, gapRpx } = tryCompute(visible));
|
||||
}
|
||||
widthRpx = Math.max(minWidthRpx, Math.min(widthRpx, maxWidthRpx));
|
||||
this.setData({ dateItemWidthRpx: widthRpx, dateItemGapRpx: gapRpx });
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
centerDateStrip() {
|
||||
try {
|
||||
const items = this.data.dateStripItems || [];
|
||||
if (!items || items.length < 2) return;
|
||||
const idx = items.findIndex((it: any) => it && it.key === this.data.selectedDateKey);
|
||||
if (idx < 0) return;
|
||||
const windowInfo = wx.getWindowInfo();
|
||||
const windowInfo = (wx as any).getWindowInfo();
|
||||
const ww = windowInfo.windowWidth;
|
||||
const rpx = ww / 750;
|
||||
const itemW = Math.floor(120 * rpx);
|
||||
const gap = Math.floor(24 * rpx);
|
||||
const widthRpx = (this.data as any).dateItemWidthRpx || 120;
|
||||
const gapRpx = (this.data as any).dateItemGapRpx || 24;
|
||||
const itemW = Math.floor(widthRpx * rpx);
|
||||
const gap = Math.floor(gapRpx * rpx);
|
||||
const n = items.length;
|
||||
const contentW = n * itemW + (n - 1) * gap;
|
||||
const containerW = ww;
|
||||
const containerW = ww - Math.floor(40 * rpx);
|
||||
if (contentW > containerW) {
|
||||
let target = Math.floor(idx * (itemW + gap) - (containerW - itemW) / 2);
|
||||
target = Math.max(0, Math.min(contentW - containerW, target));
|
||||
this.setData({ dateStripScrollLeft: target, dateStripTransform: '' });
|
||||
this.setData({ dateStripScrollLeft: target, dateStripTransform: '', stripCentered: false });
|
||||
} else {
|
||||
const selectedX = Math.floor(idx * (itemW + gap));
|
||||
const centerX = Math.floor((containerW - itemW) / 2);
|
||||
const shift = centerX - selectedX;
|
||||
this.setData({ dateStripTransform: `transform: translateX(${shift}px);` });
|
||||
this.setData({ dateStripScrollLeft: 0, dateStripTransform: '', stripCentered: true });
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
@@ -64,13 +64,13 @@
|
||||
<!-- <t-icon name="calendar" size="40rpx" /> -->
|
||||
</view>
|
||||
<scroll-view class="date-strip" scroll-x scroll-with-animation="{{true}}" scroll-left="{{dateStripScrollLeft}}">
|
||||
<view class="date-strip-inner {{stripCentered ? 'strip-center' : 'strip-default'}}" style="{{dateStripTransform}}">
|
||||
<view class="date-strip-inner {{stripCentered ? 'strip-center' : 'strip-default'}}" style="gap: {{dateItemGapRpx}}rpx; {{dateStripTransform}}">
|
||||
<block wx:for="{{dateStripItems}}" wx:key="index">
|
||||
<view wx:if="{{item.type === 'day'}}" id="date-item-{{item.key}}" class="date-item {{item.key === selectedDateKey ? 'active' : ''}}" data-key="{{item.key}}" bindtap="onDateTap">
|
||||
<view wx:if="{{item.type === 'day'}}" id="date-item-{{item.key}}" class="date-item {{item.key === selectedDateKey ? 'active' : ''}}" data-key="{{item.key}}" bindtap="onDateTap" style="width: {{dateItemWidthRpx}}rpx; flex: 0 0 {{dateItemWidthRpx}}rpx;">
|
||||
<view class="weekday">{{item.weekday}}</view>
|
||||
<view class="day">{{item.day}}</view>
|
||||
</view>
|
||||
<view wx:elif="{{item.type === 'ym'}}" id="date-item-{{item.key}}" class="date-item label">
|
||||
<view wx:elif="{{item.type === 'ym'}}" id="date-item-{{item.key}}" class="date-item label" style="width: {{dateItemWidthRpx}}rpx; flex: 0 0 {{dateItemWidthRpx}}rpx;">
|
||||
<view class="weekday">{{item.year}}</view>
|
||||
<view class="day">{{item.month}}</view>
|
||||
</view>
|
||||
@@ -85,7 +85,19 @@
|
||||
<view class="image-list">
|
||||
<view class="list-item {{animateWaterfall ? 'fade-in' : ''}}" wx:for="{{selectedDateImages}}" wx:for-item="image" wx:for-index="k" wx:key="image_id" catch:tap="onImageTap" data-image-id="{{image.image_id}}" style="animation-delay: {{k * 0.15}}s;">
|
||||
<t-skeleton wx:if="{{image.thumbnail_loading}}" class="full-image" row-col="{{[1]}}" animation="gradient" loading></t-skeleton>
|
||||
<image wx:else class="full-image" src="{{image.thumbnail_url}}" mode="widthFix"/>
|
||||
<image wx:else class="full-image" src="{{image.thumbnail_url}}" mode="widthFix" bindload="onThumbLoaded" data-image-id="{{image.image_id}}"/>
|
||||
<view wx:if="{{image.thumb_loaded}}" class="image-more-wrap" catchtap="noop">
|
||||
<view class="mini-button" bindtap="onImageMoreTap" data-image-id="{{image.image_id}}">
|
||||
<view class="ul-mini {{image.more_open ? 'active' : ''}}">
|
||||
<view class="dot1"></view>
|
||||
<view class="dot2"></view>
|
||||
<view class="dot3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{image.more_open || image.menu_closing}}" class="image-more-menu {{image.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{image.image_id}}">场景句型</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
@@ -94,13 +106,37 @@
|
||||
<view class="wf-col">
|
||||
<view class="wf-item {{animateWaterfall ? 'fade-in' : ''}}" wx:for="{{waterfallLeft}}" wx:for-index="i" wx:key="image_id" catch:tap="onImageTap" data-image-id="{{item.image_id}}" style="animation-delay: {{i * 0.15}}s;">
|
||||
<t-skeleton wx:if="{{item.thumbnail_loading}}" class="wf-image" row-col="{{[1]}}" animation="gradient" loading></t-skeleton>
|
||||
<image wx:else class="wf-image" src="{{item.thumbnail_url}}" mode="widthFix" />
|
||||
<image wx:else class="wf-image" src="{{item.thumbnail_url}}" mode="widthFix" bindload="onWfThumbLoaded" data-image-id="{{item.image_id}}"/>
|
||||
<view wx:if="{{item.thumb_loaded}}" class="image-more-wrap" catchtap="noop">
|
||||
<view class="mini-button" bindtap="onImageMoreTap" data-image-id="{{item.image_id}}">
|
||||
<view class="ul-mini {{item.more_open ? 'active' : ''}}">
|
||||
<view class="dot1"></view>
|
||||
<view class="dot2"></view>
|
||||
<view class="dot3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.more_open || item.menu_closing}}" class="image-more-menu {{item.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{item.image_id}}">场景句型</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="wf-col">
|
||||
<view class="wf-item {{animateWaterfall ? 'fade-in' : ''}}" wx:for="{{waterfallRight}}" wx:for-index="j" wx:key="image_id" catch:tap="onImageTap" data-image-id="{{item.image_id}}" style="animation-delay: {{j * 0.15 + 0.15}}s;">
|
||||
<t-skeleton wx:if="{{item.thumbnail_loading}}" class="wf-image" row-col="{{[1]}}" animation="gradient" loading></t-skeleton>
|
||||
<image wx:else class="wf-image" src="{{item.thumbnail_url}}" mode="widthFix"/>
|
||||
<image wx:else class="wf-image" src="{{item.thumbnail_url}}" mode="widthFix" bindload="onWfThumbLoaded" data-image-id="{{item.image_id}}"/>
|
||||
<view wx:if="{{item.thumb_loaded}}" class="image-more-wrap" catchtap="noop">
|
||||
<view class="mini-button" bindtap="onImageMoreTap" data-image-id="{{item.image_id}}">
|
||||
<view class="ul-mini {{item.more_open ? 'active' : ''}}">
|
||||
<view class="dot1"></view>
|
||||
<view class="dot2"></view>
|
||||
<view class="dot3"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{item.more_open || item.menu_closing}}" class="image-more-menu {{item.menu_closing ? 'closing' : 'opening'}}">
|
||||
<view class="more-item-mini" bindtap="onImageSceneSentenceTap" data-image-id="{{item.image_id}}">场景句型</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -113,13 +113,109 @@
|
||||
|
||||
/* 新:单列全幅图片列表 */
|
||||
.image-list { padding: 24rpx; box-sizing: border-box; }
|
||||
.list-item { margin-bottom: 24rpx; border-radius: 24rpx; overflow: hidden; background: #fff; }
|
||||
.list-item { margin-bottom: 24rpx; border-radius: 24rpx; overflow: hidden; background: #fff; position: relative; }
|
||||
.full-image { width: 100%; height: auto; display: block; --td-skeleton-text-height: 550rpx;}
|
||||
|
||||
/* 新:两列瀑布布局 */
|
||||
.waterfall { display: flex; gap: 12rpx; padding: 12rpx; box-sizing: border-box; }
|
||||
|
||||
/* 图片右下角更多按钮 */
|
||||
.image-more-wrap {
|
||||
position: absolute;
|
||||
right: 16rpx;
|
||||
bottom: 16rpx;
|
||||
z-index: 2;
|
||||
}
|
||||
.mini-button {
|
||||
padding: 6rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.95);
|
||||
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.ul-mini {
|
||||
position: relative;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: transparent;
|
||||
}
|
||||
.ul-mini .dot1,
|
||||
.ul-mini .dot2,
|
||||
.ul-mini .dot3 {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
background: #666;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
transition: 0.6s;
|
||||
}
|
||||
.ul-mini .dot1 { left: 10%; z-index: 2; }
|
||||
.ul-mini .dot2 { left: 50%; transition: .6s; }
|
||||
.ul-mini .dot3 { left: 90%; z-index: 2; }
|
||||
.ul-mini.active .dot1,
|
||||
.ul-mini.active .dot3 {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
border-radius: 0;
|
||||
width: 6rpx;
|
||||
height: 38rpx;
|
||||
border-radius: 6rpx;
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
.ul-mini.active .dot1 {
|
||||
transform: translate(-50%, -50%) rotate(405deg);
|
||||
}
|
||||
.ul-mini.active .dot3 {
|
||||
transform: translate(-50%, -50%) rotate(-405deg);
|
||||
}
|
||||
.ul-mini.active .dot2 {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: #f5f5f5;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
}
|
||||
.image-more-menu {
|
||||
position: absolute;
|
||||
right: 0rpx;
|
||||
bottom: 70rpx;
|
||||
background: #fff;
|
||||
color: #666;
|
||||
border-radius: 12rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.08);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
.image-more-menu.opening {
|
||||
animation: menuFadeIn 240ms ease forwards;
|
||||
}
|
||||
.image-more-menu.closing {
|
||||
animation: menuFadeOut 240ms ease forwards;
|
||||
}
|
||||
.more-item-mini {
|
||||
font-size: 26rpx;
|
||||
white-space: nowrap;
|
||||
line-height: 32rpx;
|
||||
}
|
||||
|
||||
@keyframes menuFadeIn {
|
||||
0% { opacity: 0; transform: translateY(8rpx) scale(0.98); }
|
||||
100% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes menuFadeOut {
|
||||
0% { opacity: 1; transform: translateY(0) scale(1); }
|
||||
100% { opacity: 0; transform: translateY(8rpx) scale(0.98); }
|
||||
}
|
||||
.wf-col { width: calc(50% - 6rpx); }
|
||||
.wf-item { margin-bottom: 12rpx; border-radius: 24rpx; overflow: hidden; background: #fff; }
|
||||
.wf-item { margin-bottom: 12rpx; border-radius: 24rpx; overflow: hidden; background: #fff; position: relative; }
|
||||
.wf-image { width: 100%; height: auto; display: block; --td-skeleton-text-height: 330rpx; }
|
||||
|
||||
/* 瀑布流淡入动画 */
|
||||
@@ -371,15 +467,16 @@
|
||||
.month-title { font-size: 36rpx; }
|
||||
.date-strip { white-space: nowrap; height: 100rpx;}
|
||||
.date-strip-inner { display: flex; gap: 24rpx; transition: transform 0.3s ease; will-change: transform; }
|
||||
.strip-center { justify-content: center; min-width: 100%; }
|
||||
.strip-center { justify-content: center; }
|
||||
.strip-default { justify-content: flex-start; }
|
||||
.date-strip::-webkit-scrollbar { display: none; }
|
||||
.date-item {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 120rpx;
|
||||
flex: 0 0 100rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
|
||||
@@ -131,9 +131,9 @@ initPersistentCacheAsync()
|
||||
|
||||
// 词典等级映射
|
||||
const DICT_LEVEL_OPTIONS = {
|
||||
LEVEL1: "初级",
|
||||
LEVEL2: "中级",
|
||||
LEVEL3: "高级"
|
||||
LEVEL1: "level1",
|
||||
LEVEL2: "level2",
|
||||
LEVEL3: "level3"
|
||||
}
|
||||
|
||||
class ApiManager {
|
||||
@@ -479,7 +479,7 @@ class ApiManager {
|
||||
async smartLogin(forceRefresh: boolean = false): Promise<ILoginResponse | null> {
|
||||
if (USE_CLOUD) {
|
||||
const app = getApp<IAppOption>()
|
||||
let dictLevel = wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
let dictLevel = wx.getStorageSync('dictLevel') || 'level1'
|
||||
try {
|
||||
const resp = await this.request<{ user: IUserInfo; dict_level?: string; session_uuid?: string; settings?: { dict_level?: string }; points?: { balance: number; expired_time: string } }>(
|
||||
'/api/v1/wx/user',
|
||||
@@ -492,7 +492,7 @@ class ApiManager {
|
||||
app.globalData.userInfo = data.user
|
||||
wx.setStorageSync('userInfo', data.user)
|
||||
}
|
||||
const dl = data.dict_level || 'LEVEL1'
|
||||
const dl = data.dict_level || 'level1'
|
||||
if (dl) {
|
||||
dictLevel = dl
|
||||
wx.setStorageSync('dictLevel', dictLevel)
|
||||
@@ -521,12 +521,12 @@ class ApiManager {
|
||||
if (!isExpired) {
|
||||
app.globalData.isLoggedIn = true
|
||||
app.globalData.token = authInfo.token
|
||||
app.globalData.dictLevel = authInfo.dictLevel || 'LEVEL1'
|
||||
app.globalData.dictLevel = authInfo.dictLevel || 'level1'
|
||||
return {
|
||||
access_token: authInfo.token,
|
||||
access_token_expire_time: new Date(authInfo.tokenExpiry).toISOString(),
|
||||
session_uuid: authInfo.sessionUuid || '',
|
||||
dict_level: authInfo.dictLevel || 'LEVEL1'
|
||||
dict_level: authInfo.dictLevel || 'level1'
|
||||
}
|
||||
} else {
|
||||
this.clearAuthData()
|
||||
@@ -936,7 +936,7 @@ class ApiManager {
|
||||
|
||||
// 获取当前的词典等级配置
|
||||
const app = getApp<IAppOption>()
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||||
|
||||
console.log('开始图片识别请求:', { fileId, type, dictLevel })
|
||||
const response = await this.request<{task_id: string, status: string, message: string}>('/api/v1/image/recognize/async', 'POST', {
|
||||
@@ -1862,7 +1862,7 @@ class ApiManager {
|
||||
try {
|
||||
console.log('开始获取图片文本和评分信息', { imageId });
|
||||
const app = getApp<IAppOption>()
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'LEVEL1'
|
||||
const dictLevel = app.globalData.dictLevel || wx.getStorageSync('dictLevel') || 'level1'
|
||||
const response = await this.request<{
|
||||
image_file_id: string,
|
||||
assessments: Array<{
|
||||
@@ -2024,6 +2024,29 @@ class ApiManager {
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getSceneSentence(imageId: string): Promise<any> {
|
||||
const resp = await this.request<any>(`/api/v1/scene/image/${imageId}/sentence`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getRecordResult(textId: string | number): Promise<any> {
|
||||
const resp = await this.request<any>(`/api/v1/image_text/${textId}/record_result`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async createScene(imageId: string, scene_type: string = 'scene_sentence'): Promise<{ task_id: string; status?: string }> {
|
||||
const resp = await this.request<{ task_id: string; status?: string }>(`/api/v1/scene/create`, 'POST', {
|
||||
image_id: imageId,
|
||||
scene_type
|
||||
})
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getSceneTask(taskId: string | number): Promise<{ status: string; message?: string }> {
|
||||
const resp = await this.request<{ status: string; message?: string }>(`/api/v1/scene/task/${taskId}`, 'GET')
|
||||
return resp.data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
|
||||
Reference in New Issue
Block a user