diff --git a/src/views/jingdong/index.vue b/src/views/jingdong/index.vue index e7257b9..942a0b9 100644 --- a/src/views/jingdong/index.vue +++ b/src/views/jingdong/index.vue @@ -197,7 +197,15 @@ const caseItems = [ { label: config.messages.caseLabels[2], src: config.images.caseScene }, ] +/** + * 剩余次数: + * - 首屏用接口 remain_count 打底 + * - 之后只按「本地点成功出图」递减 + * - 接口若未扣减(仍返回原值),不会把次数加回去 + */ const remainCount = ref(0) +/** 打底时已统计到的成功次数;新增成功 = 当前成功数 - baselineUsed */ +const remainBaselineUsed = ref(0) const sessionId = ref('') const subjectName = ref('') /** stat 返回的主体文案(订单约定主体,标签始终展示此值) */ @@ -270,6 +278,64 @@ const isEditing = computed(() => !!editingResultId.value) const isPendingCartoon = computed( () => Boolean(pendingPreview.value && photoInspect.value && isCartoonImage(photoInspect.value)), ) + +/** 统计已消耗的生图次数:成功 task + 对话里已出的结果卡 */ +function countUsedGenerations() { + const ids = new Set() + const detail = sessionDetail.value as { tasks?: Array> } | null + detail?.tasks?.forEach((task) => { + const taskId = String(task?.task_id || '').trim() + if (!taskId) return + const images = Array.isArray(task.images) ? task.images : [] + const hasImage = images.some((image: any) => Boolean(image?.url)) + const status = String(task.status || '').toLowerCase() + if (status === 'success' || hasImage) ids.add(`task:${taskId}`) + }) + messages.value.forEach((msg) => { + if (msg.type !== 'result') return + const taskId = String(msg.taskId || '').trim() + // 与 task 去重;无 taskId 的本地结果用消息 id + ids.add(taskId ? `task:${taskId}` : `msg:${msg.id}`) + }) + return ids.size +} + +/** + * 首屏打底:接口 remain_count 在京东侧常不随出图减少,这里当作可生图总额, + * 再按会话内已成功次数扣除;后续只减不增。 + */ +function seedRemainFromApi(apiRemainRaw?: unknown) { + const apiRemain = Number(apiRemainRaw) + remainBaselineUsed.value = 0 + remainCount.value = Number.isFinite(apiRemain) ? Math.max(0, apiRemain) : 0 + refreshRemainByUsed() +} + +/** 按已成功出图数重算剩余(只减不加) */ +function refreshRemainByUsed(apiRemainRaw?: unknown) { + const used = countUsedGenerations() + const earned = Math.max(0, used - remainBaselineUsed.value) + remainCount.value = Math.max(0, remainCount.value - earned) + remainBaselineUsed.value = used + + const apiRemain = Number(apiRemainRaw) + if (!Number.isFinite(apiRemain)) return + // 接口若已扣得更少,跟随接口;偏高(未扣)则忽略,避免加回次数 + if (apiRemain < remainCount.value) { + remainCount.value = Math.max(0, apiRemain) + } +} + +/** 生图接口成功即扣 1 次(不等待过渡动画),并抬高 baseline 避免结果入列后重复扣 */ +function noteGenerationSuccess() { + remainCount.value = Math.max(0, remainCount.value - 1) + remainBaselineUsed.value += 1 +} + +function resetRemainQuota() { + remainCount.value = 0 + remainBaselineUsed.value = 0 +} const titleSpec = computed(() => productTitleSpecFromOrder(orderStat.value)) const hasRequiredTitle = computed(() => { if (!titleSpec.value) return true @@ -655,17 +721,17 @@ function onGenerateStageDone(generatingId: string) { } if (pending.uploadId) lastUploadId.value = pending.uploadId - remainCount.value = Math.max(0, remainCount.value - 1) + // 成功扣次已在 noteGenerationSuccess;这里只把 baseline 与结果卡对齐 + remainBaselineUsed.value = Math.max(remainBaselineUsed.value, countUsedGenerations()) isBusy.value = false photoInspect.value = null scrollToBottom() - // 仅同步剩余次数,避免整页历史重绘冲掉当前气泡 + // 仅同步剩余次数;接口未扣减时不会把次数加回去 void badgeApi .getOrderStat({}) .then((res: any) => { - const count = Number(res?.remain_count) - if (Number.isFinite(count)) remainCount.value = Math.max(0, count) orderStat.value = res || orderStat.value + refreshRemainByUsed(res?.remain_count) }) .catch(() => {}) } @@ -797,6 +863,8 @@ async function startGenerate(opts: { startedAt, } patchGeneratingResult(generatingId, resultUrl) + // 接口已成功出图:立刻扣剩余次数(不依赖过渡动画是否播完) + noteGenerationSuccess() scrollToBottom() } catch (error) { removeGenerating(generatingId) @@ -942,6 +1010,8 @@ function applySessionHistory(detail: ReturnType { sessionDetail.value = null + seedRemainFromApi(orderStat.value?.remain_count) }) } @@ -974,19 +1046,21 @@ function fetchOrderStat() { .then((res: any) => { // interceptor 已解包 data:remain_count / subject_name / type_name / session_id / prod_id ... orderStat.value = res || {} - const count = Number(res?.remain_count) - remainCount.value = Number.isFinite(count) ? Math.max(0, count) : 0 subjectNameFromStat.value = String(res?.subject_name || '').trim() subjectName.value = subjectNameFromStat.value styleName.value = String(res?.style_name || '').trim() typeName.value = String(res?.type_name || '').trim() sessionId.value = String(res?.session_id || '').trim() subjectMatched.value = true + // 先记下接口值;等会话历史回来后再按已生成次数扣除 + const apiRemain = Number(res?.remain_count) + remainCount.value = Number.isFinite(apiRemain) ? Math.max(0, apiRemain) : 0 + remainBaselineUsed.value = 0 return fetchSessionDetail(sessionId.value) }) .catch(() => { orderStat.value = null - remainCount.value = 0 + resetRemainQuota() subjectName.value = '' subjectNameFromStat.value = '' styleName.value = ''