refactor: 重构存储与接口适配,统一金额单位与会话管理
- 新增配置、存储、金额转换工具服务 - 按userId隔离本地缓存,替换旧设备级存储 - 统一移动端金额单位为万元,新增转换适配逻辑 - 重构认证与请求逻辑,支持真实后端会话 - 调整搜索、帮助、融资模块的模拟开关与数据适配 - 更新文档说明开发状态与配置规则
This commit is contained in:
@@ -24,12 +24,12 @@
|
||||
<script setup lang="uts">
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import BrandLogo from '@/components/BrandLogo.uvue'
|
||||
import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/packages/account/services/auth.uts'
|
||||
import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword } from '@/packages/account/services/auth.uts'
|
||||
import { showAgreementNotice } from '@/services/assessment.uts'
|
||||
import { consumeInitialLaunch } from '@/services/launch.uts'
|
||||
onLoad(() => { consumeInitialLaunch() })
|
||||
const { statusStyle, contentStyle, pageStyle } = usePageLayout()
|
||||
const mode = ref('phone')
|
||||
const mode = ref('password')
|
||||
const phone = ref('')
|
||||
const password = ref('')
|
||||
const smsCode = ref('')
|
||||
@@ -58,7 +58,7 @@
|
||||
if (countdown.value <= 1) { countdown.value = 0; clearInterval(countdownTimer) } else countdown.value -= 1
|
||||
}, 1000)
|
||||
}
|
||||
const finishLogin = (token: string) => { saveSession(token, phone.value); uni.redirectTo({ url: '/pages/home/home' }) }
|
||||
const finishLogin = (_token: string) => { uni.redirectTo({ url: '/pages/home/home' }) }
|
||||
const resendSms = async () => {
|
||||
if (loading.value || countdown.value > 0 || !validPhone()) return
|
||||
if (!agreed.value) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return }
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import BottomNav from '@/components/BottomNav.uvue'
|
||||
@@ -36,14 +38,14 @@
|
||||
const unreadCount = computed((): number => notices.value.filter((item: Notice): boolean => item.unread).length)
|
||||
onShow(() => {
|
||||
notices.value.forEach((item: Notice): void => {
|
||||
if (uni.getStorageSync('finance.notice.read.' + item.id) == true) item.unread = false
|
||||
if (getUserStorage('finance.notice.read.' + item.id) == true) item.unread = false
|
||||
})
|
||||
})
|
||||
const markRead = (id: string): void => {
|
||||
const item = notices.value.find((value: Notice): boolean => value.id == id)
|
||||
if (item == null) return
|
||||
item.unread = false
|
||||
uni.setStorageSync('finance.notice.read.' + id, true)
|
||||
setUserStorage('finance.notice.read.' + id, true)
|
||||
}
|
||||
const openNotice = (item: Notice): void => {
|
||||
markRead(item.id)
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
@@ -58,7 +60,7 @@
|
||||
}
|
||||
const restore = (): void => {
|
||||
today.value = localDate()
|
||||
const stored = uni.getStorageSync('finance.points.checkin.days')
|
||||
const stored = getUserStorage('finance.points.checkin.days')
|
||||
if (typeof stored != 'string' || stored == '') { signDays.value = []; return }
|
||||
const restored = (stored as string).split(',')
|
||||
const unique: string[] = []
|
||||
@@ -71,7 +73,7 @@
|
||||
if (signedToday.value) { uni.showToast({ title: '今天已签到,明天再来', icon: 'none' }); return }
|
||||
const next = signDays.value.slice()
|
||||
next.unshift(today.value)
|
||||
uni.setStorageSync('finance.points.checkin.days', next.join(','))
|
||||
setUserStorage('finance.points.checkin.days', next.join(','))
|
||||
signDays.value = next
|
||||
recordsExpanded.value = true
|
||||
uni.showToast({ title: '签到完成,体验积分 +10', icon: 'none' })
|
||||
|
||||
@@ -15,11 +15,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import BottomNav from '@/components/BottomNav.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
import { clearSession, getSession } from '@/services/api.uts'
|
||||
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'
|
||||
const { contentStyle, pageStyle } = usePageLayout()
|
||||
@@ -39,7 +42,7 @@
|
||||
const type = activeAssessmentType()
|
||||
displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册'
|
||||
completion.value = assessmentProgress(type).percent
|
||||
const latest = uni.getStorageSync('latestAssessment')
|
||||
const latest = getUserStorage('latestAssessment')
|
||||
hasReport.value = latest != null && typeof latest == 'object'
|
||||
})
|
||||
const go = (url: string) => { uni.navigateTo({ url }) }
|
||||
@@ -48,13 +51,16 @@
|
||||
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) { uni.setStorageSync('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) { setUserStorage('assessmentEditStep', 3); go('/packages/finance/assessment/assessment?type=' + activeAssessmentType()) } } })
|
||||
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) { clearSession(); uni.reLaunch({ url: '/packages/account/login/login' }) } } })
|
||||
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' }) } })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { API_BASE_URL, getSession } from '@/services/api.uts'
|
||||
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'
|
||||
|
||||
export type AccountProfile = { nickname: string, avatar: string, phone: string }
|
||||
@@ -23,7 +24,7 @@ export function getAccountProfile(): AccountProfile {
|
||||
const phone = session == null ? '' : draftText(session, 'phone')
|
||||
let nickname = '融资用户'; let avatar = ''
|
||||
if (phone.length > 0) {
|
||||
const saved = uni.getStorageSync(profileKey(phone))
|
||||
const saved = getUserStorage(profileKey(phone))
|
||||
if (saved != null && typeof saved == 'object') {
|
||||
nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar')
|
||||
} else {
|
||||
@@ -35,19 +36,9 @@ export function getAccountProfile(): AccountProfile {
|
||||
}
|
||||
|
||||
// 安全接口必须经过服务端认证。未配置服务时不模拟验证或修改成功。
|
||||
function accountRequest(path: string, method: RequestMethod, payload: UTSJSONObject): Promise<UTSJSONObject> {
|
||||
return new Promise<UTSJSONObject>((resolve, reject) => {
|
||||
const session = requireAccount()
|
||||
if (API_BASE_URL.length == 0) { reject(new Error('账户服务暂未开放,请稍后再试')); return }
|
||||
uni.request({ url: API_BASE_URL + path, method, data: payload, header: { Authorization: 'Bearer ' + draftText(session, 'token') }, timeout: 10000,
|
||||
success: (response) => {
|
||||
if (!sameAccount(session)) { reject(new Error('登录状态已变化,请重新进入页面')); return }
|
||||
if (response.statusCode == 401) { reject(new Error('登录已失效,请重新登录')); return }
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) { reject(new Error('操作失败,请核对信息后重试')); return }
|
||||
resolve(response.data == null ? new UTSJSONObject() : response.data as UTSJSONObject)
|
||||
}, fail: () => reject(new Error('网络连接失败,请稍后重试'))
|
||||
})
|
||||
})
|
||||
async function accountRequest(path: string, method: RequestMethod, payload: UTSJSONObject): Promise<UTSJSONObject> {
|
||||
requireAccount()
|
||||
return await requestJson(path, method, payload)
|
||||
}
|
||||
export async function saveAccountProfile(nickname: string, avatar: string): Promise<void> {
|
||||
const session = requireAccount(); const phone = draftText(session, 'phone')
|
||||
@@ -55,7 +46,7 @@ export async function saveAccountProfile(nickname: string, avatar: string): Prom
|
||||
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 })
|
||||
if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面')
|
||||
uni.setStorageSync(profileKey(phone), { nickname: name, avatar })
|
||||
setUserStorage(profileKey(phone), { nickname: name, avatar })
|
||||
}
|
||||
export async function requestSecurityCode(purpose: string, newPhone: string): Promise<void> {
|
||||
if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效')
|
||||
|
||||
@@ -1,35 +1,40 @@
|
||||
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||
export { API_BASE_URL, saveSession } from '@/services/api.uts'
|
||||
import { API_BASE_URL, requestJson, saveSession, clearSession, getSession } from '@/services/api.uts'
|
||||
export { API_BASE_URL } from '@/services/api.uts'
|
||||
import { setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
export async function requestSmsCode(phone: string): Promise<boolean> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(350)
|
||||
return phone.length == 11
|
||||
}
|
||||
await requestJson('/auth/sms-code', 'POST', { phone })
|
||||
return true
|
||||
await requestJson('/auth/sms-code', 'POST', { phone, purpose: 'login' })
|
||||
return true
|
||||
}
|
||||
|
||||
function acceptSession(result: UTSJSONObject, phone: string): string {
|
||||
const token = result['token'] as string | null
|
||||
const userId = result['userId'] as string | null
|
||||
const expiresAt = result['expiresAt'] as string | null
|
||||
if (token == null || userId == null || expiresAt == null) throw new Error('登录响应缺少会话信息')
|
||||
saveSession(token, phone, userId, expiresAt)
|
||||
const profile = result['profile'] as UTSJSONObject | null
|
||||
if (profile != null) setUserStorage('finance.account.profile:' + phone, { nickname: profile['nickname'], avatar: profile['avatarUrl'] ?? '' })
|
||||
return token
|
||||
}
|
||||
|
||||
export async function loginWithSms(phone: string, code: string): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(300)
|
||||
if (code.length != 6) throw new Error('验证码应为 6 位数字')
|
||||
return 'demo-sms-token'
|
||||
}
|
||||
const result = await requestJson('/auth/sms-login', 'POST', { phone, code })
|
||||
const token = result['token'] as string | null
|
||||
if (token == null || token.length == 0) throw new Error('登录响应缺少 token')
|
||||
return token
|
||||
return acceptSession(await requestJson('/auth/sms-login', 'POST', { phone, code }), phone)
|
||||
}
|
||||
|
||||
export async function loginWithPassword(phone: string, password: string): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
await wait(300)
|
||||
return 'demo-password-token'
|
||||
}
|
||||
const result = await requestJson('/auth/password-login', 'POST', { phone, password })
|
||||
const token = result['token'] as string | null
|
||||
if (token == null || token.length == 0) throw new Error('登录响应缺少 token')
|
||||
return token
|
||||
return acceptSession(await requestJson('/auth/password-login', 'POST', { phone, password }), phone)
|
||||
}
|
||||
|
||||
export async function logoutAccount(): Promise<void> {
|
||||
const session = getSession()
|
||||
if (session == null) { clearSession(); return }
|
||||
try {
|
||||
await requestJson('/auth/logout', 'POST', {})
|
||||
} catch (error) {
|
||||
// 401已清理会话,视为退出完成;网络失败保留会话以便重试撤销。
|
||||
if (getSession() != null) throw error
|
||||
}
|
||||
const current = getSession()
|
||||
if (current == null || current['token'] == session['token']) clearSession()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { getUserStorage, setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import { financeLessons, findFinanceLesson, FinanceLesson } from '@/packages/content/services/content.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
@@ -41,7 +43,7 @@
|
||||
const scrollTarget = ref('')
|
||||
const storageKey = (index: number): string => 'finance.lesson.' + lesson.value.id + '.chapter.' + index
|
||||
const restoreProgress = (): void => {
|
||||
completed.value = lesson.value.chapters.map((_item, index): boolean => uni.getStorageSync(storageKey(index)) == true)
|
||||
completed.value = lesson.value.chapters.map((_item, index): boolean => getUserStorage(storageKey(index)) == true)
|
||||
}
|
||||
const completedCount = computed((): number => completed.value.filter((value: boolean): boolean => value).length)
|
||||
const progress = computed((): number => Math.round(completedCount.value * 100 / lesson.value.chapters.length))
|
||||
@@ -59,7 +61,7 @@
|
||||
})
|
||||
const toggleChapter = (index: number): void => { activeIndex.value = activeIndex.value == index ? -1 : index }
|
||||
const completeChapter = (index: number): void => {
|
||||
uni.setStorageSync(storageKey(index), true)
|
||||
setUserStorage(storageKey(index), true)
|
||||
const next = completed.value.slice()
|
||||
next[index] = true
|
||||
completed.value = next
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { API_BASE_URL, requestJson } from '@/services/api.uts'
|
||||
import { API_DEMO_ENABLED, API_BASE_URL, requestJson } from '@/services/api.uts'
|
||||
|
||||
export type HelpQuestion = { id: string, category: string, question: string, answer: string }
|
||||
export const helpCategories: string[] = ['账户使用', '融资评估', '资料与隐私']
|
||||
@@ -8,13 +8,13 @@ const questions: HelpQuestion[] = [
|
||||
{ id: 'phone', category: '账户使用', question: '如何更换绑定手机号?', answer: '进入“我的 → 账户安全 → 绑定手机号”,填写新手机号,并分别验证当前号码与新号码。换绑成功后使用新号码登录。如果原号码无法接收短信,需要通过官方客服核验身份。' },
|
||||
{ id: 'assessment', category: '融资评估', question: '如何开始融资评估?', answer: '从首页选择个人或企业融资入口,依次填写基本信息、收入或经营信息、资产与负债情况,再核对并确认。已有资料可在“我的融资档案”中继续完善。' },
|
||||
{ id: 'result', category: '融资评估', question: '评估结果是否代表贷款已经获批?', answer: '不代表。评估结果用于帮助了解融资需求和方案,不是金融机构的授信或放款承诺。实际额度、利率、审批与放款结果,以相关机构的审核和合同为准。' },
|
||||
{ id: 'amount', category: '融资评估', question: '资产与负债信息应该怎么填写?', answer: '信用贷款笔数、信用卡张数填写非负整数,没有则填 0。欠款余额、月还款金额、信用卡额度与已使用额度以元填写,最多保留两位小数;房产、车辆按实际情况选择“有”或“无”。' },
|
||||
{ id: 'amount', category: '融资评估', question: '资产与负债信息应该怎么填写?', answer: '信用贷款笔数、信用卡张数填写非负整数,没有则填 0。欠款余额、月还款金额、信用卡额度与已使用额度统一以万元填写,最多保留六位小数,最高 99999.999999 万元;房产、车辆按实际情况选择“有”或“无”。' },
|
||||
{ id: 'files', category: '资料与隐私', question: '选择资料或勾选授权会立即上传、查询吗?', answer: '当前选择的资料仅用于本机预览,勾选授权仅保存您的选择,不会自动上传资料或查询外部数据。使用相关服务前,请确认授权对象、用途和范围。' },
|
||||
{ id: 'draft', category: '资料与隐私', question: '退出登录后,填写的资料还在吗?', answer: '退出登录会结束当前会话,本机保存的评估草稿仍会保留。个人与企业评估分别保存。清理应用数据、更换设备或卸载应用可能使本地资料丢失,请勿将本机草稿视为云端备份。' }
|
||||
]
|
||||
|
||||
export async function fetchHelpQuestions(): Promise<HelpQuestion[]> {
|
||||
if (API_BASE_URL.length == 0) return questions.slice()
|
||||
if (API_DEMO_ENABLED) return questions.slice()
|
||||
const response = await requestJson('/help/faqs', 'GET', {})
|
||||
const items = response['items'] as UTSJSONObject[] | null
|
||||
if (items == null) throw new Error('帮助内容加载失败')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { API_BASE_URL, requestJson } from '@/services/api.uts'
|
||||
import { API_DEMO_ENABLED, API_BASE_URL, requestJson } from '@/services/api.uts'
|
||||
import { newsArticles, financeLessons, ContentSection } from './content.uts'
|
||||
|
||||
export type ContentSearchResult = {
|
||||
@@ -14,7 +14,7 @@ export async function searchContent(keyword: string, type: string): Promise<Cont
|
||||
const query = keyword.trim().replace(/\s+/g, ' ')
|
||||
const results: ContentSearchResult[] = []
|
||||
if (query.length == 0) return results
|
||||
if (API_BASE_URL.length > 0) {
|
||||
if (!API_DEMO_ENABLED) {
|
||||
const response = await requestJson('/content/search', 'GET', { keyword: query, type })
|
||||
const items = response['items'] as UTSJSONObject[] | null
|
||||
if (items == null) throw new Error('搜索结果加载失败,请重试')
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
@@ -28,8 +30,8 @@
|
||||
])
|
||||
const refresh = () => { progress.value = assessmentProgress(type.value) }
|
||||
onShow(() => { type.value = activeAssessmentType(); refresh() })
|
||||
const switchType = (next: string) => { type.value = next; uni.setStorageSync('assessmentActiveType', next); refresh() }
|
||||
const edit = (step: number) => { uni.setStorageSync('assessmentEditStep', step); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
||||
const switchType = (next: string) => { type.value = next; setUserStorage('assessmentActiveType', next); refresh() }
|
||||
const edit = (step: number) => { setUserStorage('assessmentEditStep', step); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
|
||||
const complete = () => {
|
||||
if (progress.value.percent == 100) uni.navigateTo({ url: '/packages/finance/confirm/confirm' })
|
||||
else if (!progress.value.basicDone) edit(0)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<template v-else>
|
||||
<AssessmentSelect label="职业类型" v-model="form.occupation" :options="['企业职员', '事业单位', '公务员', '个体经营', '自由职业', '其他']" />
|
||||
<view class="form-row"><text class="field-label">工作单位</text><input v-model="form.company" class="field-input" placeholder="请输入工作单位" /></view>
|
||||
<view class="form-row"><text class="field-label">月收入(元)</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /></view>
|
||||
<view class="form-row"><text class="field-label">月收入(万元)</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /></view>
|
||||
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||
</template>
|
||||
@@ -33,12 +33,12 @@
|
||||
<view v-else-if="step == 2" class="form-list">
|
||||
<text class="asset-section-title">贷款信息</text>
|
||||
<view class="form-row"><text class="field-label">信用贷款笔数</text><input v-model="form.creditLoanCount" class="field-input" type="number" maxlength="4" placeholder="无则填 0" /><text class="field-unit">笔</text></view>
|
||||
<view class="form-row"><text class="field-label">欠款总余额</text><input v-model="form.totalDebtBalance" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">元</text></view>
|
||||
<view class="form-row"><text class="field-label">贷款月还款金额</text><input v-model="form.monthlyLoanPayment" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">元</text></view>
|
||||
<view class="form-row"><text class="field-label">欠款总余额</text><input v-model="form.totalDebtBalance" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<view class="form-row"><text class="field-label">贷款月还款金额</text><input v-model="form.monthlyLoanPayment" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<text class="asset-section-title">信用卡信息</text>
|
||||
<view class="form-row"><text class="field-label">银行信用卡张数</text><input v-model="form.creditCardCount" class="field-input" type="number" maxlength="4" placeholder="无则填 0" /><text class="field-unit">张</text></view>
|
||||
<view class="form-row"><text class="field-label">总额度</text><input v-model="form.creditCardTotalLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">元</text></view>
|
||||
<view class="form-row"><text class="field-label">已使用总额度</text><input v-model="form.creditCardUsedLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">元</text></view>
|
||||
<view class="form-row"><text class="field-label">总额度</text><input v-model="form.creditCardTotalLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<view class="form-row"><text class="field-label">已使用总额度</text><input v-model="form.creditCardUsedLimit" class="field-input" type="digit" maxlength="15" placeholder="请输入金额" /><text class="field-unit">万元</text></view>
|
||||
<text class="asset-section-title">房车信息</text>
|
||||
<AssessmentSelect label="是否有房" v-model="form.house" :options="['有', '无']" />
|
||||
<AssessmentSelect label="是否有车" v-model="form.car" :options="['有', '无']" />
|
||||
@@ -58,6 +58,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { getUserStorage, removeUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { wanAmountValid } from '@/services/money.uts'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import AppIcon from '@/components/AppIcon.uvue'
|
||||
@@ -75,7 +78,7 @@
|
||||
const documents = ref<SelectedDocument[]>([])
|
||||
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
|
||||
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
|
||||
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '银行卡', '收入证明'])
|
||||
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '身份证反面', '银行卡', '收入证明'])
|
||||
let initialized = false
|
||||
const saveDraft = () => {
|
||||
if (!initialized) return
|
||||
@@ -106,8 +109,8 @@
|
||||
restore(type)
|
||||
})
|
||||
onShow(() => {
|
||||
const requestedStep = uni.getStorageSync('assessmentEditStep')
|
||||
if (typeof requestedStep == 'number') { step.value = Math.max(0, Math.min(3, requestedStep as number)); uni.removeStorageSync('assessmentEditStep'); saveDraft() }
|
||||
const requestedStep = getUserStorage('assessmentEditStep')
|
||||
if (typeof requestedStep == 'number') { step.value = Math.max(0, Math.min(3, requestedStep as number)); removeUserStorage('assessmentEditStep'); saveDraft() }
|
||||
})
|
||||
onHide(() => saveDraft())
|
||||
onUnload(() => saveDraft())
|
||||
@@ -149,7 +152,7 @@
|
||||
}
|
||||
if (step.value == 1) {
|
||||
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
||||
if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效的年营业收入' : '请输入有效的月收入'
|
||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效年营收(万元,最多六位小数)' : '请输入有效月收入(万元,最多六位小数)'
|
||||
}
|
||||
if (step.value == 2) message = assetValidationMessage(form.value)
|
||||
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { setUserStorage } from '@/services/storage.uts'
|
||||
|
||||
import { wanAmountValid } from '@/services/money.uts'
|
||||
import { usePageLayout } from '@/services/layout.uts'
|
||||
import AppHeader from '@/components/AppHeader.uvue'
|
||||
import { submitAssessment } from '@/packages/finance/services/api.uts'
|
||||
@@ -53,7 +56,7 @@
|
||||
const incomeRows: InfoRow[] = enterprise ? [
|
||||
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营年限', value: displayValue(saved, 'operatingYears') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
||||
] : [
|
||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 万元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||
]
|
||||
const files = documentsFromDraft(saved)
|
||||
const fileNames = files.map((file) => file.name).join('、')
|
||||
@@ -65,11 +68,11 @@
|
||||
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
||||
{ title: '资产与负债', step: 2, rows: [
|
||||
{ label: '信用贷款笔数', value: valueWithUnit(saved, 'creditLoanCount', '笔') },
|
||||
{ label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '元') },
|
||||
{ label: '贷款月还款金额', value: valueWithUnit(saved, 'monthlyLoanPayment', '元') },
|
||||
{ label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '万元') },
|
||||
{ label: '贷款月还款金额', value: valueWithUnit(saved, 'monthlyLoanPayment', '万元') },
|
||||
{ label: '银行信用卡张数', value: valueWithUnit(saved, 'creditCardCount', '张') },
|
||||
{ label: '总额度', value: valueWithUnit(saved, 'creditCardTotalLimit', '元') },
|
||||
{ label: '已使用总额度', value: valueWithUnit(saved, 'creditCardUsedLimit', '元') },
|
||||
{ label: '总额度', value: valueWithUnit(saved, 'creditCardTotalLimit', '万元') },
|
||||
{ label: '已使用总额度', value: valueWithUnit(saved, 'creditCardUsedLimit', '万元') },
|
||||
{ label: '是否有房', value: ownershipFromDraft(saved, 'house').length > 0 ? ownershipFromDraft(saved, 'house') : '未填写' },
|
||||
{ label: '是否有车', value: ownershipFromDraft(saved, 'car').length > 0 ? ownershipFromDraft(saved, 'car') : '未填写' }
|
||||
], expanded: false },
|
||||
@@ -79,7 +82,7 @@
|
||||
}
|
||||
onShow(() => loadDraft())
|
||||
const edit = (step: number) => {
|
||||
uni.setStorageSync('assessmentEditStep', step)
|
||||
setUserStorage('assessmentEditStep', step)
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/assessment/assessment') uni.navigateBack({ delta: 1 })
|
||||
else uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
|
||||
@@ -92,7 +95,7 @@
|
||||
if (draftText(saved, 'name').length < 2 || !/^1[3-9][0-9]{9}$/.test(draftText(saved, 'phone')) || draftText(saved, 'city').length == 0) { uni.showToast({ title: '请先完善基本信息', icon: 'none' }); edit(0); return }
|
||||
if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(draftText(saved, 'idCard'))) { uni.showToast({ title: '请补全身份证信息', icon: 'none' }); edit(0); return }
|
||||
if (assessmentType.value == 'enterprise' && (draftText(saved, 'enterpriseName').length < 2 || !/^[0-9A-Z]{18}$/.test(draftText(saved, 'businessCode').toUpperCase()))) { uni.showToast({ title: '请补全企业信息', icon: 'none' }); edit(0); return }
|
||||
if (amount.length == 0 || parseFloat(amount) <= 0 || isNaN(parseFloat(amount))) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||
if (!wanAmountValid(amount) || parseFloat(amount) <= 0) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||
const grants = saved['authorizations'] as string[] | null
|
||||
if ((documentsFromDraft(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
|
||||
const assetError = assetValidationMessage(formFromDraft(saved))
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<text class="result-summary">共 {{ matches.length }} 个方案 · 了解额度与申请条件</text>
|
||||
<view class="match-card" v-for="item in sortedMatches" :key="item.id" @click="openProduct(item.id)">
|
||||
<view class="bank-row"><view class="bank-logo" :style="'background-color:' + item.color"><text class="bank-initial">{{ item.initials }}</text></view><view class="bank-copy"><text class="bank-name">{{ item.bank }}</text><text class="match-tag">个人信用融资</text></view><text class="match-score">{{ item.score }}%匹配</text></view>
|
||||
<view class="metrics"><view class="metric"><text class="metric-value">{{ item.amount }}<text class="metric-unit"> 万</text></text><text class="metric-label">最高额度</text></view><view class="metric"><text class="metric-value rate-value">{{ item.rate.toFixed(2) }}%</text><text class="metric-label">年化利率(起)</text></view><view class="metric term-metric"><text class="term-value">{{ item.term }}</text><text class="metric-label">贷款期限</text></view></view>
|
||||
<view class="metrics"><view class="metric"><text class="metric-value">{{ item.amount }}<text class="metric-unit"> 万元</text></text><text class="metric-label">最高额度</text></view><view class="metric"><text class="metric-value rate-value">{{ item.rate.toFixed(2) }}%</text><text class="metric-label">年化利率(起)</text></view><view class="metric term-metric"><text class="term-value">{{ item.term }}</text><text class="metric-label">贷款期限</text></view></view>
|
||||
<view class="card-footer"><text class="footer-hint">{{ item.advantages.join(' · ') }}</text><view class="detail-link"><text class="detail-label">查看详情</text><AppIcon name="chevron-right" :size="26" /></view></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<view class="product-actions"><text class="share" @click="shareProduct">分享产品</text></view>
|
||||
<!-- #endif -->
|
||||
<view class="bank-head"><view class="bank-mark" :style="'background-color:' + product.color"><text class="bank-initial">{{ product.initials }}</text></view><view class="bank-copy"><text class="product-name">{{ product.bank }}</text><text class="product-tags">个人信用融资 · 产品信息</text></view></view>
|
||||
<view class="product-metrics"><view class="metric"><text class="metric-label">最高额度</text><text class="amount">{{ product.amount }}万</text></view><view class="metric"><text class="metric-label">年化利率(起)</text><text class="rate">{{ product.rate.toFixed(2) }}%</text></view><view class="metric last-metric"><text class="metric-label">贷款期限</text><text class="term">{{ product.term }}</text></view></view>
|
||||
<view class="product-metrics"><view class="metric"><text class="metric-label">最高额度</text><text class="amount">{{ product.amount }}万元</text></view><view class="metric"><text class="metric-label">年化利率(起)</text><text class="rate">{{ product.rate.toFixed(2) }}%</text></view><view class="metric last-metric"><text class="metric-label">贷款期限</text><text class="term">{{ product.term }}</text></view></view>
|
||||
<view class="detail-section"><text class="section-title">产品优势</text><view class="advantage-grid"><view class="advantage" v-for="item in product.advantages" :key="item"><AppIcon name="check-circle" :size="30" /><text class="advantage-label">{{ item }}</text></view></view></view>
|
||||
<view class="detail-section"><text class="section-title">申请条件</text><view class="condition"><text class="condition-label">年龄要求</text><text class="condition-value">{{ product.age }}</text></view><view class="condition"><text class="condition-label">工作要求</text><text class="condition-value">{{ product.work }}</text></view><view class="condition"><text class="condition-label">信用要求</text><text class="condition-value">{{ product.credit }}</text></view></view>
|
||||
<view class="detail-section"><text class="section-title">所需材料</text><text class="material">{{ product.materials }}</text></view>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||
import { setUserStorage } from '@/services/storage.uts'
|
||||
import { API_DEMO_ENABLED, API_BASE_URL, wait, requestJson } from '@/services/api.uts'
|
||||
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
|
||||
import { assessmentApiPayload } from '@/services/assessment.uts'
|
||||
export { API_BASE_URL } from '@/services/api.uts'
|
||||
|
||||
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
const apiPayload = assessmentApiPayload(payload)
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(650)
|
||||
uni.setStorageSync('latestAssessment', payload)
|
||||
setUserStorage('latestAssessment', apiPayload)
|
||||
return 'ASSESSMENT-DEMO-001'
|
||||
}
|
||||
const result = await requestJson('/assessments', 'POST', payload)
|
||||
const result = await requestJson('/assessments', 'POST', apiPayload)
|
||||
const id = result['id'] as string | null
|
||||
if (id == null || id.length == 0) throw new Error('评估提交成功但未返回编号')
|
||||
return id
|
||||
}
|
||||
|
||||
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(320)
|
||||
return demoProducts.slice()
|
||||
}
|
||||
@@ -25,7 +28,7 @@ export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||
}
|
||||
|
||||
export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(180)
|
||||
const products = demoProducts.filter((item: FinanceProduct): boolean => item.id == id)
|
||||
return products.length > 0 ? products[0] : null
|
||||
@@ -35,7 +38,7 @@ export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
|
||||
}
|
||||
|
||||
export async function sendAssistantMessage(message: string): Promise<string> {
|
||||
if (API_BASE_URL.length == 0) {
|
||||
if (API_DEMO_ENABLED) {
|
||||
await wait(600)
|
||||
return '结合您的评估结果,建议优先选择年化利率较低、可随借随还的产品,并重点确认提前还款规则。'
|
||||
}
|
||||
@@ -44,4 +47,3 @@ export async function sendAssistantMessage(message: string): Promise<string> {
|
||||
if (reply == null || reply.length == 0) throw new Error('助手暂时没有返回内容')
|
||||
return reply
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { yuanToWan } from '@/services/money.uts'
|
||||
|
||||
export type FinanceProduct = {
|
||||
id: string, bank: string, initials: string, color: string,
|
||||
amount: number, rate: number, term: string, score: number,
|
||||
@@ -12,10 +14,12 @@ export const demoProducts: FinanceProduct[] = [
|
||||
]
|
||||
|
||||
export function parseProduct(item: UTSJSONObject): FinanceProduct {
|
||||
// 接口额度使用元的十进制字符串,视图模型始终使用万元。
|
||||
const amountYuan = item['amountYuan'] as string | null
|
||||
return {
|
||||
id: item['id'] as string ?? '', bank: item['bank'] as string ?? '融资产品',
|
||||
initials: item['initials'] as string ?? '融', color: item['color'] as string ?? '#2869b7',
|
||||
amount: item['amount'] as number ?? 0, rate: item['rate'] as number ?? 0,
|
||||
amount: amountYuan != null ? parseFloat(yuanToWan(amountYuan)) : (item['amount'] as number ?? 0), rate: item['rate'] as number ?? 0,
|
||||
term: item['term'] as string ?? '以审批为准', score: item['score'] as number ?? 0,
|
||||
advantages: item['advantages'] as string[] ?? [], age: item['age'] as string ?? '以机构要求为准',
|
||||
work: item['work'] as string ?? '以机构要求为准', credit: item['credit'] as string ?? '以机构要求为准',
|
||||
|
||||
Reference in New Issue
Block a user