add subscribe
This commit is contained in:
@@ -82,7 +82,7 @@ Component({
|
||||
const hasPastExam = !!(detail['individual'] && detail['individual'].pastExamSents && detail['individual'].pastExamSents.length > 0)
|
||||
const defaultTab = hasCollins ? '0' : (hasPhrs ? '1' : (hasPastExam ? '2' : '3'))
|
||||
const protoTemp = detail.ee?.word?.['return-phrase']?.['l']?.['i'] || ''
|
||||
const proto = protoTemp != '' && protoTemp != word ? protoTemp : (detail?.ec?.word?.[0]?.prototype || '')
|
||||
const proto = protoTemp != '' && protoTemp.toLowerCase() != word.toLowerCase() ? protoTemp : (detail?.ec?.word?.[0]?.prototype || '')
|
||||
const hideProto = !!self.data.forceHidePrototype
|
||||
self.setData({
|
||||
wordDict: {
|
||||
|
||||
@@ -1387,7 +1387,9 @@ Page<IPageData, IPageInstance>({
|
||||
})
|
||||
|
||||
recorderManager.onError((res) => {
|
||||
wx.showToast({ title: '录音失败', icon: 'none' })
|
||||
logger.error('录音失败', res)
|
||||
const msg = (res as any)?.errMsg || '录音失败'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
// 录音出错时淡出高亮层
|
||||
this.setData({ overlayVisible: false, isRecording: false })
|
||||
const timer = setTimeout(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import logger from '../../utils/logger'
|
||||
Page({
|
||||
data: {
|
||||
products: [] as Array<any>,
|
||||
plans: [] as Array<any>,
|
||||
userPoints: 0,
|
||||
displayUserPoints: '0',
|
||||
vipLevel: 0
|
||||
@@ -11,11 +12,20 @@ Page({
|
||||
async onLoad(options: Record<string, string>) {
|
||||
logger.info('Coupon page loaded')
|
||||
try {
|
||||
const list = await apiManager.getProductList()
|
||||
const products = (list || []).map((p: any) => ({
|
||||
const [productList, planList] = await Promise.all([
|
||||
apiManager.getProductList(),
|
||||
apiManager.getSubscriptionPlans()
|
||||
])
|
||||
|
||||
const products = (productList || []).map((p: any) => ({
|
||||
...p,
|
||||
amountYuan: ((p.amount_cents || 0) / 100).toFixed(2)
|
||||
}))
|
||||
|
||||
const plans = (planList || []).map((p: any) => ({
|
||||
...p,
|
||||
priceYuan: ((p.price || 0) / 100).toFixed(2)
|
||||
}))
|
||||
const ptsStr = options?.points || '0'
|
||||
const lvlStr = options?.vipLevel || '0'
|
||||
const ptsNum = Number(ptsStr) || 0
|
||||
@@ -26,6 +36,7 @@ Page({
|
||||
}
|
||||
this.setData({
|
||||
products,
|
||||
plans,
|
||||
userPoints: ptsNum,
|
||||
displayUserPoints: fmt(ptsNum),
|
||||
vipLevel: lvlNum
|
||||
@@ -35,6 +46,59 @@ Page({
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handlePlanTap(e: any) {
|
||||
try {
|
||||
const productId = e?.currentTarget?.dataset?.id
|
||||
if (!productId) {
|
||||
wx.showToast({ title: '订阅商品信息错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const order = await apiManager.createSubscriptionOrder(String(productId))
|
||||
const prepayId = order?.prepay_id
|
||||
const outTradeNo = order?.out_trade_no
|
||||
if (!prepayId) {
|
||||
wx.showToast({ title: '下单失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const pkg = `prepay_id=${prepayId}`
|
||||
const timeStamp = order?.timeStamp
|
||||
const nonceStr = order?.nonceStr
|
||||
const paySign = order?.paySign
|
||||
const signType = (order?.signType || 'RSA') as any
|
||||
|
||||
if (timeStamp && nonceStr && paySign) {
|
||||
wx.requestPayment({
|
||||
timeStamp,
|
||||
nonceStr,
|
||||
package: pkg,
|
||||
signType,
|
||||
paySign,
|
||||
success: async () => {
|
||||
if (outTradeNo) {
|
||||
await this.queryPaymentWithRetry(outTradeNo)
|
||||
} else {
|
||||
wx.showToast({ title: '订阅成功', icon: 'success' })
|
||||
}
|
||||
},
|
||||
fail: async (err) => {
|
||||
logger.error('订阅支付失败', err)
|
||||
if (outTradeNo) {
|
||||
await this.queryPaymentWithRetry(outTradeNo)
|
||||
} else {
|
||||
wx.showToast({ title: '订阅支付失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '缺少支付签名参数', icon: 'none' })
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('发起订阅支付失败', error)
|
||||
wx.showToast({ title: '发起订阅支付失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
async handleCouponTap(e: any) {
|
||||
try {
|
||||
const productId = e?.currentTarget?.dataset?.id
|
||||
@@ -55,7 +119,7 @@ Page({
|
||||
const timeStamp = order?.timeStamp
|
||||
const nonceStr = order?.nonceStr
|
||||
const paySign = order?.paySign
|
||||
const signType = order?.signType || 'RSA'
|
||||
const signType = (order?.signType || 'RSA') as any
|
||||
|
||||
if (timeStamp && nonceStr && paySign) {
|
||||
wx.requestPayment({
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
<!--coupon.wxml-->
|
||||
<view class="coupon-container">
|
||||
<view class="coupon_title">
|
||||
订阅计划
|
||||
</view>
|
||||
<view class="card-box">
|
||||
<view class="card" wx:for="{{plans}}" wx:key="id" wx:for-item="plan" bindtap="handlePlanTap" data-id="{{plan.id}}">
|
||||
<view class="card-title">{{plan.name}}</view>
|
||||
<view class="card-points">{{plan.features && plan.features.label ? plan.features.label : ''}}</view>
|
||||
<view class="card-credits">{{plan.features && plan.features.extra ? plan.features.extra : ''}}</view>
|
||||
<view class="card-price">¥{{plan.priceYuan}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon_title">
|
||||
获取更多积分
|
||||
</view>
|
||||
<!-- <view class='coupon_box {{item.one_time ? "one_time" : ""}}' wx:for="{{products}}" wx:key="id" wx:for-item="item">
|
||||
<view class='content' bindtap="handleCouponTap" data-id="{{item.id}}" data-points="{{item.points}}">
|
||||
<view class='title'>{{item.title}}</view>
|
||||
<view class='how_much'>{{item.points}}</view>
|
||||
</view>
|
||||
<view class='btn'> ¥{{item.amountYuan}}</view>
|
||||
</view> -->
|
||||
|
||||
<view class="card-box">
|
||||
<view class="card" wx:for="{{products}}" wx:key="id" wx:for-item="item" bindtap="handleCouponTap" data-id="{{item.id}}" data-points="{{item.points}}">
|
||||
<view class="card-title">{{item.title}}</view>
|
||||
@@ -26,6 +29,7 @@
|
||||
<view class="tips-title">积分说明</view>
|
||||
</view>
|
||||
<view class="tips-list">
|
||||
<view class="tips-item">订阅期间不消耗积分。</view>
|
||||
<view class="tips-item">积分购买后永久有效。</view>
|
||||
<!-- <view class="tips-item"></view> -->
|
||||
</view>
|
||||
|
||||
@@ -23,7 +23,9 @@ Page({
|
||||
frozen_balance: 0,
|
||||
total_purchased: 0,
|
||||
total_refunded: 0,
|
||||
expired_time: ''
|
||||
expired_time: '',
|
||||
is_subscribed: false,
|
||||
subscription_expires_at: null as string | null
|
||||
},
|
||||
|
||||
// 兑换码弹窗显示控制
|
||||
@@ -316,7 +318,7 @@ Page({
|
||||
const pointsData = await apiManager.getPointsData()
|
||||
|
||||
// 如果返回的数据为null,则设置默认值
|
||||
const finalPointsData = pointsData || { balance: 0, available_balance: 0, frozen_balance: 0, total_purchased: 0, total_refunded: 0, expired_time: '' }
|
||||
const finalPointsData = pointsData || { balance: 0, available_balance: 0, frozen_balance: 0, total_purchased: 0, total_refunded: 0, expired_time: '', is_subscribed: false, subscription_expires_at: null }
|
||||
|
||||
const endTime = Date.now()
|
||||
logger.info('积分数据加载完成,耗时:', endTime - startTime, 'ms', finalPointsData)
|
||||
@@ -331,12 +333,31 @@ Page({
|
||||
frozen_balance: 0,
|
||||
total_purchased: 0,
|
||||
total_refunded: 0,
|
||||
expired_time: ''
|
||||
expired_time: '',
|
||||
is_subscribed: false,
|
||||
subscription_expires_at: null as string | null
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateStr: string | null) {
|
||||
if (!dateStr) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(dateStr)
|
||||
const time = date.getTime()
|
||||
if (isNaN(time)) {
|
||||
return dateStr || ''
|
||||
}
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const mm = month < 10 ? '0' + month : '' + month
|
||||
const dd = day < 10 ? '0' + day : '' + day
|
||||
return `${year}-${mm}-${dd}`
|
||||
},
|
||||
|
||||
// 加载缓存统计信息
|
||||
loadCacheStats() {
|
||||
return new Promise<void>((resolve) => {
|
||||
|
||||
@@ -69,11 +69,11 @@
|
||||
maxcharacter="{{6}}"
|
||||
/>
|
||||
</t-dialog>
|
||||
<navigator url="/pages/coupon/coupon?points={{points.available_balance}}" class="cell-navigator">
|
||||
<t-cell title="积分" hover note="{{points.available_balance}}">
|
||||
<t-icon slot="left-icon" name="star" size="44rpx"></t-icon>
|
||||
</t-cell>
|
||||
</navigator>
|
||||
<navigator url="/pages/coupon/coupon?points={{points.available_balance}}" class="cell-navigator">
|
||||
<t-cell title="{{points.is_subscribed ? '订阅到期时间' : '积分'}}" hover note="{{points.is_subscribed ? points.subscription_expires_at : points.available_balance}}">
|
||||
<t-icon slot="left-icon" name="star" size="44rpx"></t-icon>
|
||||
</t-cell>
|
||||
</navigator>
|
||||
<navigator url="/pages/order/order" class="cell-navigator">
|
||||
<t-cell title="订单记录" hover arrow>
|
||||
<t-icon slot="left-icon" name="shop" size="44rpx"></t-icon>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"navigationBarTitleText": "问答练习",
|
||||
"navigationBarTitleText": "场景练习",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#ffffff",
|
||||
|
||||
@@ -5,13 +5,13 @@ import { IQaExerciseItem, IQaExerciseSession, IAppOption } from '../../types/app
|
||||
export const QUESTION_MODES = {
|
||||
CLOZE: 'cloze',
|
||||
CHOICE: 'choice',
|
||||
FREE_TEXT: 'free_text',
|
||||
VARIATION: 'variation'
|
||||
VARIATION: 'variation',
|
||||
CONVERSATION: 'conversation'
|
||||
} as const
|
||||
|
||||
export type QuestionMode = typeof QUESTION_MODES[keyof typeof QUESTION_MODES]
|
||||
|
||||
export const ENABLED_QUESTION_MODES: QuestionMode[] = [QUESTION_MODES.CHOICE, QUESTION_MODES.CLOZE, QUESTION_MODES.VARIATION]
|
||||
export const ENABLED_QUESTION_MODES: QuestionMode[] = [QUESTION_MODES.CHOICE, QUESTION_MODES.CLOZE, QUESTION_MODES.VARIATION, QUESTION_MODES.CONVERSATION]
|
||||
|
||||
interface IData {
|
||||
loadingMaskVisible: boolean
|
||||
@@ -42,7 +42,6 @@ interface IData {
|
||||
selectedClozeIndex?: number
|
||||
clozeParts?: string[]
|
||||
clozeSentenceTokens?: Array<{ text: string; word?: string; isBlank?: boolean }>
|
||||
freeTextInput: string
|
||||
imageLocalUrl?: string
|
||||
imageLoaded?: boolean
|
||||
modeAnim?: '' | 'fade-in' | 'fade-out'
|
||||
@@ -97,16 +96,20 @@ interface IData {
|
||||
variationSubmitted?: boolean
|
||||
variationResultStatus?: 'correct' | 'incorrect'
|
||||
variationExerciseId?: string
|
||||
}
|
||||
conversationSetting?: any
|
||||
}
|
||||
|
||||
interface IPageInstance {
|
||||
pollTimer?: number
|
||||
variationPollTimer?: number
|
||||
conversationPollTimer?: number
|
||||
audioCtx?: WechatMiniprogram.InnerAudioContext
|
||||
fetchQaExercises: (imageId: string, referrerId?: string) => Promise<void>
|
||||
fetchVariationExercises: (imageId: string) => Promise<void>
|
||||
fetchConversationSetting: (imageId: string) => Promise<void>
|
||||
startPolling: (taskId: string, imageId: string) => void
|
||||
startVariationPolling: (taskId: string, imageId: string) => void
|
||||
startConversationInitPolling: (taskId: string, imageId: string) => void
|
||||
initExerciseContent: (exercise: any, session?: IQaExerciseSession) => void
|
||||
updateActionButtonsState: () => void
|
||||
shuffleArray: <T>(arr: T[]) => T[]
|
||||
@@ -119,7 +122,6 @@ interface IPageInstance {
|
||||
onRetryTap: () => void
|
||||
ensureQuestionResultFetched: (questionId: string) => Promise<void>
|
||||
applyCachedResultForQuestion: (questionId: string) => void
|
||||
inputChange: (e: any) => void
|
||||
previewImage: () => void
|
||||
submitAttempt: () => Promise<void>
|
||||
onPrevTap: () => void
|
||||
@@ -138,7 +140,6 @@ interface IPageInstance {
|
||||
onTabsChange: (e: any) => void
|
||||
onTabsClick: (e: any) => void
|
||||
handleBackToPreviousWord: () => void
|
||||
onHintTap: () => Promise<void>
|
||||
onOptionLongPress: (e: any) => void
|
||||
onImageLoad: () => void
|
||||
onImageError: () => void
|
||||
@@ -152,13 +153,14 @@ interface IPageInstance {
|
||||
resetConfetti: () => void
|
||||
loadVariationImages: (list: any[]) => void
|
||||
onVariationImageLoad: (e: any) => void
|
||||
previewVariationImage: (e: any) => void
|
||||
}
|
||||
|
||||
Page<IData, IPageInstance>({
|
||||
data: {
|
||||
loadingMaskVisible: false,
|
||||
imageId: '',
|
||||
statusText: '加载中...',
|
||||
statusText: '生成练习中...',
|
||||
exercises: [],
|
||||
contentVisible: false,
|
||||
contentReady: false,
|
||||
@@ -176,7 +178,6 @@ Page<IData, IPageInstance>({
|
||||
choiceSubmitted: false,
|
||||
evalClasses: [],
|
||||
clozeSentenceTokens: [],
|
||||
freeTextInput: '',
|
||||
modeAnim: '',
|
||||
isModeSwitching: false,
|
||||
progressText: '',
|
||||
@@ -241,8 +242,8 @@ Page<IData, IPageInstance>({
|
||||
const r = cache[qid] || {}
|
||||
const c1 = !!((r.choice || {}).evaluation)
|
||||
const c2 = !!((r.cloze || {}).evaluation)
|
||||
const c3 = !!((r.free_text || {}).evaluation)
|
||||
let depth = Number(c1) + Number(c2) + Number(c3)
|
||||
const c4 = !!((r.variation || {}).evaluation)
|
||||
let depth = Number(c1) + Number(c2) + Number(c4)
|
||||
|
||||
if (depth === 0 && attemptMap.has(qid)) {
|
||||
const att = attemptMap.get(qid)
|
||||
@@ -263,11 +264,11 @@ Page<IData, IPageInstance>({
|
||||
const session = this.data.session
|
||||
|
||||
// 如果是试玩模式,仅检查本地缓存是否有结果
|
||||
if (this.data.isTrialMode) {
|
||||
const allAttempted = list.every((q: any) => {
|
||||
const qid = String(q.id)
|
||||
const r = cache[qid] || {}
|
||||
return !!((r.choice || {}).evaluation) || !!((r.cloze || {}).evaluation) || !!((r.free_text || {}).evaluation)
|
||||
if (this.data.isTrialMode) {
|
||||
const allAttempted = list.every((q: any) => {
|
||||
const qid = String(q.id)
|
||||
const r = cache[qid] || {}
|
||||
return !!((r.choice || {}).evaluation) || !!((r.cloze || {}).evaluation) || !!((r.variation || {}).evaluation)
|
||||
})
|
||||
if (allAttempted) {
|
||||
this.setData({ showCompletionPopup: true })
|
||||
@@ -285,7 +286,7 @@ Page<IData, IPageInstance>({
|
||||
const qid = String(q.id)
|
||||
// Check local cache (latest submission)
|
||||
const r = cache[qid] || {}
|
||||
const hasLocalResult = !!((r.choice || {}).evaluation) || !!((r.cloze || {}).evaluation) || !!((r.free_text || {}).evaluation)
|
||||
const hasLocalResult = !!((r.choice || {}).evaluation) || !!((r.cloze || {}).evaluation) || !!((r.variation || {}).evaluation)
|
||||
// Check session history
|
||||
const hasSessionAttempt = attemptSet.has(qid)
|
||||
return hasLocalResult || hasSessionAttempt
|
||||
@@ -316,9 +317,6 @@ Page<IData, IPageInstance>({
|
||||
} else if (mode === QUESTION_MODES.CLOZE) {
|
||||
submitDisabled = hasResult
|
||||
retryDisabled = !hasResult
|
||||
} else if (mode === QUESTION_MODES.FREE_TEXT) {
|
||||
submitDisabled = false
|
||||
retryDisabled = true
|
||||
} else if (mode === QUESTION_MODES.VARIATION) {
|
||||
const hasSelection = typeof this.data.variationSelectedIndex === 'number' && this.data.variationSelectedIndex >= 0
|
||||
submitDisabled = hasResult || !hasSelection
|
||||
@@ -371,6 +369,16 @@ Page<IData, IPageInstance>({
|
||||
this.setData({ imageLoaded: true })
|
||||
this.updateContentReady()
|
||||
},
|
||||
previewVariationImage(e: any) {
|
||||
logger.log('previewVariationImage', e)
|
||||
const ds = e?.currentTarget?.dataset || {}
|
||||
const fileId = String(ds.fileid || '')
|
||||
if (!fileId) return
|
||||
const map = this.data.variationImages || {}
|
||||
const url = map[fileId]
|
||||
if (!url) return
|
||||
wx.previewImage({ urls: [url] })
|
||||
},
|
||||
onOptionLongPress(e: any) {
|
||||
const ds = e?.currentTarget?.dataset || {}
|
||||
const raw = String(ds.word || '').trim()
|
||||
@@ -393,7 +401,6 @@ Page<IData, IPageInstance>({
|
||||
this.switchQuestion(1)
|
||||
},
|
||||
toggleMode() {
|
||||
// const order: Array<'choice' | 'cloze' | 'free_text'> = ['choice', 'cloze', 'free_text']
|
||||
const order: QuestionMode[] = ENABLED_QUESTION_MODES
|
||||
const i = order.indexOf(this.data.questionMode)
|
||||
const next = order[(i + 1) % order.length]
|
||||
@@ -427,7 +434,7 @@ Page<IData, IPageInstance>({
|
||||
|
||||
const type = options.type as QuestionMode
|
||||
if (type && ENABLED_QUESTION_MODES.includes(type)) {
|
||||
this.setData({ fixedMode: type })
|
||||
this.setData({ fixedMode: type, questionMode: type })
|
||||
}
|
||||
|
||||
// 处理推荐人ID
|
||||
@@ -463,7 +470,7 @@ Page<IData, IPageInstance>({
|
||||
|
||||
const imageId = options?.id || options?.image_id || ''
|
||||
const thumbnailId = options?.thumbnail_id || '' // 兼容旧逻辑,如果是分享进来的可能需要通过 API 获取图片链接
|
||||
this.setData({ imageId, loadingMaskVisible: true, statusText: '加载中...' })
|
||||
this.setData({ imageId, loadingMaskVisible: true, statusText: '生成练习中...' })
|
||||
if (type === QUESTION_MODES.VARIATION) {
|
||||
await this.fetchVariationExercises(imageId)
|
||||
} else {
|
||||
@@ -588,7 +595,7 @@ Page<IData, IPageInstance>({
|
||||
return
|
||||
}
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'scene_basic')
|
||||
this.setData({ taskId: task_id, statusText: '解析中...' })
|
||||
this.setData({ taskId: task_id, statusText: '生成练习中...' })
|
||||
this.startPolling(task_id, imageId)
|
||||
} catch (e) {
|
||||
logger.error('获取练习失败', e)
|
||||
@@ -601,10 +608,9 @@ Page<IData, IPageInstance>({
|
||||
if (!list || list.length === 0) return
|
||||
const loadingMap: Record<string, boolean> = {}
|
||||
|
||||
list.forEach(item => {
|
||||
const fileId = (item.ext || {}).file_id
|
||||
list.forEach((item: any) => {
|
||||
const fileId = String(item.file_id || (item.ext || {}).file_id || '')
|
||||
if (fileId) {
|
||||
// If not already in variationImages, fetch url
|
||||
if (!this.data.variationImages || !this.data.variationImages[fileId]) {
|
||||
loadingMap[fileId] = true
|
||||
}
|
||||
@@ -613,25 +619,24 @@ Page<IData, IPageInstance>({
|
||||
|
||||
if (Object.keys(loadingMap).length === 0) return
|
||||
|
||||
// Update loading state for new items
|
||||
this.setData({
|
||||
variationImagesLoading: {
|
||||
...this.data.variationImagesLoading,
|
||||
...loadingMap
|
||||
}
|
||||
variationImagesLoading: {
|
||||
...this.data.variationImagesLoading,
|
||||
...loadingMap
|
||||
}
|
||||
})
|
||||
|
||||
// Fetch URLs
|
||||
Object.keys(loadingMap).forEach(async (fileId) => {
|
||||
try {
|
||||
const url = await apiManager.getFileDisplayUrl(fileId)
|
||||
this.setData({
|
||||
variationImages: { ...this.data.variationImages, [fileId]: url }
|
||||
variationImages: { ...this.data.variationImages, [fileId]: url },
|
||||
variationImagesLoading: { ...this.data.variationImagesLoading, [fileId]: false }
|
||||
})
|
||||
} catch (err) {
|
||||
logger.error('Failed to get variation image url', err)
|
||||
this.setData({
|
||||
variationImagesLoading: { ...this.data.variationImagesLoading, [fileId]: false }
|
||||
variationImagesLoading: { ...this.data.variationImagesLoading, [fileId]: false }
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -653,6 +658,7 @@ Page<IData, IPageInstance>({
|
||||
if (res && Array.isArray(res.questions) && res.questions.length > 0) {
|
||||
const exercise = res.exercise as any
|
||||
const variationExerciseId = String(exercise?.id || '')
|
||||
const session = res.session as IQaExerciseSession
|
||||
const qaList = (res.questions || []).map((q: any) => ({
|
||||
id: q.id,
|
||||
exercise_id: q.exercise_id,
|
||||
@@ -660,31 +666,72 @@ Page<IData, IPageInstance>({
|
||||
question: q.question,
|
||||
...(q.ext || {})
|
||||
}))
|
||||
|
||||
let idx = 0
|
||||
if (!this.data.isTrialMode && session?.progress && Array.isArray((session.progress as any).attempts)) {
|
||||
const attempts = (session.progress as any).attempts
|
||||
const attemptedIds = new Set(attempts.map((a: any) => String(a.question_id)))
|
||||
const firstUnansweredIndex = qaList.findIndex((q: any) => !attemptedIds.has(String(q.id)))
|
||||
if (firstUnansweredIndex !== -1) {
|
||||
idx = firstUnansweredIndex
|
||||
}
|
||||
}
|
||||
const total = Number((exercise as any)?.question_count || qaList.length || 0)
|
||||
let completed = 0
|
||||
if (typeof (session as any)?.progress === 'number') {
|
||||
completed = (session as any).progress
|
||||
} else if (session?.progress && typeof (session.progress as any).answered === 'number') {
|
||||
completed = (session.progress as any).answered
|
||||
}
|
||||
const progressText = `已完成 ${completed} / ${total}`
|
||||
this.setData({ session, progressText })
|
||||
logger.log('Variation exercises loaded:', qaList.length)
|
||||
this.setData({
|
||||
variationQaList: qaList,
|
||||
variationExerciseId,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '加载完成',
|
||||
qaList: qaList,
|
||||
currentIndex: 0,
|
||||
questionMode: 'variation' as QuestionMode
|
||||
variationQaList: qaList,
|
||||
variationExerciseId,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '加载完成',
|
||||
qaList: qaList,
|
||||
currentIndex: idx,
|
||||
questionMode: 'variation' as QuestionMode
|
||||
})
|
||||
this.loadVariationImages(qaList)
|
||||
this.switchQuestion(0)
|
||||
return
|
||||
}
|
||||
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'scene_variation')
|
||||
this.setData({ variationTaskId: task_id, statusText: '生成变化练习中...' })
|
||||
this.setData({ variationTaskId: task_id, statusText: '生成练习中...' })
|
||||
this.startVariationPolling(task_id, imageId)
|
||||
|
||||
} catch (e) {
|
||||
logger.error('获取变化练习失败', e)
|
||||
logger.error('获取练习失败', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '加载失败' })
|
||||
}
|
||||
},
|
||||
async fetchConversationSetting(imageId: string) {
|
||||
try {
|
||||
this.setData({ loadingMaskVisible: true, statusText: '加载对话设置...' })
|
||||
const setting = await apiManager.getQaConversationSetting(imageId)
|
||||
logger.log('Conversation setting:', setting)
|
||||
if (!setting) {
|
||||
const { task_id } = await apiManager.createQaExerciseTask(imageId, 'init_conversion')
|
||||
logger.log('Conversation init task created:', task_id)
|
||||
this.startConversationInitPolling(task_id, imageId)
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
conversationSetting: setting,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '加载完成'
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('获取自由对话设置失败', e)
|
||||
const msg = (e as any)?.message || ''
|
||||
wx.showToast({ title: msg || '加载失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '加载失败' })
|
||||
}
|
||||
},
|
||||
startVariationPolling(taskId: string, imageId: string) {
|
||||
if (this.variationPollTimer) {
|
||||
clearInterval(this.variationPollTimer)
|
||||
@@ -709,6 +756,30 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
}, 3000) as any
|
||||
},
|
||||
startConversationInitPolling(taskId: string, imageId: string) {
|
||||
if (this.conversationPollTimer) {
|
||||
clearInterval(this.conversationPollTimer)
|
||||
this.conversationPollTimer = undefined
|
||||
}
|
||||
this.setData({ loadingMaskVisible: true })
|
||||
this.conversationPollTimer = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiManager.getQaExerciseTaskStatus(taskId)
|
||||
if (res.status === 'completed') {
|
||||
clearInterval(this.conversationPollTimer!)
|
||||
this.conversationPollTimer = undefined
|
||||
await this.fetchConversationSetting(imageId)
|
||||
} else if (res.status === 'failed') {
|
||||
clearInterval(this.conversationPollTimer!)
|
||||
this.conversationPollTimer = undefined
|
||||
wx.showToast({ title: '任务失败', icon: 'none' })
|
||||
this.setData({ loadingMaskVisible: false, statusText: '任务失败' })
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('轮询自由对话任务状态失败', err)
|
||||
}
|
||||
}, 3000) as any
|
||||
},
|
||||
startPolling(taskId: string, imageId: string) {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer)
|
||||
@@ -753,7 +824,7 @@ Page<IData, IPageInstance>({
|
||||
const q = qaList[idx] || {}
|
||||
const hasOptions = (Array.isArray(q?.correct_options) && q.correct_options.length > 0) || (Array.isArray(q?.incorrect_options) && q.incorrect_options.length > 0)
|
||||
const hasCloze = !!q?.cloze && !!q.cloze.sentence
|
||||
const preferredMode: QuestionMode = hasOptions ? QUESTION_MODES.CHOICE : (hasCloze ? QUESTION_MODES.CLOZE : QUESTION_MODES.FREE_TEXT)
|
||||
const preferredMode: QuestionMode = hasOptions ? QUESTION_MODES.CHOICE : QUESTION_MODES.CLOZE
|
||||
let mode: QuestionMode = ENABLED_QUESTION_MODES.includes(preferredMode) ? preferredMode : ENABLED_QUESTION_MODES[0]
|
||||
|
||||
if (this.data.fixedMode) {
|
||||
@@ -798,7 +869,6 @@ Page<IData, IPageInstance>({
|
||||
clozeParts,
|
||||
clozeSentenceTokens,
|
||||
answers: q.answers || {},
|
||||
freeTextInput: '',
|
||||
questionWords,
|
||||
loadingMaskVisible: false,
|
||||
statusText: '已获取数据',
|
||||
@@ -858,7 +928,6 @@ Page<IData, IPageInstance>({
|
||||
clozeParts,
|
||||
clozeSentenceTokens,
|
||||
answers: q.answers || {},
|
||||
freeTextInput: '',
|
||||
questionWords
|
||||
})
|
||||
const cur = this.data.qaList[idx] || {}
|
||||
@@ -866,6 +935,9 @@ Page<IData, IPageInstance>({
|
||||
if (qid) {
|
||||
const cache = (this.data.qaResultCache || {})[qid]
|
||||
const modeNow = this.data.questionMode
|
||||
if (modeNow === QUESTION_MODES.VARIATION) {
|
||||
this.setData({ variationSelectedIndex: -1, variationResultStatus: null as any, resultDisplayed: false })
|
||||
}
|
||||
const evalForMode = this.getEvaluationForMode(cache, modeNow)
|
||||
this.setData({ resultDisplayed: !!evalForMode })
|
||||
this.updateActionButtonsState()
|
||||
@@ -895,12 +967,9 @@ Page<IData, IPageInstance>({
|
||||
return
|
||||
}
|
||||
|
||||
// Immediately update questionMode to reflect user selection visually
|
||||
// Save previous mode in case we need to revert
|
||||
const previousMode = this.data.questionMode
|
||||
this.setData({ questionMode: target })
|
||||
this.setData({ questionMode: target, fixedMode: target })
|
||||
|
||||
// Special handling for Variation mode switching
|
||||
logger.log('Switching to mode:', target)
|
||||
if (target === QUESTION_MODES.VARIATION) {
|
||||
if (!this.data.variationQaList || this.data.variationQaList.length === 0) {
|
||||
@@ -914,11 +983,23 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
logger.log('Variation mode data:', this.data.variationQaList)
|
||||
if (this.data.variationQaList && this.data.variationQaList.length > 0) {
|
||||
const list = this.data.variationQaList || []
|
||||
const cache = this.data.qaResultCache || {}
|
||||
let nextIndex = 0
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const qid = String((list[i] as any)?.id || '')
|
||||
const r = cache[qid] || {}
|
||||
const hasEval = !!((r.variation || {}).evaluation)
|
||||
if (!hasEval) {
|
||||
nextIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
this.setData({ isModeSwitching: true, modeAnim: 'fade-out' })
|
||||
setTimeout(() => {
|
||||
this.setData({
|
||||
qaList: this.data.variationQaList,
|
||||
currentIndex: 0,
|
||||
currentIndex: nextIndex,
|
||||
modeAnim: 'fade-in',
|
||||
isModeSwitching: false
|
||||
})
|
||||
@@ -931,6 +1012,16 @@ Page<IData, IPageInstance>({
|
||||
return
|
||||
}
|
||||
|
||||
if (target === QUESTION_MODES.CONVERSATION) {
|
||||
try {
|
||||
await this.fetchConversationSetting(this.data.imageId)
|
||||
} catch (e) {
|
||||
this.setData({ questionMode: previousMode, fixedMode: previousMode })
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Switching FROM Variation TO others
|
||||
if (previousMode === QUESTION_MODES.VARIATION) {
|
||||
if (this.data.mainQaList && this.data.mainQaList.length > 0) {
|
||||
@@ -956,7 +1047,6 @@ Page<IData, IPageInstance>({
|
||||
const resetData: any = {
|
||||
selectedOptionIndexes: [],
|
||||
selectedIndex: -1,
|
||||
freeTextInput: '',
|
||||
modeAnim: 'fade-in',
|
||||
selectedClozeIndex: -1,
|
||||
choiceSubmitted: false
|
||||
@@ -996,7 +1086,6 @@ Page<IData, IPageInstance>({
|
||||
try {
|
||||
if (mode === QUESTION_MODES.CHOICE) return (resultObj.choice || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.CLOZE) return (resultObj.cloze || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.FREE_TEXT) return (resultObj.free_text || {}).evaluation || null
|
||||
if (mode === QUESTION_MODES.VARIATION) return (resultObj.variation || {}).evaluation || null
|
||||
return null
|
||||
} catch (e) {
|
||||
@@ -1065,12 +1154,6 @@ Page<IData, IPageInstance>({
|
||||
})
|
||||
const idx = optionContents.findIndex((w) => w === input)
|
||||
this.setData({ selectedClozeIndex: idx, evalClasses })
|
||||
} else if (this.data.questionMode === QUESTION_MODES.FREE_TEXT) {
|
||||
const base = (r as any).free_text || {}
|
||||
const text = String(base.text || '')
|
||||
if (text) {
|
||||
this.setData({ freeTextInput: text })
|
||||
}
|
||||
} else if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
const base = (r as any).variation || {}
|
||||
const fileId = String(base.file_id || '')
|
||||
@@ -1166,20 +1249,6 @@ Page<IData, IPageInstance>({
|
||||
}
|
||||
}
|
||||
|
||||
} else if (type === QUESTION_MODES.FREE_TEXT) {
|
||||
const text = String((base && base.text) || '')
|
||||
const info1: string[] = []
|
||||
if (text) info1.push(text)
|
||||
if (info1.length) blocks.push({ title: '你的回答', variant: 'info', items: info1 })
|
||||
const info2: string[] = []
|
||||
if (result === 'correct' || result === 'exact') info2.push('完全匹配')
|
||||
else if (result === 'partial') info2.push('部分匹配')
|
||||
else info2.push('不匹配')
|
||||
if (detail) info2.push(detail)
|
||||
if (info2.length) blocks.push({ title: '结果说明', variant: 'info', items: info2 })
|
||||
const info3: string[] = []
|
||||
if (feedback) info3.push(feedback)
|
||||
if (info3.length) blocks.push({ title: '反馈', variant: 'info', items: info3 })
|
||||
} else {
|
||||
const infoItems: string[] = []
|
||||
if (detail) infoItems.push(detail)
|
||||
@@ -1200,7 +1269,14 @@ Page<IData, IPageInstance>({
|
||||
if (!evaluation) return
|
||||
const type = String(evaluation?.type || '')
|
||||
const qText = String(cur?.question || '')
|
||||
const base = (modeNow === QUESTION_MODES.CHOICE) ? (r as any).choice : (modeNow === QUESTION_MODES.CLOZE) ? (r as any).cloze : (r as any).free_text
|
||||
let base: any = {}
|
||||
if (modeNow === QUESTION_MODES.CHOICE) {
|
||||
base = (r as any).choice
|
||||
} else if (modeNow === QUESTION_MODES.CLOZE) {
|
||||
base = (r as any).cloze
|
||||
} else if (modeNow === QUESTION_MODES.VARIATION) {
|
||||
base = (r as any).variation
|
||||
}
|
||||
const vm = this.buildDetailViewModel(evaluation, qText, base)
|
||||
this.setData({
|
||||
qaDetailQuestionText: qText,
|
||||
@@ -1231,38 +1307,12 @@ Page<IData, IPageInstance>({
|
||||
this.updateActionButtonsState()
|
||||
return
|
||||
}
|
||||
if (this.data.questionMode === QUESTION_MODES.FREE_TEXT) {
|
||||
this.setData({ freeTextInput: '', resultDisplayed: false })
|
||||
this.updateActionButtonsState()
|
||||
return
|
||||
}
|
||||
if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
this.setData({ variationSelectedIndex: -1, variationSubmitted: false, variationResultStatus: null as any, resultDisplayed: false })
|
||||
this.updateActionButtonsState()
|
||||
return
|
||||
}
|
||||
},
|
||||
async onHintTap() {
|
||||
if (this.data.questionMode !== QUESTION_MODES.FREE_TEXT) {
|
||||
wx.showToast({ title: '当前非输入题目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const a = this.data.answers || {}
|
||||
const keys = ['lively', 'spoken', 'written', 'friendly']
|
||||
const items = keys.map((k) => k)
|
||||
try {
|
||||
const res = await wx.showActionSheet({ itemList: items })
|
||||
const idx = res?.tapIndex ?? -1
|
||||
if (idx >= 0) {
|
||||
const key = keys[idx]
|
||||
const v = a[key] || ''
|
||||
if (v) {
|
||||
this.setData({ freeTextInput: v })
|
||||
this.updateActionButtonsState()
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
selectOption(e: any) {
|
||||
if (this.data.resultDisplayed) return
|
||||
const i = Number(e?.currentTarget?.dataset?.index) || 0
|
||||
@@ -1340,14 +1390,6 @@ Page<IData, IPageInstance>({
|
||||
const i = Number(e?.currentTarget?.dataset?.index) || 0
|
||||
this.setData({ selectedClozeIndex: i })
|
||||
},
|
||||
inputChange(e: any) {
|
||||
const v = String(e?.detail?.value || '')
|
||||
const english = v.replace(/[^A-Za-z0-9 ,.'!?-]/g, '')
|
||||
if (english !== v) {
|
||||
wx.showToast({ title: '仅限英文输入', icon: 'none' })
|
||||
}
|
||||
this.setData({ freeTextInput: english })
|
||||
},
|
||||
previewImage() {
|
||||
const url = this.data.imageLocalUrl
|
||||
if (url) {
|
||||
@@ -1359,10 +1401,40 @@ Page<IData, IPageInstance>({
|
||||
if (['correct'].includes(resStr)) {
|
||||
this.fireConfetti()
|
||||
|
||||
// Check if there is a next question
|
||||
const currentIndex = this.data.currentIndex
|
||||
const total = (this.data.qaList || []).length
|
||||
if (currentIndex >= total - 1) {
|
||||
const list = this.data.qaList || []
|
||||
if (!list.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const cache = this.data.qaResultCache || {}
|
||||
const session = this.data.session
|
||||
let attempts: any[] = []
|
||||
if (!this.data.isTrialMode && session && typeof session.progress === 'object' && session.progress !== null && 'attempts' in session.progress) {
|
||||
attempts = (session.progress as any).attempts
|
||||
}
|
||||
const attemptSet = new Set(attempts.map((a: any) => String(a.question_id)))
|
||||
|
||||
const findFirstUnattemptedIndex = () => {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const q = list[i] || {}
|
||||
const id = String((q as any)?.id || '')
|
||||
if (!id) continue
|
||||
const r = cache[id] || {}
|
||||
const hasLocalResult =
|
||||
!!((r.choice || {}).evaluation) ||
|
||||
!!((r.cloze || {}).evaluation) ||
|
||||
!!((r.variation || {}).evaluation)
|
||||
const hasSessionAttempt = this.data.isTrialMode ? false : attemptSet.has(id)
|
||||
if (!(hasLocalResult || hasSessionAttempt)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const targetIndex = findFirstUnattemptedIndex()
|
||||
|
||||
if (targetIndex === -1 || targetIndex === this.data.currentIndex) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1389,7 +1461,8 @@ Page<IData, IPageInstance>({
|
||||
setTimeout(() => {
|
||||
const currentQ = this.data.qaList[this.data.currentIndex] || {}
|
||||
if (String(currentQ.id) === qid) {
|
||||
this.onNextTap()
|
||||
const delta = targetIndex - this.data.currentIndex
|
||||
this.switchQuestion(delta)
|
||||
}
|
||||
this.setData({ isAutoSwitching: false, nextButtonIcon: 'chevron-right' })
|
||||
}, 3000)
|
||||
@@ -1426,8 +1499,6 @@ Page<IData, IPageInstance>({
|
||||
if (typeof sel === 'number' && sel >= 0 && sel < opt.length) {
|
||||
payload.input_text = opt[sel]
|
||||
}
|
||||
} else if (this.data.questionMode === QUESTION_MODES.FREE_TEXT) {
|
||||
payload.input_text = this.data.freeTextInput
|
||||
} else if (this.data.questionMode === QUESTION_MODES.VARIATION) {
|
||||
const idx = typeof this.data.variationSelectedIndex === 'number' ? this.data.variationSelectedIndex : -1
|
||||
if (idx < 0) {
|
||||
@@ -1474,7 +1545,6 @@ Page<IData, IPageInstance>({
|
||||
let evaluation: any = null
|
||||
if (mode === QUESTION_MODES.CHOICE) evaluation = res.choice?.evaluation
|
||||
else if (mode === QUESTION_MODES.CLOZE) evaluation = res.cloze?.evaluation
|
||||
else if (mode === QUESTION_MODES.FREE_TEXT) evaluation = res.free_text?.evaluation
|
||||
else if (mode === QUESTION_MODES.VARIATION) evaluation = res.variation?.evaluation
|
||||
|
||||
if (evaluation) {
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
</view>
|
||||
|
||||
<view class="type-container" wx:if="{{fixedMode}}">
|
||||
<view class="type-item {{questionMode === 'cloze' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="cloze">填空</view>
|
||||
<view class="type-item {{questionMode === 'choice' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="choice">问答</view>
|
||||
<view class="type-item {{questionMode === 'variation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="variation">扩展训练</view>
|
||||
<view class="type-item {{fixedMode === 'cloze' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="cloze">填空</view>
|
||||
<view class="type-item {{fixedMode === 'choice' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="choice">问答</view>
|
||||
<view class="type-item {{fixedMode === 'variation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="variation">识图</view>
|
||||
<view class="type-item {{fixedMode === 'conversation' ? 'active' : ''}}" hover-class="type-item-hover" bindtap="switchMode" data-mode="conversation">对话</view>
|
||||
</view>
|
||||
|
||||
<view class="container {{contentVisible ? 'fade-in' : 'fade-out'}}" wx:if="{{contentReady && !loadingMaskVisible}}">
|
||||
<view class="process-container" wx:if="{{qaList && qaList.length > 0}}">
|
||||
<view class="process-container" wx:if="{{qaList && qaList.length > 0 && questionMode !== 'conversation'}}">
|
||||
<block wx:for="{{qaList}}" wx:key="index">
|
||||
<view class="process-dot {{processDotClasses[index]}} {{index === currentIndex ? 'current' : ''}}"></view>
|
||||
</block>
|
||||
@@ -30,7 +31,7 @@
|
||||
<!-- <text>View Full</text> -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="question-title">
|
||||
<view class="question-title" wx:if="{{questionMode !== 'conversation'}}">
|
||||
<text wx:for="{{questionWords}}" wx:key="index" class="word-item" data-word="{{item}}" bindtap="handleWordClick">{{item}}</text>
|
||||
</view>
|
||||
<!-- <view class="progress-text">{{progressText}}</view> -->
|
||||
@@ -78,7 +79,7 @@
|
||||
<view class="variation-container">
|
||||
<view class="variation-grid">
|
||||
<view class="variation-item" wx:for="{{variationQaList}}" wx:key="index">
|
||||
<view class="variation-image-wrapper {{index === variationSelectedIndex ? 'selected' : ''}}" data-index="{{index}}" bindtap="selectVariationOption">
|
||||
<view class="variation-image-wrapper {{index === variationSelectedIndex ? 'selected' : ''}}" data-index="{{index}}" bindtap="selectVariationOption">
|
||||
<cloud-image
|
||||
file-id="{{item.file_id}}"
|
||||
mode="widthFix"
|
||||
@@ -87,6 +88,9 @@
|
||||
bind:load="onVariationImageLoad"
|
||||
data-fileid="{{item.file_id}}"
|
||||
/>
|
||||
<view class="view-full" wx:if="{{variationImageLoaded[item.file_id]}}" data-fileid="{{item.file_id}}" catchtap="previewVariationImage">
|
||||
<t-icon name="zoom-in" size="32rpx" />
|
||||
</view>
|
||||
<view wx:if="{{variationImageLoaded[item.file_id]}}" class="selection-badge {{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'incorrect' : (index === variationSelectedIndex ? 'selected' : 'unselected')}}">
|
||||
<t-icon name="{{variationResultStatus === 'incorrect' && index === variationSelectedIndex ? 'close-circle' : 'check-circle'}}" size="36rpx" color="#fff" />
|
||||
</view>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
.image-card { position: relative; border-radius: 24rpx; overflow: hidden; height: 360rpx; }
|
||||
.image { width: 100%; height: 360rpx; border-radius: 24rpx; background: #f5f5f5; }
|
||||
.view-full { position: absolute; right: 16rpx; bottom: 16rpx; display: flex; align-items: center; gap: 8rpx; padding: 10rpx 16rpx; border-radius: 24rpx; background: rgba(0,0,0,0.4); color: #fff; }
|
||||
.view-full { position: absolute; right: 16rpx; bottom: 16rpx; display: flex; align-items: center; gap: 8rpx; padding: 8rpx; border-radius: 24rpx; background: rgba(0,0,0,0.4); color: #fff; }
|
||||
.question-title { font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; width: 100%; display: flex; flex-wrap: wrap; justify-content: flex-start;}
|
||||
.progress-text { font-size: 26rpx; color: #666; }
|
||||
.word-item { display: inline; font-size: 40rpx; font-weight: 700; color: #001858; line-height: 56rpx; margin-right: 12rpx; }
|
||||
@@ -491,6 +491,7 @@
|
||||
background: #f5f5f5;
|
||||
border-radius: 40rpx;
|
||||
padding: 8rpx;
|
||||
margin-top: 12rpx;
|
||||
width: fit-content;
|
||||
position: relative;
|
||||
left: 50%;
|
||||
|
||||
@@ -642,6 +642,9 @@ Page({
|
||||
try {
|
||||
this.setData({ isProcessing: true })
|
||||
wx.showLoading({ title: '准备拍照...' })
|
||||
try {
|
||||
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
|
||||
} catch (e) {}
|
||||
|
||||
// const imagePath = await imageManager.takePhoto({
|
||||
// quality: 80,
|
||||
@@ -679,6 +682,9 @@ Page({
|
||||
try {
|
||||
this.setData({ isProcessing: true })
|
||||
wx.showLoading({ title: '准备选择图片...' })
|
||||
try {
|
||||
wx.pageScrollTo({ scrollTop: 0, duration: 300 })
|
||||
} catch (e) {}
|
||||
|
||||
// const imagePath = await imageManager.chooseFromAlbum({
|
||||
// quality: 80,
|
||||
@@ -767,7 +773,6 @@ Page({
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
|
||||
async ensurePhotoPersistent() {
|
||||
try {
|
||||
const p = String(this.data.photoPath || '').trim()
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{takePhoto && showExpandLayer }}" class="photo-expand-layer" style="{{photoExpandTransform}} {{photoExpandTransition}}">
|
||||
<view wx:if="{{takePhoto && showExpandLayer }}" class="photo-expand-layer" style="{{photoExpandTransform}} {{photoExpandTransition}}" catchtouchmove="noop">
|
||||
<!-- <view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}} {{photoExpandCurrentHeight ? ('height: ' + photoExpandCurrentHeight + 'px;') : ''}}"> -->
|
||||
<view class="photo-expand-inner" style="{{photoExpandCurrentWidth ? ('width: ' + photoExpandCurrentWidth + 'px;') : ''}} {{photoExpandCurrentHeight ? ('height: ' + photoExpandCurrentHeight + 'px;') : ''}}">
|
||||
<image class="photo-expand-image {{photoExpandLoaded ? 'visible' : 'hidden'}}" src="{{takePhoto ? photoExpandSrc : photoSvgData}}" mode="widthFix" bindload="onPhotoExpandLoaded" binderror="onPhotoExpandError"></image>
|
||||
|
||||
@@ -168,6 +168,8 @@ export interface IPointsData {
|
||||
total_purchased: number
|
||||
total_refunded: number
|
||||
expired_time: string
|
||||
is_subscribed: boolean
|
||||
subscription_expires_at: string | null
|
||||
}
|
||||
|
||||
export interface IQaExerciseSession {
|
||||
|
||||
@@ -1961,7 +1961,7 @@ class ApiManager {
|
||||
|
||||
// 如果返回的数据为null,则返回默认值
|
||||
if (!response.data) {
|
||||
return { balance: 0, available_balance: 0, frozen_balance: 0, total_purchased: 0, total_refunded: 0, expired_time: '' };
|
||||
return { balance: 0, available_balance: 0, frozen_balance: 0, total_purchased: 0, total_refunded: 0, expired_time: '', is_subscribed: false, subscription_expires_at: null as any };
|
||||
}
|
||||
const d: any = response.data as any
|
||||
const normalized = {
|
||||
@@ -1970,7 +1970,9 @@ class ApiManager {
|
||||
frozen_balance: Number(d.frozen_balance || 0),
|
||||
total_purchased: Number(d.total_purchased || 0),
|
||||
total_refunded: Number(d.total_refunded || 0),
|
||||
expired_time: String(d.expired_time || '')
|
||||
expired_time: String(d.expired_time || ''),
|
||||
is_subscribed: !!d.is_subscribed,
|
||||
subscription_expires_at: d.subscription_expires_at || null
|
||||
}
|
||||
return normalized as import("../types/app").IPointsData;
|
||||
} catch (error) {
|
||||
@@ -2017,6 +2019,29 @@ class ApiManager {
|
||||
return response.data
|
||||
}
|
||||
|
||||
async createSubscriptionOrder(product_id: string): Promise<{
|
||||
out_trade_no: string
|
||||
prepay_id: string
|
||||
trade_state: string
|
||||
timeStamp?: string
|
||||
nonceStr?: string
|
||||
paySign?: string
|
||||
signType?: string
|
||||
}> {
|
||||
const response = await this.request<{
|
||||
out_trade_no: string
|
||||
prepay_id: string
|
||||
trade_state: string
|
||||
timeStamp?: string
|
||||
nonceStr?: string
|
||||
paySign?: string
|
||||
signType?: string
|
||||
}>('/api/v1/wxpay/order/jsapi/subscription', 'POST', {
|
||||
product_id
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getProductList(): Promise<Array<{
|
||||
id: number
|
||||
title: string
|
||||
@@ -2036,6 +2061,33 @@ class ApiManager {
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getSubscriptionPlans(): Promise<Array<{
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
cycle_type: string
|
||||
cycle_length: number
|
||||
max_cycle_usage: number
|
||||
features?: {
|
||||
label?: string
|
||||
extra?: string
|
||||
}
|
||||
}>> {
|
||||
const response = await this.request<Array<{
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
cycle_type: string
|
||||
cycle_length: number
|
||||
max_cycle_usage: number
|
||||
features?: {
|
||||
label?: string
|
||||
extra?: string
|
||||
}
|
||||
}>>('/api/v1/product/plan', 'GET')
|
||||
return response.data
|
||||
}
|
||||
|
||||
async getWxpayOrder(out_trade_no: string): Promise<{
|
||||
out_trade_no: string
|
||||
trade_state: string
|
||||
@@ -2111,6 +2163,13 @@ class ApiManager {
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async getQaConversationSetting(imageId: string): Promise<any> {
|
||||
const resp = await this.request<any>('/api/v1/qa/conversations/setting', 'POST', {
|
||||
image_id: imageId
|
||||
})
|
||||
return resp.data
|
||||
}
|
||||
|
||||
async createQaQuestionAttempt(
|
||||
questionId: string,
|
||||
payload: {
|
||||
|
||||
Reference in New Issue
Block a user