feat: 重构评估流程为内存状态管理并接入真实提交与报告接口

本次重构将评估草稿改为内存状态管理,移除本地存储草稿,新增评估流程内存状态模块;接入真实的账户资料获取、头像上传接口,新增账户缓存清理逻辑;重构评估提交逻辑,接入真实提交接口并返回评估ID;新增分析页和报告页的真实状态查询、报告获取功能;更新README文档,记录开发进度和接口状态。
This commit is contained in:
hujun
2026-09-24 11:50:07 +08:00
parent dcf12a4471
commit dbfd637388
22 changed files with 1468 additions and 434 deletions
@@ -16,16 +16,23 @@
import AppIcon from '@/components/AppIcon.uvue'
import { usePageLayout } from '@/services/layout.uts'
import { getSession } from '@/services/api.uts'
import { DEFAULT_AVATAR, maskPhone, getAccountProfile, saveAccountProfile, readAvatar } from '@/packages/account/services/account.uts'
import { DEFAULT_AVATAR, maskPhone, getAccountProfile, fetchAccountProfile, saveAccountProfile, uploadAccountAvatar } from '@/packages/account/services/account.uts'
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
const nickname = ref(''); const avatar = ref(''); const phone = ref(''); const saving = ref(false); const choosing = ref(false); const errorMessage = ref('')
const avatarId = ref('')
let originalName = ''; let originalAvatar = ''; let alive = true
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar
onLoad(() => {
if (getSession() == null) { uni.redirectTo({ url: '/packages/account/login/login' }); return }
const profile = getAccountProfile(); nickname.value = profile.nickname; avatar.value = profile.avatar; phone.value = profile.phone
originalName = profile.nickname; originalAvatar = profile.avatar
// 服务端资料优先;请求返回时已开始编辑则不覆盖用户输入。
fetchAccountProfile().then((fresh) => {
if (!alive || dirty()) return
nickname.value = fresh.nickname; avatar.value = fresh.avatar; phone.value = fresh.phone
originalName = fresh.nickname; originalAvatar = fresh.avatar
}).catch((error) => { if (alive) errorMessage.value = error instanceof Error ? error.message : '资料加载失败,请重试' })
})
const dirty = (): boolean => nickname.value.trim() != originalName || avatar.value != originalAvatar
const leave = () => { if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/account/profile/profile' }) }
const back = () => {
if (saving.value) return
@@ -35,14 +42,17 @@
onBackPress((event): boolean => { if (event.from == 'navigateBack') return false; if (saving.value || dirty()) { back(); return true }; return false })
const loadAvatar = async (path: string) => {
choosing.value = true; errorMessage.value = ''
try { const image = await readAvatar(path); if (alive) avatar.value = image }
try {
const result = await uploadAccountAvatar(path)
if (alive) { avatar.value = result['avatarUrl'] as string; avatarId.value = result['attachmentId'] as string }
}
catch (error) { if (alive) errorMessage.value = error instanceof Error ? error.message : '图片读取失败' }
finally { if (alive) choosing.value = false }
}
const chooseAvatar = () => {
if (saving.value || choosing.value) return
uni.showActionSheet({ itemList: ['从相册选择', '恢复默认头像'], success: (action) => {
if (action.tapIndex == 1) { avatar.value = ''; return }
if (action.tapIndex == 1) { avatar.value = ''; avatarId.value = 'default'; return }
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album'], success: (result) => {
if (result.tempFilePaths.length == 0) return
if (result.tempFiles[0].size > 512000) { errorMessage.value = '请选择 500KB 以内的图片'; return }
@@ -56,7 +66,7 @@
errorMessage.value = ''; saving.value = true
try {
if (getAccountProfile().phone != phone.value) throw new Error('登录状态已变化,请重新进入页面')
await saveAccountProfile(nickname.value, avatar.value)
await saveAccountProfile(nickname.value, avatarId.value)
originalName = nickname.value.trim(); originalAvatar = avatar.value
uni.showToast({ title: '资料已保存', icon: 'success' }); leave()
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '保存失败,请稍后重试' }
+10 -13
View File
@@ -4,7 +4,7 @@
<scroll-view scroll-y :style="contentStyle(226, false)">
<view class="profile-content">
<view class="profile-card" @click="openAccount"><view class="avatar"><image class="profile-avatar" :src="avatar.length > 0 ? avatar : DEFAULT_AVATAR" mode="aspectFill" @error="avatar = ''" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text v-if="!loggedIn" class="account-state">未登录</text><AppIcon v-if="loggedIn" name="chevron-right" :size="28" /></view>
<view class="stat-row"><view class="stat-item" @click="go('/packages/finance/archive/archive')"><text class="stat-value">{{ completion }}%</text><text class="stat-label">档案完整度</text></view><view class="stat-item" @click="openReport"><text class="stat-value">{{ hasReport ? '已生成' : '暂无' }}</text><text class="stat-label">评估报告</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
<view class="stat-row"><view class="stat-item" @click="go('/packages/finance/archive/archive')"><text class="stat-value">查看</text><text class="stat-label">评估记录</text></view><view class="stat-item" @click="openReport"><text class="stat-value">查看</text><text class="stat-label">评估报告</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
<view class="menu-list"><view class="menu-row" v-for="item in menus" :key="item.name" @click="open(item.key)"><AppIcon :name="item.icon" :size="46" /><text class="menu-name">{{ item.name }}</text><text v-if="item.note.length > 0" class="menu-note">{{ item.note }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
<text class="version-note">AI融资 · 让融资更简单</text>
@@ -15,7 +15,7 @@
</template>
<script setup lang="uts">
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue'
@@ -23,8 +23,8 @@
import AppIcon from '@/components/AppIcon.uvue'
import { getSession } from '@/services/api.uts'
import { logoutAccount } from '@/packages/account/services/auth.uts'
import { activeAssessmentType, assessmentProgress, showAgreementNotice } from '@/services/assessment.uts'
import { DEFAULT_AVATAR, getAccountProfile, maskPhone } from '@/packages/account/services/account.uts'
import { showAgreementNotice } from '@/services/assessment.uts'
import { DEFAULT_AVATAR, getAccountProfile, fetchAccountProfile, maskPhone } from '@/packages/account/services/account.uts'
const { contentStyle, pageStyle } = usePageLayout()
type ProfileMenu = { icon: string, name: string, note: string, key: string }
const menus: ProfileMenu[] = [{ icon: 'archive', name: '我的融资档案', note: '', key: 'archive' }, { icon: 'shield', name: '账户安全', note: '', key: 'security' }, { icon: 'privacy', name: '隐私与授权', note: '', key: 'privacy' }, { icon: 'help', name: '帮助与反馈', note: '', key: 'help' }, { icon: 'info', name: '关于我们', note: 'V1.0.0', key: 'about' }]
@@ -32,32 +32,29 @@
const displayName = ref('登录 / 注册')
const maskedPhone = ref('')
const avatar = ref('')
const completion = ref(0)
const hasReport = ref(false)
onShow(() => {
const session = getSession()
loggedIn.value = session != null
const profile = getAccountProfile()
maskedPhone.value = maskPhone(profile.phone); avatar.value = loggedIn.value ? profile.avatar : ''
const type = activeAssessmentType()
displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册'
completion.value = assessmentProgress(type).percent
const latest = getUserStorage('latestAssessment')
hasReport.value = latest != null && typeof latest == 'object'
if (loggedIn.value) fetchAccountProfile().then((fresh) => {
displayName.value = fresh.nickname; avatar.value = fresh.avatar; maskedPhone.value = maskPhone(fresh.phone)
}).catch(() => { /* 读取失败保留已归属当前账户的缓存,不显示其他账户资料。 */ })
})
const go = (url: string) => { uni.navigateTo({ url }) }
const openReport = () => { if (hasReport.value) go('/packages/finance/report/report'); else uni.showModal({ title: '暂无评估报告', content: '完善融资信息后,即可体验融资评估与方案匹配。', confirmText: '去完善', success: (result) => { if (result.confirm) go('/packages/finance/archive/archive') } }) }
const openReport = () => { go('/packages/finance/archive/archive') }
const open = (key: string) => {
if (key == 'archive') go('/packages/finance/archive/archive')
else if (key == 'help') go('/packages/content/help/help')
else if (key == 'security') go(loggedIn.value ? '/packages/account/security/security' : '/packages/account/login/login')
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { setUserStorage('assessmentEditStep', 3); go('/packages/finance/assessment/assessment?type=' + activeAssessmentType()) } } })
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { go('/packages/finance/archive/archive') } } })
else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识与融资方案。评估结果仅供参考,不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
}
const openAccount = () => { go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') }
const logout = () => {
if (!loggedIn.value) { go('/packages/account/login/login'); return }
uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) {
uni.showModal({ title: '退出登录', content: '将清除本机账户缓存和未提交的填写内容,已提交的评估记录不受影响。', confirmText: '退出登录', success: (result) => { if (result.confirm) {
logoutAccount().then(() => { uni.reLaunch({ url: '/packages/account/login/login' }) })
.catch(() => { uni.showToast({ title: '退出失败,请检查网络后重试', icon: 'none' }) })
} }, fail: (error) => { console.error('logout confirmation failed', error); uni.showToast({ title: '暂时无法打开确认框,请重试', icon: 'none' }) } })
+24 -32
View File
@@ -1,6 +1,6 @@
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
import { API_BASE_URL, getSession, requestJson } from '@/services/api.uts'
import { activeAssessmentType, loadAssessmentDraft, draftText } from '@/services/assessment.uts'
import { getSession, requestJson, uploadJson } from '@/services/api.uts'
import { draftText } from '@/services/assessment.uts'
export type AccountProfile = { nickname: string, avatar: string, phone: string }
export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png'
@@ -27,9 +27,6 @@ export function getAccountProfile(): AccountProfile {
const saved = getUserStorage(profileKey(phone))
if (saved != null && typeof saved == 'object') {
nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar')
} else {
const draft = loadAssessmentDraft(activeAssessmentType())
if (draft != null && draftText(draft, 'phone') == phone && draftText(draft, 'name').length > 0) nickname = draftText(draft, 'name')
}
}
return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar }
@@ -40,13 +37,32 @@ async function accountRequest(path: string, method: RequestMethod, payload: UTSJ
requireAccount()
return await requestJson(path, method, payload)
}
export async function saveAccountProfile(nickname: string, avatar: string): Promise<void> {
function cacheProfile(result: UTSJSONObject, phone: string): AccountProfile {
const profile: AccountProfile = { phone, nickname: draftText(result, 'nickname'), avatar: draftText(result, 'avatarUrl') }
setUserStorage(profileKey(phone), { nickname: profile.nickname, avatar: profile.avatar })
return profile
}
export async function fetchAccountProfile(): Promise<AccountProfile> {
const session = requireAccount()
const result = await accountRequest('/account/profile', 'GET', {})
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
return cacheProfile(result, draftText(session, 'phone'))
}
export async function saveAccountProfile(nickname: string, avatarId: string): Promise<void> {
const session = requireAccount(); const phone = draftText(session, 'phone')
const name = nickname.trim()
if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符')
if (API_BASE_URL.length > 0) await accountRequest('/account/profile', 'PUT', { nickname: name, avatar })
const payload: UTSJSONObject = { nickname: name }
if (avatarId == 'default') payload['avatarId'] = null
else if (avatarId.length > 0) payload['avatarId'] = avatarId
const result = await accountRequest('/account/profile', 'PUT', payload)
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
setUserStorage(profileKey(phone), { nickname: name, avatar })
cacheProfile(result, phone)
}
export async function uploadAccountAvatar(path: string): Promise<UTSJSONObject> {
requireAccount()
return await uploadJson('/account/avatar', path)
}
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效')
@@ -68,27 +84,3 @@ export async function changeAccountPhone(newPhone: string, currentCode: string,
if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码')
await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode })
}
// 限制头像体积,避免把临时路径或过大的图片写入账户缓存。
export function readAvatar(path: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
// #ifdef WEB
fetch(path).then((response) => response.blob()).then((blob) => {
if (blob.size > 512000 || !['image/jpeg', 'image/png', 'image/webp'].includes(blob.type)) { reject(new Error('请选择 500KB 以内的 JPG、PNG 或 WebP 图片')); return }
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.onerror = () => reject(new Error('图片读取失败,请重新选择'))
reader.readAsDataURL(blob)
}).catch(() => reject(new Error('图片读取失败,请重新选择')))
// #endif
// #ifndef WEB
uni.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: (result) => {
const data = result.data as string
if (data.length > 683000) { reject(new Error('请选择 500KB 以内的图片')); return }
const mime = data.startsWith('/9j/') ? 'image/jpeg' : (data.startsWith('iVBOR') ? 'image/png' : (data.startsWith('UklGR') ? 'image/webp' : ''))
if (mime.length == 0) { reject(new Error('请选择 JPG、PNG 或 WebP 图片')); return }
resolve('data:' + mime + ';base64,' + data)
}, fail: () => reject(new Error('图片读取失败,请重新选择')) })
// #endif
})
}