Files
airz-uniapp/packages/finance/analyzing/analyzing.uvue
hujun 2c9d3feab5 feat: 完善登录流程、UI适配及协议文档更新
本次提交包含以下修改:
1. 新增微信登录相关服务及页面逻辑,实现首次微信绑定手机号流程
2. 优化产品匹配卡片 UI,移除不必要状态显示
3. 调整首页 scroll-view 属性及 banner 背景样式
4. 完善积分规则校验逻辑,增加时区及策略格式验证
5. 更新协议草案文档,补充资料清单及功能说明
6. 修复分析结果路由路径,适配新的匹配结果页面
7. 调整品牌 Logo 安卓端字体对齐样式
2026-10-22 11:24:15 +08:00

134 lines
11 KiB
Plaintext

<template>
<view class="analyzing-page" :style="pageStyle()">
<view class="safe" :style="statusStyle(80)"></view>
<text class="analyzing-title">{{ title }}</text>
<text class="analyzing-sub">正式提交记录 · {{ assessmentId }}</text>
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="scan-ring" :style="scanRingStyle()"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
<text class="status-message">{{ message }}</text>
<view v-if="processing" class="analysis-pulse"><view class="pulse-dot" :style="pulseDotStyle()"></view><text class="stage-label">{{ stageLabel }}</text><text class="pulse-dots" :style="pulseDotsStyle()">···</text></view>
<text v-if="error.length > 0" class="status-message">{{ error }}</text>
<text v-if="canRetry" class="view-now" @click="retry">{{ retrying ? '正在提交…' : '重新分析' }}</text>
<text v-if="succeeded" class="view-now" @click="report">查看匹配结果</text>
<text class="view-now" @click="refreshStatus">{{ retrying ? '正在提交重试…' : '刷新状态' }}</text>
<text class="view-now" @click="archive">查看已提交资料</text>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { fetchAssessmentStatus, retryAssessment } from '@/packages/finance/services/records.uts'
import { matchReturnPath } from '@/services/match-route.uts'
const { statusStyle, pageStyle } = usePageLayout()
const title = ref('正在读取评估状态')
const message = ref(''); const error = ref(''); const assessmentId = ref('')
const publishedRevisionId = ref('')
const canRetry = ref(false); const succeeded = ref(false); const retrying = ref(false); const processing = ref(false); const stageLabel = ref('正在准备分析')
let retryKey = ''; let retryTask = ''; let retryOwner = ''
let timer = 0; let motionTimer = 0; let visible = false; let generation = 0
// 动效只反映服务端 processing 状态,不用视觉帧推断分析进度或完成时间。
const motionPhase = ref(0)
const phaseRadians = (): number => motionPhase.value * Math.PI * 2
const scanRingStyle = (): string => {
if (!processing.value) return 'opacity:0;transform:scale(.78);'
const phase = phaseRadians()
const progress = (Math.sin(phase) + 1) / 2
return 'opacity:' + (0.12 + progress * 0.62).toString() + ';transform:scale(' + (0.78 + progress * 0.52).toString() + ');'
}
const pulseDotStyle = (): string => 'opacity:' + (0.5 + ((Math.sin(phaseRadians()) + 1) / 2) * 0.5).toString() + ';transform:scale(' + (0.82 + ((Math.sin(phaseRadians()) + 1) / 2) * 0.26).toString() + ');'
const pulseDotsStyle = (): string => 'opacity:' + (0.25 + ((Math.sin(phaseRadians() - Math.PI / 2) + 1) / 2) * 0.75).toString() + ';'
const stopMotion = () => { if (motionTimer != 0) { clearInterval(motionTimer); motionTimer = 0 }; motionPhase.value = 0 }
const startMotion = () => {
stopMotion()
motionTimer = setInterval(() => { if (!processing.value) { stopMotion(); return }; motionPhase.value = (motionPhase.value + 0.035) % 1 }, 80)
}
async function loadStatus(): Promise<void> {
clearTimeout(timer)
if (assessmentId.value.length == 0) { title.value = '缺少评估记录'; message.value = '请从融资档案选择已提交的记录。'; return }
const session = getSession()
if (session == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
const owner = session['token'] as string
const request = ++generation
error.value = ''
try {
const result = await fetchAssessmentStatus(assessmentId.value)
if (!visible || request != generation || getSession()?.getString('token') != owner) return
canRetry.value = result.canRetry; succeeded.value = result.status == 'succeeded'
const nextProcessing = result.status == 'processing'
if (nextProcessing != processing.value) { processing.value = nextProcessing; if (nextProcessing) startMotion(); else stopMotion() }
stageLabel.value = result.stage == 'queued' ? '正在排队,分析任务即将开始' : '正在整理资料,生成分析报告'
if (retryTask != result.taskId || retryOwner != owner) { retryKey = ''; retryTask = result.taskId; retryOwner = owner }
title.value = result.statusLabel
message.value = result.message.length > 0 ? result.message : (result.status == 'processing' ? (result.stage == 'queued' ? '资料已保存,等待分析。您可以稍后从档案查看。' : '正在分析自填资料,您可以稍后从档案查看。') : (result.status == 'failed' ? '分析未完成,可重新分析或刷新状态。已提交的资料无需重新填写。' : '分析已完成,可查看本次报告。'))
if (result.status == 'succeeded') {
// 只打开本次已发布版本;重评处理中或旧报告回包不会触发跳转。
const target = result.hasReport && result.publishedRevisionId == result.revisionId ? matchReturnPath('matches', assessmentId.value, '', result.publishedRevisionId, '', 'all') : ''
succeeded.value = target.length > 0
if (target.length > 0) { publishedRevisionId.value = result.publishedRevisionId; uni.redirectTo({ url: target }); return }
message.value = '本次报告尚未就绪,请刷新状态。'
}
// 只轮询服务端仍在处理的任务,不用计时器模拟完成。
if (result.status == 'processing') timer = setTimeout(() => { loadStatus() }, 5000)
} catch (err) {
if (visible && request == generation && getSession()?.getString('token') == owner) {
// 查询失败不能沿用旧任务的重试/报告按钮,也不能把网络错误当成分析失败。
canRetry.value = false; succeeded.value = false; processing.value = false; stopMotion(); message.value = '暂时无法获取最新状态,请刷新状态。已提交的资料无需重新填写。'
title.value = '状态加载失败'; error.value = err instanceof Error ? err.message : '请稍后重试'
}
}
}
// 重试进行中不再开启并行状态请求,避免旧状态覆盖新回执。
const refreshStatus = () => { if (!retrying.value) loadStatus() }
const retry = async () => {
if (retrying.value || !canRetry.value) return
const session = getSession()
if (session == null) { loadStatus(); return }
const owner = session['token'] as string
if (retryOwner != owner) { retryKey = ''; retryOwner = owner }
// 网络失败保留当前键;状态中的任务ID变化后才为下次尝试生成新键。
if (retryKey.length == 0) retryKey = 'retry_' + Date.now().toString() + '_' + Math.random().toString(36).substring(2, 14)
retrying.value = true; error.value = ''; clearTimeout(timer)
const request = ++generation
try {
await retryAssessment(assessmentId.value, retryKey)
if (visible && request == generation && getSession()?.getString('token') == owner) { canRetry.value = false; await loadStatus() }
} catch (err) { if (visible && request == generation && getSession()?.getString('token') == owner) error.value = err instanceof Error ? err.message : '重新分析失败,请重试' }
finally { retrying.value = false }
}
const report = () => { const target = matchReturnPath('matches', assessmentId.value, '', publishedRevisionId.value, '', 'all'); if (succeeded.value && target.length > 0) uni.redirectTo({ url: target }) }
const archive = () => { uni.reLaunch({ url: '/packages/finance/archive/archive?assessmentId=' + encodeURIComponent(assessmentId.value) }) }
onLoad((options) => { if (options != null) assessmentId.value = options['assessmentId'] ?? '' })
onShow(() => { visible = true; loadStatus() })
onHide(() => { visible = false; generation++; clearTimeout(timer); stopMotion(); canRetry.value = false; succeeded.value = false; processing.value = false; publishedRevisionId.value = ''; message.value = ''; error.value = '' })
onUnload(() => { visible = false; generation++; clearTimeout(timer); stopMotion() })
</script>
<style>
.analyzing-page { background: linear-gradient(160deg, #061c49, #002c75); align-items: center; color: #fff; }
.safe { }
.analyzing-title { font-size: 45rpx; font-weight: 800; }
.analyzing-sub { margin-top: 20rpx; color: #c6d9ff; font-size: 25rpx; }
.orbital { width: 400rpx; height: 400rpx; margin-top: 55rpx; align-items: center; justify-content: center; position: relative; }
.orbit { position: absolute; border-radius: 50%; border: 2rpx solid rgba(79, 176, 255, .46); }
.orbit-one { width: 350rpx; height: 350rpx; }
.orbit-two { width: 280rpx; height: 280rpx; border-color: rgba(57, 235, 255, .5); }
.ai-core { width: 220rpx; height: 220rpx; border-radius: 50%; background-color: rgba(10, 102, 247, .32); box-shadow: 0 0 70rpx rgba(0, 165, 255, .48); align-items: center; justify-content: center; }
.scan-ring { transition: transform 80ms linear, opacity 80ms linear; }
.ai-core-image { width: 185rpx; height: 185rpx; border-radius: 50%; }
.scan-ring { position: absolute; width: 210rpx; height: 210rpx; border: 2rpx solid rgba(70, 216, 255, .85); border-radius: 50%; opacity: 0; }
.analysis-list { margin-top: 30rpx; width: 390rpx; }
.analysis-item { flex-direction: row; align-items: center; margin-top: 23rpx; font-size: 26rpx; color: #d8e6ff; }
.analysis-label { font-size: 27rpx; color: #d8e6ff; }
.analysis-mark { font-size: 24rpx; color: #ffffff; }
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
.status-message { margin: 32rpx; color: #d8e6ff; font-size: 28rpx; line-height: 44rpx; text-align: center; }
.analysis-pulse { flex-direction: row; align-items: center; justify-content: center; margin-top: -12rpx; min-height: 34rpx; }
.pulse-dot { width: 12rpx; height: 12rpx; border-radius: 50%; background-color: #43ddff; box-shadow: 0 0 16rpx rgba(67, 221, 255, .9); }
.stage-label { margin-left: 12rpx; color: #9fc8ff; font-size: 23rpx; }
.pulse-dots { width: 40rpx; margin-left: 2rpx; color: #63dcff; font-size: 24rpx; letter-spacing: 4rpx; }
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
.view-now { margin-top: 28rpx; color: #62c8ff; font-size: 23rpx; }
.pulse-dot, .pulse-dots { transition: transform 80ms linear, opacity 80ms linear; }
</style>