add subtile animation
This commit is contained in:
@@ -118,6 +118,13 @@ interface IPageData {
|
||||
showBackIcon: boolean // 是否显示返回图标
|
||||
previousWord: string // 上一个单词
|
||||
isReturningFromPrevious: boolean // 是否正在从上一个单词返回
|
||||
overlayVisible: boolean,
|
||||
highlightWords: string[],
|
||||
highlightShow: boolean,
|
||||
highlightZoom: boolean,
|
||||
focusWordIndex: number,
|
||||
focusTransform: string,
|
||||
cachedHighlightWords: string[],
|
||||
isWordEmptyResult: boolean
|
||||
}
|
||||
|
||||
@@ -150,6 +157,8 @@ type IPageMethods = {
|
||||
getTransIconName: () => string
|
||||
onPageScroll: (e: any) => void
|
||||
processCollinsData: (collinsData: any) => any // Add this line
|
||||
computeHighlightLayout:() => void
|
||||
onMicHighlight: () => void
|
||||
onMoreTap: () => void
|
||||
}
|
||||
|
||||
@@ -231,7 +240,6 @@ Page<IPageData, IPageInstance>({
|
||||
scoreModalVisible: false,
|
||||
justSelectedByWord: false,
|
||||
isMoreMenuOpen: false, // 更多菜单是否打开
|
||||
isMoreMenuClosing: false,
|
||||
displayTotalScore: 0,
|
||||
displayAccuracyScore: 0,
|
||||
displayCompletenessScore: 0,
|
||||
@@ -242,9 +250,56 @@ Page<IPageData, IPageInstance>({
|
||||
showBackIcon: false, // 是否显示返回图标
|
||||
previousWord: '', // 上一个单词
|
||||
isReturningFromPrevious: false, // 是否正在从上一个单词返回
|
||||
isWordEmptyResult: false
|
||||
isWordEmptyResult: false,
|
||||
overlayVisible: false,
|
||||
highlightWords: [],
|
||||
highlightShow: false,
|
||||
highlightZoom: false,
|
||||
focusWordIndex: 0,
|
||||
focusTransform: '',
|
||||
cachedHighlightWords: [],
|
||||
isMoreMenuClosing: false
|
||||
},
|
||||
|
||||
onMicHighlight() {
|
||||
const pre = this.data.cachedHighlightWords || []
|
||||
if (!pre || pre.length === 0) {
|
||||
this.computeHighlightLayout()
|
||||
}
|
||||
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)
|
||||
},
|
||||
// 切换评分区域展开状态
|
||||
toggleScoreSection() {
|
||||
this.setData({
|
||||
@@ -458,12 +513,19 @@ Page<IPageData, IPageInstance>({
|
||||
// 长按开始录音
|
||||
handleRecordStart() {
|
||||
this.startRecording()
|
||||
try { this.onMicHighlight() } catch (e) {}
|
||||
},
|
||||
|
||||
// 松开结束录音
|
||||
handleRecordEnd() {
|
||||
console.log('---lisa-handleRecordEnd')
|
||||
this.stopRecording()
|
||||
// 淡出高亮层
|
||||
this.setData({ overlayVisible: false })
|
||||
const timer = setTimeout(() => {
|
||||
this.setData({ highlightShow: false, highlightZoom: false, focusTransform: '', highlightWords: [] })
|
||||
clearTimeout(timer)
|
||||
}, 320)
|
||||
},
|
||||
|
||||
// 点击图片预览
|
||||
@@ -1001,6 +1063,14 @@ Page<IPageData, IPageInstance>({
|
||||
if (currentSentence.id) {
|
||||
this.getStandardVoice(currentSentence.id)
|
||||
}
|
||||
|
||||
try {
|
||||
wx.nextTick(() => {
|
||||
this.computeHighlightLayout()
|
||||
})
|
||||
} catch (e) {
|
||||
setTimeout(() => this.computeHighlightLayout(), 0)
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options: Record<string, string>) {
|
||||
@@ -1160,6 +1230,12 @@ Page<IPageData, IPageInstance>({
|
||||
|
||||
recorderManager.onError((res) => {
|
||||
wx.showToast({ title: '录音失败', icon: 'none' })
|
||||
// 录音出错时淡出高亮层
|
||||
this.setData({ overlayVisible: false })
|
||||
const timer = setTimeout(() => {
|
||||
this.setData({ highlightShow: false, highlightZoom: false, focusTransform: '', highlightWords: [] })
|
||||
clearTimeout(timer)
|
||||
}, 320)
|
||||
})
|
||||
|
||||
recorderHandlersBound = true
|
||||
@@ -1525,7 +1601,90 @@ Page<IPageData, IPageInstance>({
|
||||
const timer = setTimeout(() => {
|
||||
this.setData({ isMoreMenuClosing: false })
|
||||
clearTimeout(timer)
|
||||
}, 360)
|
||||
}, 360)
|
||||
},
|
||||
|
||||
computeHighlightLayout() {
|
||||
const { processedSentences, selectedSentenceIndex } = this.data
|
||||
if (!processedSentences || processedSentences.length === 0) return
|
||||
const sentence = processedSentences[selectedSentenceIndex]
|
||||
const words = sentence?.words || []
|
||||
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 = 2.5
|
||||
|
||||
const centerX = windowWidth / 2
|
||||
const centerY = (windowHeight - bottomOffset) / 2
|
||||
|
||||
const query = wx.createSelectorQuery().in(this as any)
|
||||
query.selectAll('.sentence-wrapper.selected .sentence-text').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, i) => {
|
||||
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 })
|
||||
})
|
||||
},
|
||||
|
||||
// 返回上一个单词的查询结果
|
||||
@@ -1550,4 +1709,4 @@ Page<IPageData, IPageInstance>({
|
||||
// 查询上一个单词
|
||||
this.handleWordClick(event);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -29,6 +29,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="highlight-area {{overlayVisible ? 'show' : ''}}">
|
||||
<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) : ''}}">
|
||||
<text class="overlay-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="bottom-button-area">
|
||||
<view wx:if="{{isMoreMenuOpen}}" class="more-menu-modal"></view>
|
||||
@@ -38,7 +45,7 @@
|
||||
<t-icon name="translate" class="trans-button left-half {{transDisplayMode === 'en_ipa' ? 'trans-active' : 'trans-deactive'}}" size="48rpx" />
|
||||
<t-icon name="translate" class="trans-button right-half {{transDisplayMode === 'en_zh' ? 'trans-active' : 'trans-deactive'}}" size="48rpx" />
|
||||
</view>
|
||||
<view class="bottom-button mic-wrap">
|
||||
<view class="bottom-button mic-wrap" bindtap="onMicHighlight">
|
||||
<t-icon name="microphone-1" color="{{isRecording ? '#FFFFFF' : '#333333'}}" class="microphone {{isRecording ? 'recording' : 'bottom-button'}}" size="48rpx" bind:longpress="handleRecordStart" bind:touchend="handleRecordEnd" bind:touchcancel="handleRecordEnd" />
|
||||
<view wx:if="{{isRecording}}" class="mic">
|
||||
<view class="mic-shadow"></view>
|
||||
|
||||
@@ -546,6 +546,33 @@
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
/* 高亮遮罩与单词浮层 */
|
||||
.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;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.highlight-area.show { opacity: 1; }
|
||||
.overlay-word {
|
||||
position: fixed;
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
line-height: 32rpx;
|
||||
opacity: 0;
|
||||
transition: opacity 500ms ease;
|
||||
}
|
||||
.overlay-word .overlay-text { color: #ffffff; }
|
||||
.overlay-word.show { opacity: 1; }
|
||||
.overlay-word.show .overlay-text { font-weight: 600; }
|
||||
.overlay-word.zoom { transform-origin: center center; transition: transform 500ms ease; }
|
||||
|
||||
.bottom-button {
|
||||
padding: 20rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
Reference in New Issue
Block a user