refactor: 重构项目分包结构与资源管理

1.  调整页面路由,将业务页面迁移到对应分包,主包仅保留首页
2.  拆分公共服务模块,将账户、融资相关逻辑移入对应分包
3.  统一资源路径,将专属静态资源放入各分包static目录,更新导航栏图标资源
4.  修复启动引导逻辑,实现仅首次冷启动显示引导页
5.  更新README文档,补充分包配置与开发规范说明
This commit is contained in:
hujun
2026-09-20 10:52:10 +08:00
parent 94a4558290
commit 69130d11e7
43 changed files with 278 additions and 180 deletions
+3 -1
View File
@@ -1,5 +1,7 @@
<script setup lang="uts"> <script setup lang="uts">
onLaunch(() => { import { consumeInitialLaunch } from '@/services/launch.uts'
onLaunch((options) => {
if (options.path.length > 0 && options.path != 'pages/home/home') consumeInitialLaunch()
console.log('AI融资已启动') console.log('AI融资已启动')
}) })
+29 -4
View File
@@ -1,10 +1,10 @@
# AI 融资移动端 # AI 融资移动端
基于 uni-app x、Vue 3、UTS 的移动端原型,包含 19 个页面。使用 HBuilderX 5.24(VDOM 模式)开发;本项目没有独立的 npm 构建入口。 基于 uni-app x、Vue 3、UTS 的移动端原型,包含 19 个页面。使用 HBuilderX(VDOM 模式)开发,已验证 5.24 和 5.26 的本地编译;本项目没有独立的 npm 构建入口。
## 本地运行 ## 本地运行
在 HBuilderX 中导入当前目录,选择“运行 → 运行到浏览器”。当前接口由 `services/api.uts` 提供本地模拟实现,没有连接真实短信、贷款申请或 AI 服务。 在 HBuilderX 中导入当前目录,选择“运行 → 运行到浏览器”。共享请求配置位于 `services/api.uts`,账户、融资的接口模拟实现分别位于对应分包的 `services/`,没有连接真实短信、贷款申请或 AI 服务。
也可使用 HBuilderX CLI。以下 PowerShell 命令中的安装路径应按本机环境调整;在项目根目录执行: 也可使用 HBuilderX CLI。以下 PowerShell 命令中的安装路径应按本机环境调整;在项目根目录执行:
@@ -33,7 +33,8 @@ $projectPath = (Get-Location).Path
- 页面尺寸和安全区计算集中在 `services/layout.uts`。新增页面应复用这里的逻辑,并检查小屏、横屏、键盘弹出及底部安全区。 - 页面尺寸和安全区计算集中在 `services/layout.uts`。新增页面应复用这里的逻辑,并检查小屏、横屏、键盘弹出及底部安全区。
- 原生样式遵循 uni-app x 支持范围。仅 Web/小程序支持的样式用条件编译隔离;修改 UTS 类型后应重新验证 Android 编译。 - 原生样式遵循 uni-app x 支持范围。仅 Web/小程序支持的样式用条件编译隔离;修改 UTS 类型后应重新验证 Android 编译。
- 线上使用的轻量位图位于 `static/mobile/`,原始图片保留在 `static/images/` 与 `static/icons-generated/`。箭头继续使用 `static/icons/` 下的 SVG。 - 首页及跨包共用的轻量位图位于 `static/mobile/`,专属大图位于各分包的 `static/`,原始图片保留在 `static/images/` 与 `static/icons-generated/`。箭头继续使用 `static/icons/` 下的 SVG。
- 底栏使用 `static/mobile/navigation/` 的专用位图,由现有高清图标按可见边界裁切、等高居中得到。更换素材时保持一致的图案高度和画布留白,避免仅统一图片元素尺寸却出现视觉错位。
- 微信打包忽略原始图片目录和未使用的 `static/logo.png`,规则位于 `manifest.json` 的 `mp-weixin.packOptions.ignore`。新增资源要检查引用与排除规则,最终包体以微信开发者工具结果为准。 - 微信打包忽略原始图片目录和未使用的 `static/logo.png`,规则位于 `manifest.json` 的 `mp-weixin.packOptions.ignore`。新增资源要检查引用与排除规则,最终包体以微信开发者工具结果为准。
- API 地址目前为空,表示模拟模式。接入后端时需补齐环境配置、认证、错误契约及真实业务校验,再进行联调。 - API 地址目前为空,表示模拟模式。接入后端时需补齐环境配置、认证、错误契约及真实业务校验,再进行联调。
@@ -41,4 +42,28 @@ $projectPath = (Get-Location).Path
个人与企业评估分别保存本机草稿;选图仅供本机预览,资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。 个人与企业评估分别保存本机草稿;选图仅供本机预览,资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。
新闻与课程的数据定义位于 `services/content.uts`,产品定义位于 `services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中 `amount` 为万元数值、`rate` 为年化百分比数值。申请入口目前仅引导准备资料,不向金融机构提交申请。 新闻与课程的数据定义位于 `packages/content/services/content.uts`,产品定义位于 `packages/finance/services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中 `amount` 为万元数值、`rate` 为年化百分比数值。申请入口目前仅引导准备资料,不向金融机构提交申请。
## 页面与分包
`pages/` 只保留 `home/home.uvue`,也是唯一主包页面。页面注册统一维护在 `pages.json` 的 `pages` 和 `subPackages`,分包内的 `path` 相对于 `root`。
| 目录 | 内容 | 页面数 |
| --- | --- | --- |
| `pages/home` | 首页 | 1 |
| `packages/finance` | 融资入口、评估、确认、分析、报告、匹配、产品、助手、档案 | 9 |
| `packages/account` | 启动引导、登录、个人中心、积分、消息 | 5 |
| `packages/content` | 新闻、文章、融资课堂、课程详情 | 4 |
当前各模块体积较小,优先保持完整业务链;普通内容统一一个包。新增功能先放入所属分包,再根据微信编译产物大小决定是否细分。单包以 2 MiB 为保守预算,需预留增长空间,最终体积以微信开发者工具为准。
2026-09-20 使用 HBuilderX 5.26 本地微信开发编译,按 `packOptions.ignore` 排除原始素材后统计:主包约 887 KiB,融资包 378 KiB,账户包 148 KiB,内容包 355 KiB。此为磁盘产物统计,不等同于开发者工具上传后的压缩体积。
- 分包页面使用完整绝对路由 `/packages/<模块>/<页面>/<页面>`,包含路由比较和携带参数的跳转;主包首页仍为 `/pages/home/home`。
- 微信按 `subPackages` 下载分包。Android、iOS 使用相同目录、页面注册和路由编译为应用,不将小程序按需下载机制等同于 App 热更新。
- 保留现有自绘 `BottomNav`,新闻、消息、我的允许在分包中;不配置要求页面留在主包的原生 `tabBar`。底栏切换使用 `reLaunch`,详情使用 `navigateTo` / `navigateBack`。
- 冷启动首页通过 `services/launch.uts` 只触发一次账户引导,之后按登录状态进入首页或登录;直接打开分包页面和正常返回首页不会重复触发引导。
- `components/` 与 `services/` 只保留共享组件、布局、请求、会话和跨包草稿。主包或其他分包不得 import 某个分包的专属模块;分包间只通过路由与共享服务交互。
- 仅某个分包使用的图片放在该包 `static/`;首页也使用的插图及通用图标保留在根 `static/`,避免分包资源被主包引用。
参考:[uni-app x 页面与分包配置](https://doc.dcloud.net.cn/uni-app-x/collocation/pagesjson.html#subpackages)。
+8 -6
View File
@@ -1,14 +1,13 @@
<template> <template>
<view class="bottom-nav" :style="bottomStyle(14)"> <view class="bottom-nav" :style="bottomStyle(14)">
<view class="nav-item" @click="go('/pages/home/home')"><view :class="active == 'home' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="home" :size="54" :muted="active != 'home'" /></view><text :class="active == 'home' ? 'nav-label active-label' : 'nav-label'">首页</text></view> <view class="nav-item" @click="go('/pages/home/home')"><view :class="active == 'home' ? 'nav-icon active-icon' : 'nav-icon'"><image class="nav-image" :class="active != 'home' ? 'muted-icon' : ''" src="/static/mobile/navigation/home.png" mode="aspectFit" /></view><text :class="active == 'home' ? 'nav-label active-label' : 'nav-label'">首页</text></view>
<view class="nav-item" @click="go('/pages/news/news')"><view :class="active == 'news' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="file-text" :size="54" :muted="active != 'news'" /></view><text :class="active == 'news' ? 'nav-label active-label' : 'nav-label'">新闻</text></view> <view class="nav-item" @click="go('/packages/content/news/news')"><view :class="active == 'news' ? 'nav-icon active-icon' : 'nav-icon'"><image class="nav-image" :class="active != 'news' ? 'muted-icon' : ''" src="/static/mobile/navigation/file-text.png" mode="aspectFit" /></view><text :class="active == 'news' ? 'nav-label active-label' : 'nav-label'">新闻</text></view>
<view class="nav-item" @click="go('/pages/messages/messages')"><view :class="active == 'message' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="message" :size="54" :muted="active != 'message'" /></view><text :class="active == 'message' ? 'nav-label active-label' : 'nav-label'">消息</text></view> <view class="nav-item" @click="go('/packages/account/messages/messages')"><view :class="active == 'message' ? 'nav-icon active-icon' : 'nav-icon'"><image class="nav-image" :class="active != 'message' ? 'muted-icon' : ''" src="/static/mobile/navigation/message.png" mode="aspectFit" /></view><text :class="active == 'message' ? 'nav-label active-label' : 'nav-label'">消息</text></view>
<view class="nav-item" @click="go('/pages/profile/profile')"><view :class="active == 'profile' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="user" :size="54" :muted="active != 'profile'" /></view><text :class="active == 'profile' ? 'nav-label active-label' : 'nav-label'">我的</text></view> <view class="nav-item" @click="go('/packages/account/profile/profile')"><view :class="active == 'profile' ? 'nav-icon active-icon' : 'nav-icon'"><image class="nav-image" :class="active != 'profile' ? 'muted-icon' : ''" src="/static/mobile/navigation/user.png" mode="aspectFit" /></view><text :class="active == 'profile' ? 'nav-label active-label' : 'nav-label'">我的</text></view>
</view> </view>
</template> </template>
<script setup lang="uts"> <script setup lang="uts">
import AppIcon from '@/components/AppIcon.uvue'
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
const { bottomStyle } = usePageLayout() const { bottomStyle } = usePageLayout()
defineProps({ active: { type: String, default: 'home' } }) defineProps({ active: { type: String, default: 'home' } })
@@ -23,7 +22,10 @@
.bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; min-height: 130rpx; background-color: #ffffff; border-top: 1rpx solid #e7edf6; flex-direction: row; box-shadow: 0 -6rpx 18rpx rgba(18, 38, 75, 0.04); } .bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; min-height: 130rpx; background-color: #ffffff; border-top: 1rpx solid #e7edf6; flex-direction: row; box-shadow: 0 -6rpx 18rpx rgba(18, 38, 75, 0.04); }
.nav-item { flex: 1; height: 116rpx; align-items: center; justify-content: center; } .nav-item { flex: 1; height: 116rpx; align-items: center; justify-content: center; }
.nav-icon { width: 64rpx; height: 60rpx; border-radius: 18rpx; align-items: center; justify-content: center; } .nav-icon { width: 64rpx; height: 60rpx; border-radius: 18rpx; align-items: center; justify-content: center; }
/* 专用位图已统一图案边界,不再叠加 AppIcon 的整体缩放。 */
.nav-image { width: 58rpx; height: 58rpx; flex-shrink: 0; }
.muted-icon { opacity: .70; }
.active-icon { background-color: #edf5ff; } .active-icon { background-color: #edf5ff; }
.nav-label { margin-top: 7rpx; font-size: 24rpx; color: #64748b; } .nav-label { margin-top: 7rpx; height: 34rpx; line-height: 34rpx; font-size: 24rpx; color: #64748b; text-align: center; }
.active-label { color: #0868f7; font-weight: 700; } .active-label { color: #0868f7; font-weight: 700; }
</style> </style>
+7 -6
View File
@@ -1,7 +1,7 @@
<template> <template>
<view class="brand-logo"> <view class="brand-logo">
<view class="brand-symbol"> <view class="brand-symbol">
<text class="letter-a">A</text><view class="brand-dot"></view><text class="letter-i">i</text> <text class="letter-a">A</text><view class="letter-i"><view class="brand-dot"></view><view class="brand-stem"></view></view>
</view> </view>
<text class="brand-name">融资</text> <text class="brand-name">融资</text>
</view> </view>
@@ -9,9 +9,10 @@
<style> <style>
.brand-logo { flex-direction: row; align-items: center; } .brand-logo { flex-direction: row; align-items: center; }
.brand-symbol { width: 96rpx; height: 68rpx; flex-direction: row; align-items: flex-end; position: relative; } .brand-symbol { width: 78rpx; height: 68rpx; flex-direction: row; align-items: center; }
.letter-a { color: #0871ff; font-size: 70rpx; line-height: 70rpx; font-weight: 900; } .letter-a { width: 53rpx; color: #0871ff; font-size: 70rpx; line-height: 68rpx; font-weight: 900; }
.letter-i { color: #102557; font-size: 55rpx; line-height: 62rpx; font-weight: 900; } .letter-i { width: 11rpx; height: 50rpx; margin-left: 3rpx; align-items: center; }
.brand-dot { position: absolute; width: 11rpx; height: 11rpx; border-radius: 50%; background-color: #ff5b35; left: 66rpx; top: 2rpx; } .brand-dot { width: 11rpx; height: 11rpx; border-radius: 50%; background-color: #ff5b35; }
.brand-name { color: #102557; font-size: 42rpx; line-height: 58rpx; font-weight: 900; } .brand-stem { width: 10rpx; height: 35rpx; margin-top: 4rpx; background-color: #102557; }
.brand-name { color: #102557; font-size: 52rpx; line-height: 68rpx; font-weight: 900; }
</style> </style>
@@ -6,7 +6,7 @@
<text class="tagline">智能评估 · 精准匹配</text> <text class="tagline">智能评估 · 精准匹配</text>
<text class="slogan">让融资更简单</text> <text class="slogan">让融资更简单</text>
</view> </view>
<image class="splash-art" src="/static/mobile/images/shield.png" mode="aspectFit"></image> <image class="splash-art" src="/packages/account/static/shield.png" mode="aspectFit"></image>
<view class="launch-line"><view class="launch-dot"></view><text class="launch-text">开启智能融资新体验</text></view> <view class="launch-line"><view class="launch-dot"></view><text class="launch-text">开启智能融资新体验</text></view>
</view> </view>
</template> </template>
@@ -16,13 +16,15 @@
const { statusStyle, pageStyle } = usePageLayout() const { statusStyle, pageStyle } = usePageLayout()
import BrandLogo from '@/components/BrandLogo.uvue' import BrandLogo from '@/components/BrandLogo.uvue'
import { isLoggedIn } from '@/services/api.uts' import { isLoggedIn } from '@/services/api.uts'
import { consumeInitialLaunch } from '@/services/launch.uts'
onLoad(() => { consumeInitialLaunch() })
let enterTimer = 0 let enterTimer = 0
let entered = false let entered = false
const enter = () => { const enter = () => {
if (entered) return if (entered) return
entered = true entered = true
clearTimeout(enterTimer) clearTimeout(enterTimer)
uni.redirectTo({ url: isLoggedIn() ? '/pages/home/home' : '/pages/login/login' }) uni.redirectTo({ url: isLoggedIn() ? '/pages/home/home' : '/packages/account/login/login' })
} }
onShow(() => { if (!entered) enterTimer = setTimeout(() => enter(), 1800) }) onShow(() => { if (!entered) enterTimer = setTimeout(() => enter(), 1800) })
onHide(() => { clearTimeout(enterTimer) }) onHide(() => { clearTimeout(enterTimer) })
@@ -15,7 +15,7 @@
<view class="mode-row"><text class="mode-link" @click="switchMode">{{ mode == 'phone' ? '密码登录' : '使用验证码登录' }}</text><text v-if="isDemo" class="demo-label">演示模式</text></view> <view class="mode-row"><text class="mode-link" @click="switchMode">{{ mode == 'phone' ? '密码登录' : '使用验证码登录' }}</text><text v-if="isDemo" class="demo-label">演示模式</text></view>
<text v-if="isDemo && smsSent" class="demo-hint">未发送真实短信,可输入任意 6 位数字体验。</text> <text v-if="isDemo && smsSent" class="demo-hint">未发送真实短信,可输入任意 6 位数字体验。</text>
<view class="other-login"><view class="line"></view><text class="other-text">其他登录方式</text><view class="line"></view></view> <view class="other-login"><view class="line"></view><text class="other-text">其他登录方式</text><view class="line"></view></view>
<view class="social-row"><view class="social-button" @click="socialLogin('微信')"><AppIcon name="message" :size="62" /><text class="social-label">微信</text></view><view class="social-button" @click="socialLogin('一键登录')"><AppIcon name="user" :size="62" /><text class="social-label">一键登录</text></view></view> <view class="social-row"><view class="social-button" @click="socialLogin('微信')"><image class="social-icon" src="/packages/account/static/wechat.svg" mode="aspectFit" /><text class="social-label">微信</text></view><view class="social-button" @click="socialLogin('一键登录')"><image class="social-icon" src="/packages/account/static/quick-login.svg" mode="aspectFit" /><text class="social-label">一键登录</text></view></view>
<view class="agreement"><view class="check-hit" @click="agreed = !agreed"><view :class="agreed ? 'check checked' : 'check'"><text v-if="agreed" class="check-mark">✓</text></view></view><text class="agreement-copy">我已阅读并同意<text class="agreement-link" @click="showAgreement('用户协议')">《用户协议》</text>和<text class="agreement-link" @click="showAgreement('隐私政策')">《隐私政策》</text></text></view> <view class="agreement"><view class="check-hit" @click="agreed = !agreed"><view :class="agreed ? 'check checked' : 'check'"><text v-if="agreed" class="check-mark">✓</text></view></view><text class="agreement-copy">我已阅读并同意<text class="agreement-link" @click="showAgreement('用户协议')">《用户协议》</text>和<text class="agreement-link" @click="showAgreement('隐私政策')">《隐私政策》</text></text></view>
</view> </view>
</scroll-view> </scroll-view>
@@ -25,9 +25,10 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import BrandLogo from '@/components/BrandLogo.uvue' import BrandLogo from '@/components/BrandLogo.uvue'
import AppIcon from '@/components/AppIcon.uvue' import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/packages/account/services/auth.uts'
import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/services/api.uts'
import { showAgreementNotice } from '@/services/assessment.uts' import { showAgreementNotice } from '@/services/assessment.uts'
import { consumeInitialLaunch } from '@/services/launch.uts'
onLoad(() => { consumeInitialLaunch() })
const { statusStyle, contentStyle, pageStyle } = usePageLayout() const { statusStyle, contentStyle, pageStyle } = usePageLayout()
const mode = ref('phone') const mode = ref('phone')
const phone = ref('') const phone = ref('')
@@ -113,6 +114,7 @@
.line { height: 1rpx; background-color: #e5eaf1; flex: 1; margin: 0 20rpx; } .line { height: 1rpx; background-color: #e5eaf1; flex: 1; margin: 0 20rpx; }
.social-row { flex-direction: row; justify-content: center; margin-top: 28rpx; } .social-row { flex-direction: row; justify-content: center; margin-top: 28rpx; }
.social-button { width: 150rpx; min-height: 112rpx; align-items: center; justify-content: center; margin: 0 26rpx; } .social-button { width: 150rpx; min-height: 112rpx; align-items: center; justify-content: center; margin: 0 26rpx; }
.social-icon { width: 62rpx; height: 62rpx; flex-shrink: 0; }
.social-label { color: #58677e; font-size: 24rpx; margin-top: 12rpx; } .social-label { color: #58677e; font-size: 24rpx; margin-top: 12rpx; }
.agreement { margin-top: 52rpx; flex-direction: row; align-items: flex-start; } .agreement { margin-top: 52rpx; flex-direction: row; align-items: flex-start; }
.check-hit { width: 44rpx; min-height: 44rpx; flex-shrink: 0; justify-content: center; } .check-hit { width: 44rpx; min-height: 44rpx; flex-shrink: 0; justify-content: center; }
@@ -86,7 +86,7 @@
const invite = (): void => { copyText('邀请你一起了解 AI融资:学习融资知识,梳理申请资料,提前做好融资准备。体验邀请码:AI2680。', '邀请文案已复制,可自行分享') } const invite = (): void => { copyText('邀请你一起了解 AI融资:学习融资知识,梳理申请资料,提前做好融资准备。体验邀请码:AI2680。', '邀请文案已复制,可自行分享') }
const doTask = (id: string): void => { const doTask = (id: string): void => {
if (id == 'checkin') { checkIn(); return } if (id == 'checkin') { checkIn(); return }
if (id == 'profile') { uni.navigateTo({ url: '/pages/archive/archive' }); return } if (id == 'profile') { uni.navigateTo({ url: '/packages/finance/archive/archive' }); return }
if (id == 'invite') { invite(); return } if (id == 'invite') { invite(); return }
copyText('分享一个融资准备方法:先梳理信用、收入与负债,再比较适合自己的产品。可以在 AI融资的融资课堂中学习相关知识。', '分享文案已复制,可自行分享') copyText('分享一个融资准备方法:先梳理信用、收入与负债,再比较适合自己的产品。可以在 AI融资的融资课堂中学习相关知识。', '分享文案已复制,可自行分享')
} }
@@ -4,7 +4,7 @@
<scroll-view scroll-y :style="contentStyle(226, false)"> <scroll-view scroll-y :style="contentStyle(226, false)">
<view class="profile-content"> <view class="profile-content">
<view class="profile-card" @click="openAccount"><view class="avatar"><AppIcon name="user" :size="80" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text class="account-state">{{ loggedIn ? '演示账户' : '未登录' }}</text></view> <view class="profile-card" @click="openAccount"><view class="avatar"><AppIcon name="user" :size="80" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text class="account-state">{{ loggedIn ? '演示账户' : '未登录' }}</text></view>
<view class="stat-row"><view class="stat-item" @click="go('/pages/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('/pages/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">{{ 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="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> <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> <button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
<text class="version-note">AI融资 · 让融资更简单</text> <text class="version-note">AI融资 · 让融资更简单</text>
@@ -42,18 +42,18 @@
hasReport.value = latest != null && typeof latest == 'object' hasReport.value = latest != null && typeof latest == 'object'
}) })
const go = (url: string) => { uni.navigateTo({ url }) } const go = (url: string) => { uni.navigateTo({ url }) }
const openReport = () => { if (hasReport.value) go('/pages/report/report'); else uni.showModal({ title: '暂无评估报告', content: '完善融资信息后,即可体验融资评估与方案匹配。', confirmText: '去完善', success: (result) => { if (result.confirm) go('/pages/archive/archive') } }) } 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 open = (key: string) => { const open = (key: string) => {
if (key == 'archive') go('/pages/archive/archive') if (key == 'archive') go('/packages/finance/archive/archive')
else if (key == 'help') go('/pages/assistant/assistant') else if (key == 'help') go('/packages/finance/assistant/assistant')
else if (key == 'security') uni.showModal({ title: '账户安全', content: loggedIn.value ? '当前账户:' + maskedPhone.value + '\n当前为本地演示登录,尚未进行真实身份认证。请勿使用真实密码体验。' : '当前尚未登录,可使用手机号进入演示账户。', showCancel: false, confirmText: '我知道了' }) else if (key == 'security') uni.showModal({ title: '账户安全', content: loggedIn.value ? '当前账户:' + maskedPhone.value + '\n当前为本地演示登录,尚未进行真实身份认证。请勿使用真实密码体验。' : '当前尚未登录,可使用手机号进入演示账户。', showCancel: false, confirmText: '我知道了' })
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { uni.setStorageSync('assessmentEditStep', 3); go('/pages/assessment/assessment?type=' + activeAssessmentType()) } } }) 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 == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识并体验智能方案匹配。当前为功能演示,评估结果不构成授信承诺。', showCancel: false, confirmText: '我知道了' }) else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识并体验智能方案匹配。当前为功能演示,评估结果不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
} }
const openAccount = () => { if (!loggedIn.value) go('/pages/login/login'); else open('security') } const openAccount = () => { if (!loggedIn.value) go('/packages/account/login/login'); else open('security') }
const logout = () => { const logout = () => {
if (!loggedIn.value) { go('/pages/login/login'); return } if (!loggedIn.value) { go('/packages/account/login/login'); return }
uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) { clearSession(); uni.reLaunch({ url: '/pages/login/login' }) } } }) uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) { clearSession(); uni.reLaunch({ url: '/packages/account/login/login' }) } } })
} }
</script> </script>
+35
View File
@@ -0,0 +1,35 @@
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
export { API_BASE_URL, saveSession } from '@/services/api.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
}
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
}
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
}
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#0868F7">
<circle cx="16" cy="10" r="6"/>
<path d="M16 18C8.8 18 3 22.1 3 27.2 3 28.2 3.8 29 4.8 29h22.4c1 0 1.8-.8 1.8-1.8C29 22.1 23.2 18 16 18Z"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path fill="#07C160" d="M13 4C6.9 4 2 8 2 13c0 2.8 1.6 5.3 4.1 7l-1 3.3 3.8-1.9c1.3.4 2.7.6 4.1.6 6.1 0 11-4 11-9S19.1 4 13 4Z"/>
<circle cx="9" cy="11" r="1.35" fill="#fff"/>
<circle cx="17" cy="11" r="1.35" fill="#fff"/>
<path fill="#07C160" stroke="#fff" stroke-width="1.2" stroke-linejoin="round" d="M21.5 12C16.3 12 12 15.4 12 19.6s4.3 7.6 9.5 7.6c1.2 0 2.4-.2 3.5-.5l3.2 1.6-.8-2.9c2.2-1.4 3.6-3.5 3.6-5.8 0-4.2-4.3-7.6-9.5-7.6Z"/>
<circle cx="18" cy="18" r="1.1" fill="#fff"/>
<circle cx="25" cy="18" r="1.1" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -17,7 +17,7 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { newsArticles, findNewsArticle, NewsArticle } from '@/services/content.uts' import { newsArticles, findNewsArticle, NewsArticle } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
const { contentStyle, pageStyle } = usePageLayout() const { contentStyle, pageStyle } = usePageLayout()
const article = ref<NewsArticle>(newsArticles[0]) const article = ref<NewsArticle>(newsArticles[0])
@@ -31,7 +31,7 @@
available.value = selected != null available.value = selected != null
if (selected != null) article.value = selected if (selected != null) article.value = selected
}) })
const goNews = (): void => { uni.redirectTo({ url: '/pages/news/news' }) } const goNews = (): void => { uni.redirectTo({ url: '/packages/content/news/news' }) }
</script> </script>
<style> <style>
@@ -3,7 +3,7 @@
<AppHeader title="融资课堂" /> <AppHeader title="融资课堂" />
<scroll-view scroll-y :style="contentStyle(96, false)"> <scroll-view scroll-y :style="contentStyle(96, false)">
<view class="classroom-content"> <view class="classroom-content">
<view class="classroom-hero"><view class="hero-copy"><text class="hero-eyebrow">实用融资知识</text><text class="hero-title">准备更充分</text><text class="hero-title hero-title-next">申请更从容</text><text class="hero-desc">每次几分钟,掌握一个知识点</text></view><image src="/static/mobile/images/news-policy.png" mode="aspectFill" class="hero-image"></image></view> <view class="classroom-hero"><view class="hero-copy"><text class="hero-eyebrow">实用融资知识</text><text class="hero-title">准备更充分</text><text class="hero-title hero-title-next">申请更从容</text><text class="hero-desc">每次几分钟,掌握一个知识点</text></view><image src="/packages/content/static/news-policy.png" mode="aspectFill" class="hero-image"></image></view>
<view class="topic-section"> <view class="topic-section">
<text class="section-title">按场景学习</text> <text class="section-title">按场景学习</text>
<view class="topic-list"> <view class="topic-list">
@@ -28,7 +28,7 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { financeLessons, FinanceLesson } from '@/services/content.uts' import { financeLessons, FinanceLesson } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout() const { contentStyle, pageStyle } = usePageLayout()
@@ -45,7 +45,7 @@
return financeLessons.filter((item: FinanceLesson): boolean => item.topics.includes(activeTopic.value)) return financeLessons.filter((item: FinanceLesson): boolean => item.topics.includes(activeTopic.value))
}) })
const selectTopic = (id: string): void => { activeTopic.value = activeTopic.value == id ? '' : id } const selectTopic = (id: string): void => { activeTopic.value = activeTopic.value == id ? '' : id }
const openLesson = (id: string): void => { uni.navigateTo({ url: '/pages/lesson/lesson?id=' + id }) } const openLesson = (id: string): void => { uni.navigateTo({ url: '/packages/content/lesson/lesson?id=' + id }) }
</script> </script>
<style> <style>
@@ -30,7 +30,7 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { financeLessons, findFinanceLesson, FinanceLesson } from '@/services/content.uts' import { financeLessons, findFinanceLesson, FinanceLesson } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout() const { contentStyle, pageStyle } = usePageLayout()
@@ -71,7 +71,7 @@
scrollTarget.value = '' scrollTarget.value = ''
nextTick(() => { scrollTarget.value = 'chapter-' + nextIndex }) nextTick(() => { scrollTarget.value = 'chapter-' + nextIndex })
} }
const goClassroom = (): void => { uni.redirectTo({ url: '/pages/classroom/classroom' }) } const goClassroom = (): void => { uni.redirectTo({ url: '/packages/content/classroom/classroom' }) }
</script> </script>
<style> <style>
@@ -25,7 +25,7 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { newsArticles, NewsArticle } from '@/services/content.uts' import { newsArticles, NewsArticle } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue' import BottomNav from '@/components/BottomNav.uvue'
const { contentStyle, pageStyle } = usePageLayout() const { contentStyle, pageStyle } = usePageLayout()
@@ -36,7 +36,7 @@
if (tab.value == '推荐') return newsArticles.filter((item: NewsArticle): boolean => item.id != headline.id) if (tab.value == '推荐') return newsArticles.filter((item: NewsArticle): boolean => item.id != headline.id)
return newsArticles.filter((item: NewsArticle): boolean => item.category == tab.value) return newsArticles.filter((item: NewsArticle): boolean => item.category == tab.value)
}) })
const openArticle = (id: string): void => { uni.navigateTo({ url: '/pages/article/article?id=' + id }) } const openArticle = (id: string): void => { uni.navigateTo({ url: '/packages/content/article/article?id=' + id }) }
</script> </script>
<style> <style>
@@ -13,7 +13,7 @@ export const newsArticles: NewsArticle[] = [
{ {
id: 'inclusive-finance', title: '普惠金融政策,企业申请前应关注什么?', id: 'inclusive-finance', title: '普惠金融政策,企业申请前应关注什么?',
summary: '看清适用主体、申请条件与材料要求,找到适合企业的支持方式。', category: '政策解读', summary: '看清适用主体、申请条件与材料要求,找到适合企业的支持方式。', category: '政策解读',
date: '2026-09-08', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/static/mobile/images/news-city.png', date: '2026-09-08', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/packages/content/static/news-city.png',
sections: [ sections: [
{ title: '先核对政策适用范围', body: '普惠金融支持措施通常对经营主体、企业规模和经营所在地设有适用条件。阅读政策时,先核对发文机构、生效时间与适用范围,再确认企业是否满足基本要求。' }, { title: '先核对政策适用范围', body: '普惠金融支持措施通常对经营主体、企业规模和经营所在地设有适用条件。阅读政策时,先核对发文机构、生效时间与适用范围,再确认企业是否满足基本要求。' },
{ title: '区分贷款、担保与贴息', body: '信贷产品、融资担保和财政贴息的申请渠道与审核流程可能不同。获得政策支持不等于贷款必然获批,额度、利率和期限仍需以金融机构的实际审核结果为准。' }, { title: '区分贷款、担保与贴息', body: '信贷产品、融资担保和财政贴息的申请渠道与审核流程可能不同。获得政策支持不等于贷款必然获批,额度、利率和期限仍需以金融机构的实际审核结果为准。' },
@@ -33,7 +33,7 @@ export const newsArticles: NewsArticle[] = [
{ {
id: 'loan-comparison', title: '比较融资产品时,别只看宣传利率', id: 'loan-comparison', title: '比较融资产品时,别只看宣传利率',
summary: '年化利率、附加费用与还款方式,需要放在一起比较。', category: '个人融资', summary: '年化利率、附加费用与还款方式,需要放在一起比较。', category: '个人融资',
date: '2026-09-03', source: 'AI融资编辑整理', readingTime: '3 分钟', image: '/static/mobile/images/news-city.png', date: '2026-09-03', source: 'AI融资编辑整理', readingTime: '3 分钟', image: '/packages/content/static/news-city.png',
sections: [ sections: [
{ title: '先统一利率口径', body: '日利率、月利率与年化利率并不是同一个口径。比较时应查看机构披露的年化利率及计算方式,尤其注意“低至”利率所附的适用条件。' }, { title: '先统一利率口径', body: '日利率、月利率与年化利率并不是同一个口径。比较时应查看机构披露的年化利率及计算方式,尤其注意“低至”利率所附的适用条件。' },
{ title: '将费用计入总成本', body: '核对合同是否包含担保费、服务费等支出,以及提前还款是否有额外费用。应以正式合同、还款计划和实际审核结果为准。' }, { title: '将费用计入总成本', body: '核对合同是否包含担保费、服务费等支出,以及提前还款是否有额外费用。应以正式合同、还款计划和实际审核结果为准。' },
@@ -43,7 +43,7 @@ export const newsArticles: NewsArticle[] = [
{ {
id: 'business-documents', title: '小微企业融资:常见材料如何提前整理', id: 'business-documents', title: '小微企业融资:常见材料如何提前整理',
summary: '按主体、经营和用途三类整理资料,减少补充沟通。', category: '企业融资', summary: '按主体、经营和用途三类整理资料,减少补充沟通。', category: '企业融资',
date: '2026-08-30', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/static/mobile/images/news-business.png', date: '2026-08-30', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/packages/content/static/news-business.png',
sections: [ sections: [
{ title: '主体资料保持一致', body: '核对营业执照、实际经营地址、法定代表人及联系人信息。涉及经营主体或股权变更时,准备相应说明,避免不同材料之间的信息不一致。' }, { title: '主体资料保持一致', body: '核对营业执照、实际经营地址、法定代表人及联系人信息。涉及经营主体或股权变更时,准备相应说明,避免不同材料之间的信息不一致。' },
{ title: '整理持续经营的证明', body: '经营流水、纳税记录、采购或销售合同可以帮助机构了解业务情况。按照时间顺序整理,并区分经营性收入、借款与其他往来款。' }, { title: '整理持续经营的证明', body: '经营流水、纳税记录、采购或销售合同可以帮助机构了解业务情况。按照时间顺序整理,并区分经营性收入、借款与其他往来款。' },
@@ -65,7 +65,7 @@ export const financeLessons: FinanceLesson[] = [
}, },
{ {
id: 'business-checklist', title: '小微经营融资准备课', desc: '把经营情况整理成完整材料清单', duration: '8 分钟', id: 'business-checklist', title: '小微经营融资准备课', desc: '把经营情况整理成完整材料清单', duration: '8 分钟',
tag: '企业融资', image: '/static/mobile/images/news-business.png', topics: ['business', 'documents'], tag: '企业融资', image: '/packages/content/static/news-business.png', topics: ['business', 'documents'],
intro: '围绕真实经营、资金用途和还款来源,分步骤整理企业融资所需的信息。具体材料要求以申请机构为准。', intro: '围绕真实经营、资金用途和还款来源,分步骤整理企业融资所需的信息。具体材料要求以申请机构为准。',
chapters: [ chapters: [
{ title: '核对企业主体与经营信息', body: '核对营业执照、经营地址、法定代表人与实际控制人信息,确保材料在有效期内。\n\n介绍企业业务时,可以说明主要产品、客户类型和经营年限。信息以真实经营情况为准,无需为了申请夸大规模。\n\n本节练习:核对主体证件与申请表中各项信息是否一致。' }, { title: '核对企业主体与经营信息', body: '核对营业执照、经营地址、法定代表人与实际控制人信息,确保材料在有效期内。\n\n介绍企业业务时,可以说明主要产品、客户类型和经营年限。信息以真实经营情况为准,无需为了申请夸大规模。\n\n本节练习:核对主体证件与申请表中各项信息是否一致。' },
@@ -76,7 +76,7 @@ export const financeLessons: FinanceLesson[] = [
}, },
{ {
id: 'loan-cost', title: '利率与还款方式入门', desc: '看懂总成本,选好还款节奏', duration: '5 分钟', id: 'loan-cost', title: '利率与还款方式入门', desc: '看懂总成本,选好还款节奏', duration: '5 分钟',
tag: '基础知识', image: '/static/mobile/images/news-policy.png', topics: ['credit', 'business'], tag: '基础知识', image: '/packages/content/static/news-policy.png', topics: ['credit', 'business'],
intro: '把利率、期限和还款方式放在一起理解,学会用统一口径比较融资方案。', intro: '把利率、期限和还款方式放在一起理解,学会用统一口径比较融资方案。',
chapters: [ chapters: [
{ title: '比较利率先看口径', body: '比较产品时,优先查看正式披露的年化利率及计算方式。日息、月息或“每万元每天几元”的宣传表达不能直接互相比高低。\n\n实际适用利率通常与审核结果相关,页面显示的最低利率不代表每个人都能获得。\n\n本节练习:找到一份产品说明中的年化利率、适用条件与费用说明。' }, { title: '比较利率先看口径', body: '比较产品时,优先查看正式披露的年化利率及计算方式。日息、月息或“每万元每天几元”的宣传表达不能直接互相比高低。\n\n实际适用利率通常与审核结果相关,页面显示的最低利率不代表每个人都能获得。\n\n本节练习:找到一份产品说明中的年化利率、适用条件与费用说明。' },

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

@@ -23,7 +23,7 @@
finished = true finished = true
clearInterval(timer) clearInterval(timer)
clearTimeout(finishTimer) clearTimeout(finishTimer)
uni.redirectTo({ url: '/pages/report/report' }) uni.redirectTo({ url: '/packages/finance/report/report' })
} }
onShow(() => { onShow(() => {
if (finished) return if (finished) return
@@ -29,9 +29,9 @@
const refresh = () => { progress.value = assessmentProgress(type.value) } const refresh = () => { progress.value = assessmentProgress(type.value) }
onShow(() => { type.value = activeAssessmentType(); refresh() }) onShow(() => { type.value = activeAssessmentType(); refresh() })
const switchType = (next: string) => { type.value = next; uni.setStorageSync('assessmentActiveType', next); refresh() } const switchType = (next: string) => { type.value = next; uni.setStorageSync('assessmentActiveType', next); refresh() }
const edit = (step: number) => { uni.setStorageSync('assessmentEditStep', step); uni.navigateTo({ url: '/pages/assessment/assessment?type=' + type.value }) } const edit = (step: number) => { uni.setStorageSync('assessmentEditStep', step); uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
const complete = () => { const complete = () => {
if (progress.value.percent == 100) uni.navigateTo({ url: '/pages/confirm/confirm' }) if (progress.value.percent == 100) uni.navigateTo({ url: '/packages/finance/confirm/confirm' })
else if (!progress.value.basicDone) edit(0) else if (!progress.value.basicDone) edit(0)
else if (!progress.value.incomeDone) edit(1) else if (!progress.value.incomeDone) edit(1)
else edit(2) else edit(2)
@@ -55,7 +55,7 @@
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
import AssessmentSelect from '@/components/AssessmentSelect.uvue' import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue'
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice } from '@/services/assessment.uts' import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice } from '@/services/assessment.uts'
const { bottomStyle, contentStyle, pageStyle } = usePageLayout() const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
const showAgreement = (title: string) => { showAgreementNotice(title) } const showAgreement = (title: string) => { showAgreementNotice(title) }
@@ -108,7 +108,7 @@
// 系统返回与顶部返回均先保存;页内返回优先回到上一步。 // 系统返回与顶部返回均先保存;页内返回优先回到上一步。
onBackPress((): boolean => { saveDraft(); if (step.value > 0) { step.value--; saveDraft(); return true }; return false }) onBackPress((): boolean => { saveDraft(); if (step.value > 0) { step.value--; saveDraft(); return true }; return false })
const previous = () => { saveDraft(); if (step.value > 0) step.value--; saveDraft() } const previous = () => { saveDraft(); if (step.value > 0) step.value--; saveDraft() }
const back = () => { if (step.value > 0) { previous(); return }; saveDraft(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/pages/entry/entry?type=' + assessmentType.value }) } const back = () => { if (step.value > 0) { previous(); return }; saveDraft(); if (getCurrentPages().length > 1) uni.navigateBack(); else uni.redirectTo({ url: '/packages/finance/entry/entry?type=' + assessmentType.value }) }
const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path } const documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
const documentExpired = (name: string) => { const documentExpired = (name: string) => {
documents.value = documents.value.filter((file) => file.name != name); saveDraft() documents.value = documents.value.filter((file) => file.name != name); saveDraft()
@@ -150,7 +150,7 @@
if (message.length == 0) return true if (message.length == 0) return true
uni.showToast({ title: message, icon: 'none' }); return false uni.showToast({ title: message, icon: 'none' }); return false
} }
const next = () => { if (!validateStep()) return; if (step.value < 3) { step.value++; saveDraft() } else { saveDraft(); uni.navigateTo({ url: '/pages/confirm/confirm' }) } } const next = () => { if (!validateStep()) return; if (step.value < 3) { step.value++; saveDraft() } else { saveDraft(); uni.navigateTo({ url: '/packages/finance/confirm/confirm' }) } }
</script> </script>
<style> <style>
@@ -3,15 +3,15 @@
<AppHeader title="AI融资助手" /> <AppHeader title="AI融资助手" />
<scroll-view scroll-y class="chat-scroll" :scroll-into-view="lastMessageId" :style="contentStyle(210, false)"> <scroll-view scroll-y class="chat-scroll" :scroll-into-view="lastMessageId" :style="contentStyle(210, false)">
<view class="chat-content"> <view class="chat-content">
<view class="assistant-intro"><image src="/static/mobile/images/robot.png" mode="aspectFit" class="assistant-intro-image"></image><view class="assistant-bubble"><text class="bubble-text">您好!我是AI融资助手,可以帮您了解融资条件、准备材料和比较方案。</text></view></view> <view class="assistant-intro"><image src="/packages/finance/static/robot.png" mode="aspectFit" class="assistant-intro-image"></image><view class="assistant-bubble"><text class="bubble-text">您好!我是AI融资助手,可以帮您了解融资条件、准备材料和比较方案。</text></view></view>
<view class="quick-questions"> <view class="quick-questions">
<view class="question" v-for="item in questions" :key="item" @click="ask(item)"><text class="question-text">{{ item }}</text><AppIcon name="chevron-right" :size="28" muted /></view> <view class="question" v-for="item in questions" :key="item" @click="ask(item)"><text class="question-text">{{ item }}</text><AppIcon name="chevron-right" :size="28" muted /></view>
</view> </view>
<view v-for="(item,index) in messages" :key="index" :id="'message-' + index" :class="item.role == 'user' ? 'message-row user-row' : 'message-row'"> <view v-for="(item,index) in messages" :key="index" :id="'message-' + index" :class="item.role == 'user' ? 'message-row user-row' : 'message-row'">
<image v-if="item.role == 'assistant'" class="message-avatar" src="/static/mobile/images/robot.png" mode="aspectFit"></image> <image v-if="item.role == 'assistant'" class="message-avatar" src="/packages/finance/static/robot.png" mode="aspectFit"></image>
<view :class="item.role == 'user' ? 'message-bubble user-bubble' : 'message-bubble'"><text :class="item.role == 'user' ? 'bubble-text user-text' : 'bubble-text'">{{ item.text }}</text></view> <view :class="item.role == 'user' ? 'message-bubble user-bubble' : 'message-bubble'"><text :class="item.role == 'user' ? 'bubble-text user-text' : 'bubble-text'">{{ item.text }}</text></view>
</view> </view>
<view v-if="sending" class="message-row"><image class="message-avatar" src="/static/mobile/images/robot.png" mode="aspectFit"></image><view class="message-bubble"><text class="sending-text">正在整理建议…</text></view></view> <view v-if="sending" class="message-row"><image class="message-avatar" src="/packages/finance/static/robot.png" mode="aspectFit"></image><view class="message-bubble"><text class="sending-text">正在整理建议…</text></view></view>
<view v-if="errorMessage.length > 0" class="assistant-error"><text class="error-text">{{ errorMessage }}</text><text @click="retry" class="retry-text">重试</text></view> <view v-if="errorMessage.length > 0" class="assistant-error"><text class="error-text">{{ errorMessage }}</text><text @click="retry" class="retry-text">重试</text></view>
<view id="chat-end" class="chat-end"></view> <view id="chat-end" class="chat-end"></view>
</view> </view>
@@ -25,7 +25,7 @@
const { bottomStyle, contentStyle, pageStyle } = usePageLayout() const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
import { sendAssistantMessage } from '@/services/api.uts' import { sendAssistantMessage } from '@/packages/finance/services/api.uts'
const questions = ['我的融资额度受哪些因素影响?', '如何提高贷款通过率?', '企业融资需要哪些材料?', '有哪些适合我的方案?'] const questions = ['我的融资额度受哪些因素影响?', '如何提高贷款通过率?', '企业融资需要哪些材料?', '有哪些适合我的方案?']
type ChatMessage = { role: string, text: string } type ChatMessage = { role: string, text: string }
const messages = ref<ChatMessage[]>([]) const messages = ref<ChatMessage[]>([])
@@ -18,7 +18,7 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import { API_BASE_URL, submitAssessment } from '@/services/api.uts' import { API_BASE_URL, submitAssessment } from '@/packages/finance/services/api.uts'
import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft } from '@/services/assessment.uts' import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft } from '@/services/assessment.uts'
const { pageStyle, contentStyle, bottomStyle } = usePageLayout() const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
type InfoRow = { label: string, value: string } type InfoRow = { label: string, value: string }
@@ -72,8 +72,8 @@
const edit = (step: number) => { const edit = (step: number) => {
uni.setStorageSync('assessmentEditStep', step) uni.setStorageSync('assessmentEditStep', step)
const pages = getCurrentPages() const pages = getCurrentPages()
if (pages.length > 1 && pages[pages.length - 2].route == 'pages/assessment/assessment') uni.navigateBack({ delta: 1 }) if (pages.length > 1 && pages[pages.length - 2].route == 'packages/finance/assessment/assessment') uni.navigateBack({ delta: 1 })
else uni.navigateTo({ url: '/pages/assessment/assessment?type=' + assessmentType.value }) else uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + assessmentType.value })
} }
const submit = async () => { const submit = async () => {
if (!hasDraft.value) { edit(0); return } if (!hasDraft.value) { edit(0); return }
@@ -87,7 +87,7 @@
const grants = saved['authorizations'] as string[] | null 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 } if ((documentsFromDraft(saved).length > 0 && saved['documentConsent'] != true) || (grants != null && grants.length > 0 && saved['dataConsent'] != true)) { uni.showToast({ title: '请确认资料与授权选择', icon: 'none' }); edit(3); return }
submitting.value = true submitting.value = true
try { await submitAssessment(payload.value); uni.redirectTo({ url: '/pages/analyzing/analyzing' }) } try { await submitAssessment(payload.value); uni.redirectTo({ url: '/packages/finance/analyzing/analyzing' }) }
catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) }
finally { submitting.value = false } finally { submitting.value = false }
} }
@@ -37,7 +37,7 @@
benefits.value = ['匹配企业资金需求', '多维度经营资质评估', '多种融资方案对比'] benefits.value = ['匹配企业资金需求', '多维度经营资质评估', '多种融资方案对比']
} }
}) })
const start = () => { uni.navigateTo({ url: '/pages/assessment/assessment?type=' + type.value }) } const start = () => { uni.navigateTo({ url: '/packages/finance/assessment/assessment?type=' + type.value }) }
</script> </script>
<style> <style>
@@ -54,7 +54,7 @@
.benefit-dot { width: 10rpx; height: 10rpx; border-radius: 5rpx; background-color: #0868f7; margin-right: 14rpx; flex-shrink: 0; } .benefit-dot { width: 10rpx; height: 10rpx; border-radius: 5rpx; background-color: #0868f7; margin-right: 14rpx; flex-shrink: 0; }
.enterprise-page .benefit-dot { background-color: #f47a1a; } .enterprise-page .benefit-dot { background-color: #f47a1a; }
.entry-image { position: absolute; width: 285rpx; height: 300rpx; right: -35rpx; bottom: -22rpx; } .entry-image { position: absolute; width: 285rpx; height: 300rpx; right: 16rpx; bottom: -22rpx; }
.start-button { margin-top: 30rpx; } .start-button { margin-top: 30rpx; }
.process-block { margin-top: 48rpx; } .process-block { margin-top: 48rpx; }
.process-row { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 34rpx; padding: 0 20rpx; } .process-row { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 34rpx; padding: 0 20rpx; }
@@ -24,8 +24,8 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { FinanceProduct } from '@/services/finance.uts' import { FinanceProduct } from '@/packages/finance/services/finance.uts'
import { fetchMatches, API_BASE_URL } from '@/services/api.uts' import { fetchMatches, API_BASE_URL } from '@/packages/finance/services/api.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue' import BottomNav from '@/components/BottomNav.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
@@ -53,7 +53,7 @@
loading.value = false loading.value = false
} }
onMounted(() => { loadMatches() }) onMounted(() => { loadMatches() })
const openProduct = (id: string) => { uni.navigateTo({ url: '/pages/product/product?id=' + id }) } const openProduct = (id: string) => { uni.navigateTo({ url: '/packages/finance/product/product?id=' + id }) }
</script> </script>
<style> <style>
@@ -21,8 +21,8 @@
<script setup lang="uts"> <script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
import { FinanceProduct, demoProducts } from '@/services/finance.uts' import { FinanceProduct, demoProducts } from '@/packages/finance/services/finance.uts'
import { fetchProduct, API_BASE_URL } from '@/services/api.uts' import { fetchProduct, API_BASE_URL } from '@/packages/finance/services/api.uts'
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
const { bottomStyle, contentStyle, pageStyle } = usePageLayout() const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
@@ -56,7 +56,7 @@
title: '准备申请资料', title: '准备申请资料',
content: '先完善融资档案,核对申请所需材料。当前尚未开通机构申请,资料不会提交至银行。', content: '先完善融资档案,核对申请所需材料。当前尚未开通机构申请,资料不会提交至银行。',
confirmText: '完善档案', confirmText: '完善档案',
success: (result) => { if (result.confirm) uni.navigateTo({ url: '/pages/archive/archive' }) } success: (result) => { if (result.confirm) uni.navigateTo({ url: '/packages/finance/archive/archive' }) }
}) })
} }
</script> </script>
@@ -4,7 +4,7 @@
<scroll-view scroll-y class="report-scroll" :style="contentStyle(96, false)"> <scroll-view scroll-y class="report-scroll" :style="contentStyle(96, false)">
<view class="report-content"> <view class="report-content">
<view class="score-panel"><text class="score-label">综合评分 · 示例报告</text><view class="score-row"><text class="big-score">85</text><text class="score-unit">分</text><text class="score-badge">良好</text></view><text class="score-caption">从五个维度了解融资资质</text></view> <view class="score-panel"><text class="score-label">综合评分 · 示例报告</text><view class="score-row"><text class="big-score">85</text><text class="score-unit">分</text><text class="score-badge">良好</text></view><text class="score-caption">从五个维度了解融资资质</text></view>
<view class="radar-panel"><image class="radar-image" src="/static/mobile/images/finance-radar.png" mode="aspectFit"></image><text class="radar-label top">还款能力</text><text class="radar-label left">收入稳定</text><text class="radar-label right">信用状况</text><text class="radar-label bottom-left">资产条件</text><text class="radar-label bottom-right">负债情况</text></view> <view class="radar-panel"><image class="radar-image" src="/packages/finance/static/finance-radar.png" mode="aspectFit"></image><text class="radar-label top">还款能力</text><text class="radar-label left">收入稳定</text><text class="radar-label right">信用状况</text><text class="radar-label bottom-left">资产条件</text><text class="radar-label bottom-right">负债情况</text></view>
<view class="conclusion"><text class="section-title">评估结论</text><text class="conclusion-text">示例资质整体良好,可进一步比较信用融资产品的额度、期限与还款安排。</text><view class="suggest-row"><text class="suggest-label">建议期限</text><text class="suggest-value">6–36个月</text></view><text class="report-note">本报告为体验示例,正式评估以授权资料和机构审核为准。</text></view> <view class="conclusion"><text class="section-title">评估结论</text><text class="conclusion-text">示例资质整体良好,可进一步比较信用融资产品的额度、期限与还款安排。</text><view class="suggest-row"><text class="suggest-label">建议期限</text><text class="suggest-value">6–36个月</text></view><text class="report-note">本报告为体验示例,正式评估以授权资料和机构审核为准。</text></view>
<button class="primary-button" @click="viewMatches">查看匹配方案</button> <button class="primary-button" @click="viewMatches">查看匹配方案</button>
</view> </view>
@@ -16,7 +16,7 @@
import { usePageLayout } from '@/services/layout.uts' import { usePageLayout } from '@/services/layout.uts'
const { contentStyle, pageStyle } = usePageLayout() const { contentStyle, pageStyle } = usePageLayout()
import AppHeader from '@/components/AppHeader.uvue' import AppHeader from '@/components/AppHeader.uvue'
const viewMatches = () => { uni.navigateTo({ url: '/pages/matches/matches' }) } const viewMatches = () => { uni.navigateTo({ url: '/packages/finance/matches/matches' }) }
</script> </script>
<style> <style>
+47
View File
@@ -0,0 +1,47 @@
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
import { demoProducts, parseProduct, FinanceProduct } from './finance.uts'
export { API_BASE_URL } from '@/services/api.uts'
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(650)
uni.setStorageSync('latestAssessment', payload)
return 'ASSESSMENT-DEMO-001'
}
const result = await requestJson('/assessments', 'POST', payload)
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) {
await wait(320)
return demoProducts.slice()
}
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
const items = result['items'] as UTSJSONObject[] | null
return items == null ? [] : items.map((item: UTSJSONObject): FinanceProduct => parseProduct(item))
}
export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
if (API_BASE_URL.length == 0) {
await wait(180)
const products = demoProducts.filter((item: FinanceProduct): boolean => item.id == id)
return products.length > 0 ? products[0] : null
}
const result = await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject())
return parseProduct(result)
}
export async function sendAssistantMessage(message: string): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(600)
return '结合您的评估结果,建议优先选择年化利率较低、可随借随还的产品,并重点确认提前还款规则。'
}
const result = await requestJson('/assistant/messages', 'POST', { message })
const reply = result['reply'] as string | null
if (reply == null || reply.length == 0) throw new Error('助手暂时没有返回内容')
return reply
}

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

