fix assessment
This commit is contained in:
@@ -130,6 +130,9 @@ interface IPageData {
|
||||
isWordEmptyResult: boolean,
|
||||
recordPermissionGranted: boolean
|
||||
loadingMaskVisible: boolean,
|
||||
imageSectionHeight: number,
|
||||
sentenceMinHeight: number,
|
||||
sentenceMarginTop: number,
|
||||
standardAudioLocalMap: { [key: string]: string }, // 标准语音文件ID映射
|
||||
assessmentAudioLocalMap: { [key: string]: string } // 评估语音文件ID映射
|
||||
}
|
||||
@@ -180,13 +183,24 @@ interface IPageInstance extends IPageMethods {
|
||||
// 新增:录音时长(毫秒),用于避免 setData 异步导致 onStop 判断失败
|
||||
lastRecordDurationMs?: number
|
||||
circleAnimTimer?: number
|
||||
sentenceTouchStartY?: number
|
||||
sentencePullTriggered?: boolean
|
||||
sentenceBaseLayouts?: { imageSectionHeight: number; sentenceMinHeight: number; sentenceMarginTop: number }
|
||||
}
|
||||
|
||||
Page<IPageData, IPageInstance>({
|
||||
data: {
|
||||
imagePath: '', // 图片路径
|
||||
imageSmall: false, // 图片是否缩小
|
||||
imageMode: 'widthFix', // 初始完整展示模式
|
||||
imageMode: 'aspectFill', // 初始完整展示模式
|
||||
imageScale: 1,
|
||||
imageTranslateY: 0,
|
||||
imageSectionHeight: 0,
|
||||
sentenceMinHeight: 0,
|
||||
sentenceMarginTop: 0,
|
||||
dragSentenceTranslateY: 0,
|
||||
introStarted: false,
|
||||
buttonsVisible: false,
|
||||
currentSentence: '', // 当前显示的例句
|
||||
sentences: [], // 例句列表
|
||||
currentIndex: 0, // 当前例句索引
|
||||
@@ -275,6 +289,18 @@ Page<IPageData, IPageInstance>({
|
||||
loadingMaskVisible: false,
|
||||
},
|
||||
|
||||
onImageLoaded() {
|
||||
this.setData({ introStarted: true })
|
||||
setTimeout(() => { this.setData({ buttonsVisible: true }) }, 600)
|
||||
},
|
||||
|
||||
onBgTouchMove(e: any) {
|
||||
const y = (e && e.touches && e.touches[0] && typeof e.touches[0].clientY === 'number') ? e.touches[0].clientY : 0
|
||||
const delta = Math.max(0, 200 - y)
|
||||
const scale = Math.min(1.1, 1 + delta / 2000)
|
||||
this.setData({ imageScale: scale })
|
||||
},
|
||||
|
||||
onMicHighlight() {
|
||||
// const pre = this.data.cachedHighlightWords || []
|
||||
// const cachedIdx = typeof this.data.cachedSentenceIndex === 'number' ? this.data.cachedSentenceIndex : -1
|
||||
@@ -1112,12 +1138,13 @@ Page<IPageData, IPageInstance>({
|
||||
|
||||
onLoad(options: Record<string, string>) {
|
||||
this.ensureRecordPermission()
|
||||
try { this.computeDynamicLayout() } catch (e) {}
|
||||
// 如果图片ID,调用接口获取文本和评分信息
|
||||
if (options.index) {
|
||||
this.setData({ currentIndex: Number(options.index) })
|
||||
}
|
||||
if (options.imageId) {
|
||||
wx.showLoading({ title: '加载中...' })
|
||||
// wx.showLoading({ title: '加载中...' })
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
apiManager.getImageTextInit(options.imageId)
|
||||
.then(res => {
|
||||
@@ -1616,24 +1643,83 @@ Page<IPageData, IPageInstance>({
|
||||
},
|
||||
|
||||
onPageScroll(e: any) {
|
||||
if (!e || typeof e.scrollTop !== 'number') {
|
||||
// 忽略无效滚动事件,避免误判为 0 导致回到大图
|
||||
return
|
||||
if (!e || typeof e.scrollTop !== 'number') return
|
||||
const t = e.scrollTop
|
||||
const scale = Math.min(1.18, 1 + t / 900)
|
||||
const translate = Math.min(60, t / 15)
|
||||
this.setData({ imageScale: scale, imageTranslateY: translate })
|
||||
},
|
||||
|
||||
computeDynamicLayout() {
|
||||
try {
|
||||
const si = wx.getSystemInfoSync()
|
||||
const wh = si.windowHeight
|
||||
const ww = si.windowWidth
|
||||
const rpx = ww / 750
|
||||
const isTall = (wh / ww) > 1.8
|
||||
const ratio = isTall ? 0.6 : 0.52
|
||||
const imageH = Math.floor(wh * ratio)
|
||||
const bottomMinRpx = 100
|
||||
const bottomPadRpx = 40
|
||||
const bottomPx = Math.floor((bottomMinRpx + bottomPadRpx) * rpx)
|
||||
const overlapRpx = 40
|
||||
const overlapPx = Math.floor(overlapRpx * rpx)
|
||||
const sentenceMin = Math.max(80, Math.floor(wh - imageH - bottomPx))
|
||||
const sentenceTop = Math.max(0, imageH - overlapPx)
|
||||
this.setData({ imageSectionHeight: imageH, sentenceMinHeight: sentenceMin, sentenceMarginTop: sentenceTop })
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onSentenceTouchStart(e: any) {
|
||||
const y = (e && e.touches && e.touches[0] && typeof e.touches[0].clientY === 'number') ? e.touches[0].clientY : 0
|
||||
this.sentenceTouchStartY = y
|
||||
this.sentencePullTriggered = false
|
||||
this.sentenceBaseLayouts = {
|
||||
imageSectionHeight: this.data.imageSectionHeight || 0,
|
||||
sentenceMinHeight: this.data.sentenceMinHeight || 0,
|
||||
sentenceMarginTop: this.data.sentenceMarginTop || 0
|
||||
}
|
||||
const scrollTop = e.scrollTop
|
||||
const shrinkThreshold = 120
|
||||
// 一旦缩小,保持小图;不再在下滑时恢复为大图
|
||||
if (!this.data.imageSmall && scrollTop >= shrinkThreshold) {
|
||||
this.setData({
|
||||
imageSmall: true,
|
||||
imageMode: 'aspectFit'
|
||||
})
|
||||
},
|
||||
|
||||
onSentenceTouchMove(e: any) {
|
||||
const startY = this.sentenceTouchStartY || 0
|
||||
const y = (e && e.touches && e.touches[0] && typeof e.touches[0].clientY === 'number') ? e.touches[0].clientY : 0
|
||||
const delta = y - startY
|
||||
// this.sentenceDragDelta = delta
|
||||
const base = this.sentenceBaseLayouts || {
|
||||
imageSectionHeight: this.data.imageSectionHeight || 0,
|
||||
sentenceMinHeight: this.data.sentenceMinHeight || 0,
|
||||
sentenceMarginTop: this.data.sentenceMarginTop || 0
|
||||
}
|
||||
const minSentence = 80
|
||||
const expandCap = Math.max(0, (base.sentenceMinHeight || 0) - minSentence)
|
||||
const contractMinImage = Math.floor((base.imageSectionHeight || 0) * 0.4)
|
||||
const contractCap = Math.max(0, (base.imageSectionHeight || 0) - contractMinImage)
|
||||
|
||||
const down = Math.min(Math.max(delta, 0), expandCap)
|
||||
const up = Math.min(Math.max(-delta, 0), contractCap)
|
||||
|
||||
if (down > 0 && up === 0) {
|
||||
const translateSentence = down
|
||||
this.setData({ dragSentenceTranslateY: translateSentence })
|
||||
}
|
||||
|
||||
// try { if ((this as any)._layoutDebounceTimer) clearTimeout((this as any)._layoutDebounceTimer) } catch (e) {}
|
||||
// ;(this as any)._layoutDebounceTimer = setTimeout(() => {
|
||||
// this.computeHighlightLayout()
|
||||
// }, 100)
|
||||
if (up > 0 && down === 0) {
|
||||
const translateSentence = -up
|
||||
this.setData({ dragSentenceTranslateY: translateSentence })
|
||||
}
|
||||
|
||||
const previewThreshold = expandCap + 40
|
||||
if (down > previewThreshold && !this.sentencePullTriggered && this.data.imagePath) {
|
||||
this.sentencePullTriggered = true
|
||||
try { this.handleImagePreview() } catch (err) {}
|
||||
}
|
||||
},
|
||||
|
||||
onSentenceTouchEnd() {
|
||||
this.sentenceTouchStartY = 0
|
||||
this.sentencePullTriggered = false
|
||||
// 保持当前的 transform,不重置高度或缩放
|
||||
},
|
||||
|
||||
// 处理句子数据,分割单词和音标
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<!-- assessment.wxml - 评估页面 -->
|
||||
<view class="assessment-container">
|
||||
<!-- 顶部图片区域 -->
|
||||
<view class="image-section" bindtap="handleImagePreview">
|
||||
<t-skeleton wx:if="{{!imagePath}}" class="assessment-image {{imageSmall ? 'small' : ''}}" theme="image" animation="gradient" loading="{{true}}"></t-skeleton>
|
||||
<image wx:else class="assessment-image {{imageSmall ? 'small' : ''}}" src="{{imagePath}}" mode="{{imageMode}}" />
|
||||
<view class="loading-container" wx:if="{{!imagePath}}">
|
||||
<t-skeleton row-col="{{[{ size: '400rpx', borderRadius: '24rpx' }, 1, 1, 1]}}" loading></t-skeleton>
|
||||
</view>
|
||||
<!-- 中间例句区域 -->
|
||||
<view class="sentence-section">
|
||||
<view class="image-section" style="height: {{imageSectionHeight}}px;" bindtap="handleImagePreview">
|
||||
<view class="bg-image-wrap {{introStarted ? 'intro-image' : ''}}" style="transform: translateY(-{{imageTranslateY}}px); scale:{{imageScale}};">
|
||||
<image wx:if="{{imagePath}}" class="bg-image" src="{{imagePath}}" mode="{{imageMode}}" bindload="onImageLoaded" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="sentence-section {{introStarted ? 'intro-sentence' : ''}}" style="min-height: {{sentenceMinHeight}}px; margin-top: {{sentenceMarginTop}}px; transform: translateY({{dragSentenceTranslateY}}px);" bindtouchstart="onSentenceTouchStart" bindtouchmove="onSentenceTouchMove" bindtouchend="onSentenceTouchEnd">
|
||||
<view class="sentence-container">
|
||||
<!-- 例句内容 -->
|
||||
<view class="sentence-content">
|
||||
<view class="sentence-wrapper {{selectedSentenceIndex === index ? 'selected' : ''}}" wx:for="{{processedSentences}}" wx:key="index" data-index="{{index}}" bindtap="handleSentenceSelect">
|
||||
<view class="sentence-content-wrapper">
|
||||
@@ -38,8 +39,7 @@
|
||||
</view>
|
||||
<view wx:if="{{isRecording}}" class="recording-mask" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop"></view>
|
||||
<view wx:if="{{loadingMaskVisible}}" class="page-loading-mask" catchtap="noop" catchtouchstart="noop" catchtouchmove="noop" catchtouchend="noop"></view>
|
||||
<!-- 底部按钮区域 -->
|
||||
<view class="bottom-button-area">
|
||||
<view class="bottom-button-area {{buttonsVisible ? 'show' : ''}}">
|
||||
<view wx:if="{{isMoreMenuOpen}}" class="more-menu-modal"></view>
|
||||
<view class="button-row">
|
||||
<t-icon name="{{isPlaying ? 'pause' : 'play'}}" class="bottom-button {{isRecording ? 'disabled' : ''}}" size="48rpx" bind:tap="playStandardVoice" />
|
||||
|
||||
@@ -8,36 +8,59 @@
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* 顶部图片区域 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.image-section {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50vh;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.assessment-image {
|
||||
display: block;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
margin: 40rpx auto 0;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
|
||||
transition: width 500ms ease, height 500ms ease, margin 500ms ease, border-radius 500ms ease;
|
||||
.bg-image-wrap {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 80ms ease-out;
|
||||
transform-origin: center top;
|
||||
pointer-events: none;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.assessment-image.small {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin: 20rpx auto 0; /* 缩小时减少上边距,视觉更紧凑 */
|
||||
.bg-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 中间例句区域 */
|
||||
.sentence-section {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 500rpx);
|
||||
min-height: calc(100vh - 50vh);
|
||||
padding-bottom: calc(0px + env(safe-area-inset-bottom));
|
||||
background: #ffffff;
|
||||
margin-top: calc(50vh - 40rpx);
|
||||
border-top-left-radius: 24rpx;
|
||||
border-top-right-radius: 24rpx;
|
||||
box-shadow: 0 -24rpx 24rpx 0 rgba(0,0,0,0.2);
|
||||
will-change: transform, opacity;
|
||||
opacity: 0;
|
||||
transition: transform 120ms ease-out, opacity 120ms ease-out;
|
||||
}
|
||||
|
||||
.sentence-container {
|
||||
@@ -1341,7 +1364,6 @@
|
||||
|
||||
.bottom-button-area {
|
||||
position: fixed;
|
||||
bottom: calc(0px + constant(safe-area-inset-bottom));
|
||||
bottom: calc(0px + env(safe-area-inset-bottom));
|
||||
left: 0;
|
||||
width: 100%;
|
||||
@@ -1522,3 +1544,28 @@
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.intro-image {
|
||||
animation: slideDownFade 600ms ease forwards;
|
||||
}
|
||||
|
||||
.intro-sentence {
|
||||
animation: slideUpFade 600ms ease forwards;
|
||||
}
|
||||
|
||||
@keyframes slideDownFade {
|
||||
0% { transform: translateY(-10%) scale(1); opacity: 0; }
|
||||
100% { transform: translateY(0) scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideUpFade {
|
||||
0% { transform: translateY(10%); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.bottom-button-area {
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
}
|
||||
.bottom-button-area.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -810,7 +810,7 @@ Page({
|
||||
// const compressedImagePath = await imageManager.compressImage(this.data.imagePath, {
|
||||
// quality: 80,
|
||||
// maxWidth: 1200,
|
||||
// maxHeight: 1200
|
||||
// maxHeight: 1920
|
||||
// })
|
||||
|
||||
// const result = await apiManager.uploadImage(compressedImagePath)
|
||||
|
||||
@@ -967,6 +967,9 @@ class ApiManager {
|
||||
|
||||
// 第一步:上传文件获取ID
|
||||
const fileId = await this.uploadImageFile(filePath)
|
||||
try {
|
||||
await this.cacheLocalImage(fileId, filePath)
|
||||
} catch (e) {}
|
||||
|
||||
// 更新加载提示
|
||||
// wx.showLoading({ title: '识别中...' })
|
||||
@@ -994,6 +997,8 @@ class ApiManager {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
try { this.refreshImageCache(fileId) } catch (e) {}
|
||||
|
||||
// wx.hideLoading()
|
||||
console.log('图片上传和识别流程完成')
|
||||
@@ -1026,6 +1031,59 @@ class ApiManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async cacheLocalImage(fileId: string, localPath: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager()
|
||||
const extMatch = localPath && localPath.match(/\.[a-zA-Z0-9]+$/)
|
||||
if (!extMatch) {
|
||||
imageCache.set(fileId, localPath)
|
||||
resolve(localPath)
|
||||
return
|
||||
}
|
||||
const ext = extMatch[0]
|
||||
const destPath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||||
try { fs.unlinkSync(destPath) } catch (e) {}
|
||||
fs.saveFile({
|
||||
tempFilePath: localPath,
|
||||
filePath: destPath,
|
||||
success: () => {
|
||||
imageCache.set(fileId, destPath)
|
||||
fs.getFileInfo({
|
||||
filePath: destPath,
|
||||
success: (infoRes) => {
|
||||
cacheStats.set(`image_${fileId}`, infoRes.size)
|
||||
resolve(destPath)
|
||||
},
|
||||
fail: () => resolve(destPath)
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
imageCache.set(fileId, localPath)
|
||||
resolve(localPath)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
imageCache.set(fileId, localPath)
|
||||
resolve(localPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async refreshImageCache(fileId: string): Promise<void> {
|
||||
try {
|
||||
const path = await this.downloadFile(fileId)
|
||||
imageCache.set(fileId, path)
|
||||
const fs = wx.getFileSystemManager()
|
||||
fs.getFileInfo({
|
||||
filePath: path,
|
||||
success: (infoRes) => {
|
||||
cacheStats.set(`image_${fileId}`, infoRes.size)
|
||||
}
|
||||
})
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 获取单词详情
|
||||
async getWordDetail(word: string): Promise<YdWordDetail> {
|
||||
console.log('获取单词详情')
|
||||
@@ -1623,6 +1681,7 @@ class ApiManager {
|
||||
}
|
||||
const ext = extMatch[0]
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||||
try { fs.unlinkSync(filePath) } catch (e) {}
|
||||
fs.saveFile({
|
||||
tempFilePath: tempPath,
|
||||
filePath,
|
||||
@@ -1657,6 +1716,7 @@ class ApiManager {
|
||||
}
|
||||
const ext = extMatch[0]
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/${fileId}${ext}`
|
||||
try { fs.unlinkSync(filePath) } catch (e) {}
|
||||
fs.saveFile({
|
||||
tempFilePath: tempPath,
|
||||
filePath,
|
||||
|
||||
@@ -2,7 +2,7 @@ const accountInfo = wx.getAccountInfoSync()
|
||||
const envVersion = accountInfo?.miniProgram?.envVersion || 'release'
|
||||
|
||||
const BASE_URL_MAP: Record<string, string> = {
|
||||
develop: 'http://localhost:8080',
|
||||
develop: 'https://app.xhzone.cn',
|
||||
trial: 'http://guzjwuna.prod.ihnm9taa.e13h9xq5.com',
|
||||
release: 'http://guzjwuna.prod.ihnm9taa.e13h9xq5.com'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user