fix assessment

This commit is contained in:
Felix
2025-12-09 13:32:43 +08:00
parent 54da7d0a04
commit 0e45ccf09a
6 changed files with 238 additions and 45 deletions

View File

@@ -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不重置高度或缩放
},
// 处理句子数据,分割单词和音标

View File

@@ -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" />

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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'
}