+36 -19
View File
@@ -1,24 +1,41 @@
{ {
"pages": [ "pages": [
{ "path": "pages/index/index", "style": { "navigationStyle": "custom" } }, { "path": "pages/home/home", "style": { "navigationStyle": "custom" } }
{ "path": "pages/login/login", "style": { "navigationStyle": "custom" } }, ],
{ "path": "pages/home/home", "style": { "navigationStyle": "custom" } }, "subPackages": [
{ "path": "pages/entry/entry", "style": { "navigationStyle": "custom" } }, {
{ "path": "pages/assessment/assessment", "style": { "navigationStyle": "custom" } }, "root": "packages/finance",
{ "path": "pages/confirm/confirm", "style": { "navigationStyle": "custom" } }, "pages": [
{ "path": "pages/analyzing/analyzing", "style": { "navigationStyle": "custom" } }, { "path": "entry/entry", "style": { "navigationStyle": "custom" } },
{ "path": "pages/report/report", "style": { "navigationStyle": "custom" } }, { "path": "assessment/assessment", "style": { "navigationStyle": "custom" } },
{ "path": "pages/matches/matches", "style": { "navigationStyle": "custom" } }, { "path": "confirm/confirm", "style": { "navigationStyle": "custom" } },
{ "path": "pages/product/product", "style": { "navigationStyle": "custom" } }, { "path": "analyzing/analyzing", "style": { "navigationStyle": "custom" } },
{ "path": "pages/assistant/assistant", "style": { "navigationStyle": "custom" } }, { "path": "report/report", "style": { "navigationStyle": "custom" } },
{ "path": "pages/news/news", "style": { "navigationStyle": "custom" } }, { "path": "matches/matches", "style": { "navigationStyle": "custom" } },
{ "path": "pages/classroom/classroom", "style": { "navigationStyle": "custom" } }, { "path": "product/product", "style": { "navigationStyle": "custom" } },
{ "path": "pages/article/article", "style": { "navigationStyle": "custom" } }, { "path": "assistant/assistant", "style": { "navigationStyle": "custom" } },
{ "path": "pages/lesson/lesson", "style": { "navigationStyle": "custom" } }, { "path": "archive/archive", "style": { "navigationStyle": "custom" } }
{ "path": "pages/points/points", "style": { "navigationStyle": "custom" } }, ]
{ "path": "pages/messages/messages", "style": { "navigationStyle": "custom" } }, },
{ "path": "pages/archive/archive", "style": { "navigationStyle": "custom" } }, {
{ "path": "pages/profile/profile", "style": { "navigationStyle": "custom" } } "root": "packages/account",
"pages": [
{ "path": "index/index", "style": { "navigationStyle": "custom" } },
{ "path": "login/login", "style": { "navigationStyle": "custom" } },
{ "path": "profile/profile", "style": { "navigationStyle": "custom" } },
{ "path": "points/points", "style": { "navigationStyle": "custom" } },
{ "path": "messages/messages", "style": { "navigationStyle": "custom" } }
]
},
{
"root": "packages/content",
"pages": [
{ "path": "news/news", "style": { "navigationStyle": "custom" } },
{ "path": "article/article", "style": { "navigationStyle": "custom" } },
{ "path": "classroom/classroom", "style": { "navigationStyle": "custom" } },
{ "path": "lesson/lesson", "style": { "navigationStyle": "custom" } }
]
}
], ],
"globalStyle": { "globalStyle": {
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",
+35 -14
View File
@@ -1,9 +1,23 @@
<template> <template>
<view class="page" :style="pageStyle()"> <view class="page" :style="pageStyle()">
<view class="home-header"><view class="safe" :style="statusStyle(0)"></view><view class="home-bar"><BrandLogo /><view class="header-actions"><view class="round-action" @click="go('/pages/assistant/assistant')"><AppIcon name="sparkles" :size="34" /></view><view class="round-action notify" @click="go('/pages/messages/messages')"><AppIcon name="bell" :size="34" /><view v-if="hasUnread" class="red-dot"></view></view></view></view></view> <view class="home-header">
<view class="safe" :style="statusStyle(0)"></view>
<view class="home-bar">
<BrandLogo />
<view class="header-actions">
<view class="round-action" @click="go('/packages/finance/assistant/assistant')">
<view class="action-surface"><image class="header-icon" src="/static/icons/sparkles.svg" mode="aspectFit" /></view>
</view>
<view class="round-action notify" @click="go('/packages/account/messages/messages')">
<view class="action-surface"><image class="header-icon" src="/static/icons/bell.svg" mode="aspectFit" /></view>
<view v-if="hasUnread" class="red-dot"></view>
</view>
</view>
</view>
</view>
<scroll-view scroll-y class="home-scroll" :style="contentStyle(104, false)"> <scroll-view scroll-y class="home-scroll" :style="contentStyle(104, false)">
<view class="home-content"> <view class="home-content">
<view class="ai-banner" @click="go('/pages/assistant/assistant')"> <view class="ai-banner" @click="go('/packages/finance/assistant/assistant')">
<view class="banner-copy"><text class="banner-title">AI智能评估</text><text class="banner-subtitle">精准匹配融资方案</text><text class="banner-desc">让合适的资金主动找上您</text></view> <view class="banner-copy"><text class="banner-title">AI智能评估</text><text class="banner-subtitle">精准匹配融资方案</text><text class="banner-desc">让合适的资金主动找上您</text></view>
<image class="robot-img" src="/static/mobile/images/robot-cutout.png" mode="aspectFit"></image> <image class="robot-img" src="/static/mobile/images/robot-cutout.png" mode="aspectFit"></image>
</view> </view>
@@ -12,16 +26,16 @@
<view class="finance-entry enterprise entry-following" @click="openEntry('enterprise')" ><view><text class="entry-title">企业信用融资</text><text class="entry-subtitle">企业/个体工商户</text></view><image src="/static/mobile/images/enterprise-building-cutout.png" mode="aspectFit" class="finance-entry-image"></image></view> <view class="finance-entry enterprise entry-following" @click="openEntry('enterprise')" ><view><text class="entry-title">企业信用融资</text><text class="entry-subtitle">企业/个体工商户</text></view><image src="/static/mobile/images/enterprise-building-cutout.png" mode="aspectFit" class="finance-entry-image"></image></view>
</view> </view>
<view class="shortcut-row"> <view class="shortcut-row">
<view class="shortcut" @click="go('/pages/classroom/classroom')"><view class="shortcut-icon"><AppIcon name="file-text" :size="60" /></view><text class="shortcut-label">融资课堂</text></view> <view class="shortcut" @click="go('/packages/content/classroom/classroom')"><view class="shortcut-icon"><AppIcon name="file-text" :size="60" /></view><text class="shortcut-label">融资课堂</text></view>
<view class="shortcut" @click="go('/pages/assistant/assistant')"><view class="shortcut-icon"><AppIcon name="sparkles" :size="60" /></view><text class="shortcut-label">AI融资助手</text></view> <view class="shortcut" @click="go('/packages/finance/assistant/assistant')"><view class="shortcut-icon"><AppIcon name="sparkles" :size="60" /></view><text class="shortcut-label">AI融资助手</text></view>
<view class="shortcut" @click="go('/pages/entry/entry?type=personal')"><view class="shortcut-icon"><AppIcon name="calculator" :size="60" /></view><text class="shortcut-label">融资测算</text></view> <view class="shortcut" @click="go('/packages/finance/entry/entry?type=personal')"><view class="shortcut-icon"><AppIcon name="calculator" :size="60" /></view><text class="shortcut-label">融资测算</text></view>
<view class="shortcut" @click="go('/pages/points/points')"><view class="shortcut-icon"><AppIcon name="gift" :size="60" /></view><text class="shortcut-label">邀请有礼</text></view> <view class="shortcut" @click="go('/packages/account/points/points')"><view class="shortcut-icon"><AppIcon name="gift" :size="60" /></view><text class="shortcut-label">邀请有礼</text></view>
</view> </view>
<view class="archive-entry" @click="go('/pages/archive/archive')"><AppIcon name="archive" :size="42" /><text class="archive-label">我的融资档案</text><text class="archive-action">去完善</text><AppIcon name="chevron-right" :size="28" /></view> <view class="archive-entry" @click="go('/packages/finance/archive/archive')"><AppIcon name="archive" :size="42" /><text class="archive-label">我的融资档案</text><text class="archive-action">去完善</text><AppIcon name="chevron-right" :size="28" /></view>
<view class="section-head"><text class="section-title">我的融资方案</text><view class="more-link" @click="go('/pages/matches/matches')"><text class="more">查看全部</text><AppIcon name="chevron-right" :size="24" /></view></view> <view class="section-head"><text class="section-title">我的融资方案</text><view class="more-link" @click="go('/packages/finance/matches/matches')"><text class="more">查看全部</text><AppIcon name="chevron-right" :size="24" /></view></view>
<view class="plan-card" @click="go('/pages/matches/matches')"><view class="plan-main"><text class="plan-name">信用融资方案</text><text class="plan-meta">了解额度、利率与申请条件</text></view><view class="score"><text class="score-number">3</text><text class="score-label">示例方案</text></view></view> <view class="plan-card" @click="go('/packages/finance/matches/matches')"><view class="plan-main"><text class="plan-name">信用融资方案</text><text class="plan-meta">了解额度、利率与申请条件</text></view><view class="score"><text class="score-number">3</text><text class="score-label">示例方案</text></view></view>
<view class="section-head"><text class="section-title">融资课堂</text><view class="more-link" @click="go('/pages/classroom/classroom')"><text class="more">进入课堂</text><AppIcon name="chevron-right" :size="24" /></view></view> <view class="section-head"><text class="section-title">融资课堂</text><view class="more-link" @click="go('/packages/content/classroom/classroom')"><text class="more">进入课堂</text><AppIcon name="chevron-right" :size="24" /></view></view>
<view class="news-preview" @click="go('/pages/classroom/classroom')"><view class="news-copy"><text class="news-title">如何提高个人贷款通过率</text><text class="news-meta">信用管理 · 6分钟阅读</text></view><image class="news-art" src="/static/mobile/images/news-calculator.png" mode="aspectFill"></image></view> <view class="news-preview" @click="go('/packages/content/classroom/classroom')"><view class="news-copy"><text class="news-title">如何提高个人贷款通过率</text><text class="news-meta">信用管理 · 6分钟阅读</text></view><image class="news-art" src="/static/mobile/images/news-calculator.png" mode="aspectFill"></image></view>
</view> </view>
</scroll-view> </scroll-view>
<BottomNav active="home" /> <BottomNav active="home" />
@@ -34,10 +48,14 @@
import BrandLogo from '@/components/BrandLogo.uvue' import BrandLogo from '@/components/BrandLogo.uvue'
import BottomNav from '@/components/BottomNav.uvue' import BottomNav from '@/components/BottomNav.uvue'
import AppIcon from '@/components/AppIcon.uvue' import AppIcon from '@/components/AppIcon.uvue'
import { consumeInitialLaunch } from '@/services/launch.uts'
onLoad(() => {
if (consumeInitialLaunch()) uni.reLaunch({ url: '/packages/account/index/index' })
})
const hasUnread = ref(true) const hasUnread = ref(true)
onShow(() => { hasUnread.value = uni.getStorageSync('finance.notice.read.assessment-guide') != true }) onShow(() => { hasUnread.value = uni.getStorageSync('finance.notice.read.assessment-guide') != true })
const go = (url: string) => { uni.navigateTo({ url: url }) } const go = (url: string) => { uni.navigateTo({ url: url }) }
const openEntry = (type: string) => { go('/pages/entry/entry?type=' + type) } const openEntry = (type: string) => { go('/packages/finance/entry/entry?type=' + type) }
</script> </script>
<style> <style>
@@ -45,8 +63,11 @@
.safe { } .safe { }
.home-bar { height: 104rpx; padding: 0 28rpx; flex-direction: row; align-items: center; justify-content: space-between; } .home-bar { height: 104rpx; padding: 0 28rpx; flex-direction: row; align-items: center; justify-content: space-between; }
.header-actions { flex-direction: row; } .header-actions { flex-direction: row; }
.round-action { width: 62rpx; height: 62rpx; border-radius: 50%; background-color: #eef5ff; align-items: center; justify-content: center; margin-left: 16rpx; position: relative; } .round-action { width: 62rpx; height: 62rpx; margin-left: 16rpx; position: relative; flex-shrink: 0; }
.red-dot { position: absolute; width: 13rpx; height: 13rpx; right: 2rpx; top: 0; background-color: #ff4d4f; border-radius: 50%; border: 3rpx solid #fff; } .action-surface { width: 62rpx; height: 62rpx; border-radius: 50%; background-color: #eef5ff; align-items: center; justify-content: center; }
.header-icon { width: 36rpx; height: 36rpx; flex-shrink: 0; }
/* 红点与圆形背景同级,避免被跨端 view 的圆角裁切。 */
.red-dot { position: absolute; width: 16rpx; height: 16rpx; right: 2rpx; top: 2rpx; z-index: 1; background-color: #ff4d4f; border-radius: 50%; border: 2rpx solid #fff; }
.home-scroll { } .home-scroll { }
.home-content { padding: 18rpx 28rpx 180rpx; } .home-content { padding: 18rpx 28rpx 180rpx; }
.ai-banner { height: 246rpx; border-radius: 20rpx; padding: 30rpx; overflow: hidden; flex-direction: row; align-items: center; position: relative; background: linear-gradient(125deg, #1263f0 0%, #0788ff 100%); } .ai-banner { height: 246rpx; border-radius: 20rpx; padding: 30rpx; overflow: hidden; flex-direction: row; align-items: center; position: relative; background: linear-gradient(125deg, #1263f0 0%, #0788ff 100%); }
+2 -78
View File
@@ -1,5 +1,4 @@
/** Mobile API adapter with deterministic local mocks. */ /** Shared request, configuration and session infrastructure. */
import { demoProducts, parseProduct, FinanceProduct } from '@/services/finance.uts'
export const API_BASE_URL = '' export const API_BASE_URL = ''
const REQUEST_TIMEOUT = 10000 const REQUEST_TIMEOUT = 10000
const SESSION_KEY = 'aiFinanceSession' const SESSION_KEY = 'aiFinanceSession'
@@ -54,79 +53,4 @@ const requestJson = (path: string, method: RequestMethod, payload: UTSJSONObject
}) })
} }
export async function requestSmsCode(phone: string): Promise<boolean> { export { apiMessage, requestJson, wait }
if (API_BASE_URL.length == 0) {
await wait(350)
return phone.length == 11
}
await requestJson('/auth/sms-code', 'POST', { phone })
return true
}
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
}
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
}
export async function submitAssessment(payload: UTSJSONObject): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(650)
uni.setStorageSync('latestAssessment', payload)
return 'ASSESSMENT-DEMO-001'
}
const result = await requestJson('/assessments', 'POST', payload)
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) {
await wait(320)
return demoProducts.slice()
}
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
const items = result['items'] as UTSJSONObject[] | null
return items == null ? [] : items.map((item: UTSJSONObject): FinanceProduct => parseProduct(item))
}
export async function fetchProduct(id: string): Promise<FinanceProduct | null> {
if (API_BASE_URL.length == 0) {
await wait(180)
const products = demoProducts.filter((item: FinanceProduct): boolean => item.id == id)
return products.length > 0 ? products[0] : null
}
const result = await requestJson('/products/' + encodeURIComponent(id), 'GET', new UTSJSONObject())
return parseProduct(result)
}
export async function sendAssistantMessage(message: string): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(600)
return '结合您的评估结果,建议优先选择年化利率较低、可随借随还的产品,并重点确认提前还款规则。'
}
const result = await requestJson('/assistant/messages', 'POST', { message })
const reply = result['reply'] as string | null
if (reply == null || reply.length == 0) throw new Error('助手暂时没有返回内容')
return reply
}
export { apiMessage }
+8
View File
@@ -0,0 +1,8 @@
// 只在本次应用启动首次进入首页时显示引导,跨分包返回不重复触发。
let launchHandled = false
export function consumeInitialLaunch(): boolean {
if (launchHandled) return false
launchHandled = true
return true
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB