feat: 接入内容服务端真实接口并优化页面交互

- 迁移内容数据到服务端接口,移除本地模拟数据
- 首页新增推荐课程加载与未读消息检查
- 资讯、课程、搜索页面支持分页加载与错误重试
- 登录页新增恢复课程、消息、帮助页的路由逻辑
- 评估分析页新增处理中状态动效与阶段提示
- API 环境切换到正式域名 airz.hqling.cloud
This commit is contained in:
hujun
2026-10-01 20:26:41 +08:00
parent 3b2a0139e4
commit 2a9ff403c4
41 changed files with 21742 additions and 427 deletions
+33 -6
View File
@@ -3,8 +3,9 @@
<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="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
<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>
@@ -20,9 +21,25 @@
const { statusStyle, pageStyle } = usePageLayout()
const title = ref('正在读取评估状态')
const message = ref(''); const error = ref(''); const assessmentId = ref('')
const canRetry = ref(false); const succeeded = ref(false); const retrying = ref(false)
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 visible = false; let generation = 0
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 }
@@ -35,6 +52,9 @@
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' ? '资料已保存,等待分析。您可以稍后从档案查看。' : '正在分析自填资料,您可以稍后从档案查看。') : '分析已完成,可查看本次报告。')
@@ -43,7 +63,7 @@
} catch (err) {
if (visible && request == generation && getSession()?.getString('token') == owner) {
// 查询失败不能沿用旧任务的重试/报告按钮,也不能把网络错误当成分析失败。
canRetry.value = false; succeeded.value = false; message.value = '暂时无法获取最新状态,请刷新状态。已提交的资料无需重新填写。'
canRetry.value = false; succeeded.value = false; processing.value = false; stopMotion(); message.value = '暂时无法获取最新状态,请刷新状态。已提交的资料无需重新填写。'
title.value = '状态加载失败'; error.value = err instanceof Error ? err.message : '请稍后重试'
}
}
@@ -70,8 +90,8 @@
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); canRetry.value = false; succeeded.value = false; message.value = ''; error.value = '' })
onUnload(() => { visible = false; generation++; clearTimeout(timer) })
onHide(() => { visible = false; generation++; clearTimeout(timer); stopMotion(); canRetry.value = false; succeeded.value = false; processing.value = false; message.value = ''; error.value = '' })
onUnload(() => { visible = false; generation++; clearTimeout(timer); stopMotion() })
</script>
<style>
@@ -84,7 +104,9 @@
.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; }
@@ -92,6 +114,11 @@
.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>