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
+53
View File
@@ -0,0 +1,53 @@
<template>
<view class="page page-white" :style="pageStyle()">
<AppHeader title="新闻详情" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<view v-if="available" class="detail-content">
<text class="detail-category">{{ article.category }}</text><text class="detail-title">{{ article.title }}</text>
<text class="detail-source">{{ article.source }}</text><text class="detail-meta">{{ article.date }} · {{ article.readingTime }}阅读</text>
<image class="detail-image" :src="article.image" mode="aspectFill"></image>
<text class="detail-summary">{{ article.summary }}</text>
<view v-for="section in article.sections" :key="section.title" class="detail-section"><text class="detail-heading">{{ section.title }}</text><text class="detail-paragraph">{{ section.body }}</text></view>
<text class="detail-note">内容仅供信息参考,具体申请条件以相关机构官方说明为准。</text>
</view>
<view v-else class="empty-content"><text class="empty-title">暂未找到这篇新闻</text><text class="empty-copy">返回新闻列表,查看其他资讯。</text><button class="primary-button empty-button" @click="goNews">返回新闻</button></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { newsArticles, findNewsArticle, NewsArticle } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
const { contentStyle, pageStyle } = usePageLayout()
const article = ref<NewsArticle>(newsArticles[0])
const available = ref(true)
onLoad((options) => {
if (options == null) return
const id = options['id'] == null ? '' : options['id'] as string
const legacyTitle = options['title'] == null ? '' : options['title'] as string
if (id == '' && legacyTitle == '') return
const selected = findNewsArticle(id, legacyTitle)
available.value = selected != null
if (selected != null) article.value = selected
})
const goNews = (): void => { uni.redirectTo({ url: '/packages/content/news/news' }) }
</script>
<style>
.detail-content { padding: 30rpx 30rpx 64rpx; }
.detail-category { color: #0868f7; font-size: 24rpx; font-weight: 600; margin-bottom: 14rpx; }
.detail-title { color: #172640; font-size: 38rpx; line-height: 56rpx; font-weight: 700; }
.detail-source { color: #66758a; font-size: 24rpx; margin-top: 22rpx; }
.detail-meta { color: #66758a; font-size: 22rpx; margin-top: 10rpx; }
.detail-image { width: 100%; height: 300rpx; border-radius: 18rpx; margin-top: 28rpx; }
.detail-summary { color: #405675; font-size: 27rpx; line-height: 46rpx; padding: 22rpx; margin-top: 24rpx; background-color: #f2f6fc; border-radius: 12rpx; }
.detail-section { margin-top: 30rpx; }
.detail-heading { color: #172640; font-size: 30rpx; line-height: 44rpx; font-weight: 700; }
.detail-paragraph { color: #4d5e75; font-size: 28rpx; line-height: 50rpx; margin-top: 12rpx; }
.detail-note { color: #66758a; font-size: 22rpx; line-height: 36rpx; border-top: 1rpx solid #e8eef6; padding-top: 22rpx; margin-top: 36rpx; }
.empty-content { padding: 100rpx 40rpx; align-items: center; }
.empty-title { color: #172640; font-size: 32rpx; font-weight: 700; }
.empty-copy { color: #66758a; font-size: 26rpx; margin-top: 20rpx; }
.empty-button { width: 100%; margin-top: 38rpx; }
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="融资课堂" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<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="/packages/content/static/news-policy.png" mode="aspectFill" class="hero-image"></image></view>
<view class="topic-section">
<text class="section-title">按场景学习</text>
<view class="topic-list">
<view v-for="item in topics" :key="item.id" :class="activeTopic == item.id ? 'topic-card selected-topic' : 'topic-card'" @click="selectTopic(item.id)">
<AppIcon :name="item.icon" :size="48" />
<view class="topic-copy"><text class="topic-title">{{ item.title }}</text><text class="topic-desc">{{ item.desc }}</text></view>
<text v-if="activeTopic == item.id" class="topic-selected">已选</text><AppIcon v-else name="chevron-right" :size="28" muted />
</view>
</view>
</view>
<view class="lesson-section">
<view class="section-head"><text class="section-title">{{ activeTopic == '' ? '精选课程' : activeTopicTitle }}</text><view class="all-lessons" @click="activeTopic = ''"><text class="lesson-count">{{ activeTopic == '' ? lessons.length + ' 门课程' : '查看全部' }}</text></view></view>
<view class="lesson-card" v-for="item in lessons" :key="item.id" @click="openLesson(item.id)">
<image :src="item.image" mode="aspectFill" class="lesson-card-image"></image>
<view class="lesson-copy"><text class="lesson-title">{{ item.title }}</text><text class="lesson-desc">{{ item.desc }}</text><view class="lesson-meta"><text class="lesson-duration">{{ item.duration }}</text><text class="lesson-tag">{{ item.tag }}</text></view></view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { financeLessons, FinanceLesson } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout()
type LessonTopic = { id: string, title: string, desc: string, icon: string }
const topics: LessonTopic[] = [
{ id: 'credit', title: '个人信用提升', desc: '了解信用记录与还款能力', icon: 'user' },
{ id: 'business', title: '小微经营融资', desc: '梳理经营材料与融资成本', icon: 'building' },
{ id: 'documents', title: '材料准备清单', desc: '按申请场景,逐项检查资料', icon: 'file-text' }
]
const activeTopic = ref('')
const activeTopicTitle = computed((): string => topics.find((item: LessonTopic): boolean => item.id == activeTopic.value)?.title ?? '精选课程')
const lessons = computed((): FinanceLesson[] => {
if (activeTopic.value == '') return financeLessons
return financeLessons.filter((item: FinanceLesson): boolean => item.topics.includes(activeTopic.value))
})
const selectTopic = (id: string): void => { activeTopic.value = activeTopic.value == id ? '' : id }
const openLesson = (id: string): void => { uni.navigateTo({ url: '/packages/content/lesson/lesson?id=' + id }) }
</script>
<style>
.classroom-content { padding: 24rpx 28rpx 48rpx; }
.classroom-hero { border-radius: 20rpx; background-color: #e8f2ff; flex-direction: row; overflow: hidden; align-items: center; }
.hero-copy { flex: 1; padding: 28rpx 12rpx 28rpx 26rpx; }
.hero-eyebrow { color: #0868f7; font-size: 22rpx; font-weight: 600; }
.hero-title { color: #14335f; font-size: 37rpx; line-height: 51rpx; font-weight: 700; margin-top: 10rpx; }
.hero-title-next { margin-top: 0; }
.hero-desc { color: #4b6586; font-size: 23rpx; line-height: 36rpx; margin-top: 14rpx; }
.hero-image { width: 230rpx; height: 280rpx; flex-shrink: 0; }
.topic-section { margin-top: 28rpx; }
.topic-list { margin-top: 16rpx; }
.topic-card { flex-direction: row; align-items: center; min-height: 116rpx; padding: 18rpx 22rpx; border-radius: 16rpx; border: 1rpx solid #e5ecf5; background-color: #ffffff; margin-bottom: 12rpx; }
.selected-topic { background-color: #eef5ff; border-color: #99c1fb; }
.topic-copy { flex: 1; margin-left: 20rpx; }
.topic-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; }
.topic-desc { color: #66758a; font-size: 23rpx; line-height: 34rpx; margin-top: 4rpx; }
.topic-selected { color: #0868f7; font-size: 23rpx; margin-left: 10rpx; }
.lesson-section { margin-top: 18rpx; }
.section-head { flex-direction: row; align-items: center; justify-content: space-between; }
.all-lessons { padding: 12rpx 0 12rpx 20rpx; }
.lesson-count { color: #0868f7; font-size: 23rpx; }
.lesson-card { margin-top: 16rpx; padding: 20rpx; border-radius: 18rpx; background-color: #ffffff; border: 1rpx solid #e5ecf5; flex-direction: row; align-items: center; }
.lesson-card-image { width: 142rpx; height: 150rpx; border-radius: 12rpx; margin-right: 20rpx; flex-shrink: 0; }
.lesson-copy { flex: 1; }
.lesson-title { color: #172640; font-size: 30rpx; line-height: 42rpx; font-weight: 600; }
.lesson-desc { color: #66758a; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
.lesson-meta { flex-direction: row; align-items: center; margin-top: 12rpx; }
.lesson-duration { color: #66758a; font-size: 22rpx; }
.lesson-tag { margin-left: 16rpx; color: #0868f7; font-size: 22rpx; }
</style>
+112
View File
@@ -0,0 +1,112 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="课程详情" />
<scroll-view scroll-y :scroll-into-view="scrollTarget" :style="contentStyle(96, false)">
<view v-if="available" class="lesson-detail-content">
<image class="lesson-cover" :src="lesson.image" mode="aspectFill"></image>
<text class="lesson-detail-title">{{ lesson.title }}</text>
<view class="lesson-detail-meta"><text class="lesson-tag">{{ lesson.tag }}</text><text class="lesson-duration">{{ lesson.duration }} · {{ lesson.chapters.length }} 节图文</text></view>
<text class="lesson-intro">{{ lesson.intro }}</text>
<view class="progress-head"><text class="progress-label">学习进度</text><text class="progress-value">已完成 {{ completedCount }}/{{ lesson.chapters.length }} 节</text></view>
<view class="lesson-progress"><view class="progress-fill" :style="'width:' + progress + '%;'"></view></view>
<button class="primary-button start-learning" @click="start">{{ completedCount == 0 ? '开始学习' : (completedCount == lesson.chapters.length ? '回顾课程' : '继续学习') }}</button>
<text class="chapter-heading">课程目录</text>
<view class="chapter-card" v-for="(item, index) in lesson.chapters" :key="item.title" :id="'chapter-' + index">
<view class="chapter-row" @click="toggleChapter(index)">
<view :class="completed[index] ? 'chapter-index completed-index' : 'chapter-index'"><text :class="completed[index] ? 'index-text completed-text' : 'index-text'">{{ index + 1 }}</text></view>
<view class="chapter-copy"><text class="chapter-title">{{ item.title }}</text><text :class="completed[index] ? 'chapter-state completed-state' : 'chapter-state'">{{ completed[index] ? '已完成 · 可再次阅读' : '点击展开阅读' }}</text></view>
<AppIcon name="chevron-right" :size="28" :class="activeIndex == index ? 'chapter-arrow open-arrow' : 'chapter-arrow'" muted />
</view>
<view v-if="activeIndex == index" class="chapter-content">
<text class="chapter-body">{{ item.body }}</text>
<button :class="completed[index] ? 'chapter-done chapter-already-done' : 'chapter-done'" :disabled="completed[index]" @click="completeChapter(index)">{{ completed[index] ? '本节已完成' : '我已读完,完成本节' }}</button>
</view>
</view>
</view>
<view v-else class="empty-content"><text class="empty-title">暂未找到这门课程</text><text class="empty-copy">返回融资课堂,选择其他课程。</text><button class="primary-button empty-button" @click="goClassroom">返回课堂</button></view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { financeLessons, findFinanceLesson, FinanceLesson } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout()
const lesson = ref<FinanceLesson>(financeLessons[0])
const available = ref(true)
const completed = ref<boolean[]>([])
const activeIndex = ref(-1)
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)
}
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))
onLoad((options) => {
if (options != null) {
const id = options['id'] == null ? '' : options['id'] as string
const legacyTitle = options['title'] == null ? '' : options['title'] as string
if (id != '' || legacyTitle != '') {
const selected = findFinanceLesson(id, legacyTitle)
available.value = selected != null
if (selected != null) lesson.value = selected
}
}
restoreProgress()
})
const toggleChapter = (index: number): void => { activeIndex.value = activeIndex.value == index ? -1 : index }
const completeChapter = (index: number): void => {
uni.setStorageSync(storageKey(index), true)
const next = completed.value.slice()
next[index] = true
completed.value = next
}
const start = (): void => {
let nextIndex = completed.value.findIndex((value: boolean): boolean => !value)
if (nextIndex < 0) nextIndex = 0
activeIndex.value = nextIndex
scrollTarget.value = ''
nextTick(() => { scrollTarget.value = 'chapter-' + nextIndex })
}
const goClassroom = (): void => { uni.redirectTo({ url: '/packages/content/classroom/classroom' }) }
</script>
<style>
.lesson-detail-content { padding: 24rpx 28rpx 48rpx; }
.lesson-cover { width: 100%; height: 260rpx; border-radius: 18rpx; }
.lesson-detail-title { color: #172640; font-size: 36rpx; line-height: 50rpx; font-weight: 700; margin-top: 24rpx; }
.lesson-detail-meta { flex-direction: row; justify-content: space-between; align-items: center; margin-top: 14rpx; }
.lesson-tag { color: #0868f7; font-size: 24rpx; }
.lesson-duration { color: #66758a; font-size: 23rpx; }
.lesson-intro { color: #66758a; font-size: 26rpx; line-height: 44rpx; margin-top: 22rpx; }
.progress-head { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 26rpx; }
.progress-label { color: #172640; font-size: 24rpx; font-weight: 600; }
.progress-value { color: #0868f7; font-size: 23rpx; }
.lesson-progress { height: 10rpx; margin-top: 12rpx; border-radius: 6rpx; background-color: #e1eafa; overflow: hidden; }
.progress-fill { height: 100%; border-radius: 6rpx; background-color: #0868f7; }
.start-learning { margin-top: 24rpx; }
.chapter-heading { color: #172640; font-size: 30rpx; font-weight: 700; margin-top: 32rpx; margin-bottom: 14rpx; }
.chapter-card { border: 1rpx solid #e5ecf5; background-color: #ffffff; border-radius: 16rpx; margin-bottom: 14rpx; overflow: hidden; }
.chapter-row { min-height: 116rpx; padding: 20rpx; flex-direction: row; align-items: center; }
.chapter-index { width: 48rpx; height: 48rpx; border-radius: 14rpx; background-color: #eaf2ff; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
.index-text { color: #0868f7; font-size: 24rpx; font-weight: 700; }
.completed-index { background-color: #eaf7ef; }
.completed-text { color: #168454; }
.chapter-copy { flex: 1; padding-right: 8rpx; }
.chapter-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; }
.chapter-state { color: #66758a; font-size: 22rpx; margin-top: 6rpx; }
.completed-state { color: #168454; }
.chapter-arrow { transform: rotate(90deg); }
.open-arrow { transform: rotate(-90deg); }
.chapter-content { padding: 0 22rpx 24rpx; }
.chapter-body { color: #4d5e75; font-size: 27rpx; line-height: 48rpx; border-top: 1rpx solid #e8eef6; padding-top: 22rpx; }
.chapter-done { margin-top: 24rpx; width: 100%; height: 80rpx; line-height: 80rpx; border-radius: 12rpx; background-color: #eaf2ff; color: #0868f7; font-size: 26rpx; font-weight: 600; }
.chapter-already-done { color: #168454; background-color: #edf8f1; }
.empty-content { padding: 100rpx 40rpx; align-items: center; }
.empty-title { color: #172640; font-size: 32rpx; font-weight: 700; }
.empty-copy { color: #66758a; font-size: 26rpx; margin-top: 20rpx; }
.empty-button { width: 100%; margin-top: 38rpx; }
</style>
+66
View File
@@ -0,0 +1,66 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="新闻" :back="false" />
<view class="news-tabs">
<view v-for="item in tabs" :key="item" :class="tab == item ? 'news-tab active-tab' : 'news-tab'" @click="tab = item"><text :class="tab == item ? 'tab-text active-text' : 'tab-text'">{{ item }}</text></view>
</view>
<scroll-view scroll-y :style="contentStyle(314, false)">
<view class="news-content">
<view v-if="tab == '推荐'" class="headline" @click="openArticle(headline.id)">
<view class="headline-copy"><text class="headline-tag">政策观察</text><text class="headline-title">{{ headline.title }}</text><text class="headline-date">{{ headline.date }}</text></view>
<image class="headline-art" :src="headline.image" mode="aspectFill"></image>
</view>
<view class="list-head"><text class="list-title">{{ tab == '推荐' ? '最新资讯' : tab }}</text><text class="list-count">{{ filteredArticles.length }} 篇</text></view>
<view class="article-list">
<view class="article" v-for="(item, index) in filteredArticles" :key="item.id" :class="index == filteredArticles.length - 1 ? 'last-article' : ''" @click="openArticle(item.id)">
<view class="article-copy"><text class="article-title">{{ item.title }}</text><text class="article-summary">{{ item.summary }}</text><text class="article-meta">{{ item.category }} · {{ item.date }}</text></view>
<image class="article-art" :src="item.image" mode="aspectFill"></image>
</view>
</view>
</view>
</scroll-view>
<BottomNav active="news" />
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import { newsArticles, NewsArticle } from '@/packages/content/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue'
const { contentStyle, pageStyle } = usePageLayout()
const tabs: string[] = ['推荐', '企业融资', '个人融资', '政策解读']
const tab = ref('推荐')
const headline: NewsArticle = newsArticles[0]
const filteredArticles = computed((): NewsArticle[] => {
if (tab.value == '推荐') return newsArticles.filter((item: NewsArticle): boolean => item.id != headline.id)
return newsArticles.filter((item: NewsArticle): boolean => item.category == tab.value)
})
const openArticle = (id: string): void => { uni.navigateTo({ url: '/packages/content/article/article?id=' + id }) }
</script>
<style>
.news-tabs { height: 88rpx; flex-direction: row; align-items: center; background-color: #ffffff; border-bottom: 1rpx solid #e8eef6; padding: 0 22rpx; }
.news-tab { flex: 1; height: 88rpx; justify-content: center; align-items: center; border-bottom: 4rpx solid transparent; }
.active-tab { border-bottom-color: #0868f7; }
.tab-text { color: #66758a; font-size: 26rpx; }
.active-text { color: #0868f7; font-weight: 700; }
.news-content { padding: 24rpx 28rpx 90rpx; }
.headline { min-height: 256rpx; border-radius: 20rpx; background-color: #123c7b; flex-direction: row; overflow: hidden; }
.headline-copy { flex: 1; padding: 26rpx 20rpx 26rpx 26rpx; }
.headline-tag { color: #b8ddff; font-size: 22rpx; font-weight: 600; }
.headline-title { color: #ffffff; font-size: 31rpx; line-height: 45rpx; font-weight: 700; margin-top: 12rpx; }
.headline-date { color: #c4d7ed; font-size: 22rpx; margin-top: 16rpx; }
.headline-art { width: 212rpx; height: 282rpx; flex-shrink: 0; align-self: center; }
.list-head { flex-direction: row; justify-content: space-between; align-items: center; margin: 30rpx 0 16rpx; }
.list-title { color: #172640; font-size: 30rpx; font-weight: 700; }
.list-count { color: #66758a; font-size: 22rpx; }
.article-list { background-color: #ffffff; border-radius: 18rpx; padding: 0 22rpx; }
.article { padding: 26rpx 0; flex-direction: row; border-bottom: 1rpx solid #e8eef6; align-items: flex-start; }
.last-article { border-bottom-width: 0; }
.article-copy { flex: 1; padding-right: 18rpx; }
.article-title { color: #172640; font-size: 29rpx; line-height: 42rpx; font-weight: 600; }
.article-summary { color: #66758a; font-size: 24rpx; line-height: 36rpx; margin-top: 10rpx; }
.article-meta { color: #66758a; font-size: 22rpx; line-height: 32rpx; margin-top: 14rpx; }
.article-art { width: 148rpx; height: 132rpx; border-radius: 12rpx; flex-shrink: 0; margin-top: 4rpx; }
</style>
+104
View File
@@ -0,0 +1,104 @@
export type ContentSection = { title: string, body: string }
export type NewsArticle = {
id: string, title: string, summary: string, category: string,
date: string, source: string, readingTime: string, image: string, sections: ContentSection[]
}
export type FinanceLesson = {
id: string, title: string, desc: string, duration: string, tag: string,
image: string, intro: string, topics: string[], chapters: ContentSection[]
}
// 新闻与课程独立建模,接入后端时可分别替换为对应接口。
export const newsArticles: NewsArticle[] = [
{
id: 'inclusive-finance', title: '普惠金融政策,企业申请前应关注什么?',
summary: '看清适用主体、申请条件与材料要求,找到适合企业的支持方式。', category: '政策解读',
date: '2026-09-08', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/packages/content/static/news-city.png',
sections: [
{ title: '先核对政策适用范围', body: '普惠金融支持措施通常对经营主体、企业规模和经营所在地设有适用条件。阅读政策时,先核对发文机构、生效时间与适用范围,再确认企业是否满足基本要求。' },
{ title: '区分贷款、担保与贴息', body: '信贷产品、融资担保和财政贴息的申请渠道与审核流程可能不同。获得政策支持不等于贷款必然获批,额度、利率和期限仍需以金融机构的实际审核结果为准。' },
{ title: '从官方渠道核实并准备资料', body: '建议通过当地政府、金融管理部门或银行官方网站核实细则,准备营业执照、经营流水、纳税记录与资金用途说明。本文提供阅读方法,不代表具体政策承诺。' }
]
},
{
id: 'personal-credit', title: '申请个人信用融资,先核对这三项信息',
summary: '征信记录、收入稳定性与现有负债,是申请前的重点检查项。', category: '个人融资',
date: '2026-09-05', source: 'AI融资编辑整理', readingTime: '3 分钟', image: '/static/mobile/images/news-calculator.png',
sections: [
{ title: '查看信用记录是否准确', body: '通过官方渠道查询个人信用报告,核对账户、还款记录与查询记录。发现错误信息时,应通过正规异议渠道处理,不轻信付费“修复征信”承诺。' },
{ title: '整理可以核验的收入证明', body: '根据职业情况整理工资流水、经营收款、社保或纳税记录,确保填写的收入与材料一致。不同机构认可的材料范围有所不同,请以申请页面或机构说明为准。' },
{ title: '量力确定额度与月供', body: '将现有贷款和信用卡应还金额纳入每月支出,再评估新增还款是否可持续。避免为尝试额度而短期内集中向多家机构提交申请。' }
]
},
{
id: 'loan-comparison', title: '比较融资产品时,别只看宣传利率',
summary: '年化利率、附加费用与还款方式,需要放在一起比较。', category: '个人融资',
date: '2026-09-03', source: 'AI融资编辑整理', readingTime: '3 分钟', image: '/packages/content/static/news-city.png',
sections: [
{ title: '先统一利率口径', body: '日利率、月利率与年化利率并不是同一个口径。比较时应查看机构披露的年化利率及计算方式,尤其注意“低至”利率所附的适用条件。' },
{ title: '将费用计入总成本', body: '核对合同是否包含担保费、服务费等支出,以及提前还款是否有额外费用。应以正式合同、还款计划和实际审核结果为准。' },
{ title: '让还款安排匹配现金流', body: '等额本息、等额本金、先息后本的还款节奏各不相同。除了总利息,还应关注每期应还金额和到期本金,避免因最后一期集中还款造成压力。' }
]
},
{
id: 'business-documents', title: '小微企业融资:常见材料如何提前整理',
summary: '按主体、经营和用途三类整理资料,减少补充沟通。', category: '企业融资',
date: '2026-08-30', source: 'AI融资编辑整理', readingTime: '4 分钟', image: '/packages/content/static/news-business.png',
sections: [
{ title: '主体资料保持一致', body: '核对营业执照、实际经营地址、法定代表人及联系人信息。涉及经营主体或股权变更时,准备相应说明,避免不同材料之间的信息不一致。' },
{ title: '整理持续经营的证明', body: '经营流水、纳税记录、采购或销售合同可以帮助机构了解业务情况。按照时间顺序整理,并区分经营性收入、借款与其他往来款。' },
{ title: '说明资金用途与还款来源', body: '将资金需求对应到采购、周转或设备投入等具体用途,并准备与之匹配的凭证。不同机构要求不同,正式提交前请核对其最新材料清单。' }
]
}
]
export const financeLessons: FinanceLesson[] = [
{
id: 'credit-basics', title: '个人信用申请准备课', desc: '用三步梳理信用、收入与负债', duration: '6 分钟',
tag: '个人融资', image: '/static/mobile/images/news-calculator.png', topics: ['credit', 'documents'],
intro: '从自己的实际情况出发,建立一份清晰的申请前检查清单。完成每一节阅读后,可手动记录学习进度。',
chapters: [
{ title: '读懂自己的信用记录', body: '先通过官方渠道获取个人信用报告,核对账户数量、当前负债与还款记录。\n\n对确有错误的信息,按官方异议流程申请核查;真实的不良记录不能通过付费承诺消除。\n\n本节练习:记录需要核对的账户,并确认最近的还款安排。' },
{ title: '整理收入与还款支出', body: '把稳定收入、必要生活支出及已有月供分别列出,观察每月可用于新增还款的资金是否充足。\n\n奖金、临时收入与不确定的经营回款不宜直接视为稳定收入。预留应急资金也有助于避免还款压力。\n\n本节练习:用最近几个月的实际流水,估算可持续的月供范围。' },
{ title: '核对材料并选择申请渠道', body: '按照目标机构的清单准备身份证明、收入证明及相关授权,检查姓名、工作与收入信息是否一致。\n\n通过银行或持牌机构的官方渠道申请,不向不明个人账户支付所谓“保证通过”的费用。审批结果以机构审核为准。\n\n本节练习:列出已准备和待补充的材料,提交前再核对一次。' }
]
},
{
id: 'business-checklist', title: '小微经营融资准备课', desc: '把经营情况整理成完整材料清单', duration: '8 分钟',
tag: '企业融资', image: '/packages/content/static/news-business.png', topics: ['business', 'documents'],
intro: '围绕真实经营、资金用途和还款来源,分步骤整理企业融资所需的信息。具体材料要求以申请机构为准。',
chapters: [
{ title: '核对企业主体与经营信息', body: '核对营业执照、经营地址、法定代表人与实际控制人信息,确保材料在有效期内。\n\n介绍企业业务时,可以说明主要产品、客户类型和经营年限。信息以真实经营情况为准,无需为了申请夸大规模。\n\n本节练习:核对主体证件与申请表中各项信息是否一致。' },
{ title: '归集流水与纳税记录', body: '按照申请机构要求的时间范围整理经营账户流水、纳税记录和业务合同。将经营收入与借款、股东往来等款项区分开。\n\n对季节性波动或较大的单笔交易,准备可核验的业务说明,帮助机构理解经营现金流。\n\n本节练习:按月份归档资料,标记需要补充解释的收支。' },
{ title: '写清资金用途与还款计划', body: '将需求拆解为采购、订单周转或设备投入等实际用途,并评估资金回笼时间。\n\n融资期限应尽量与经营周期相匹配,避免依赖不确定的下一笔借款偿还到期本金。\n\n本节练习:写下所需金额、预计使用时间与还款来源。' },
{ title: '检查授权范围与提交清单', body: '提交前阅读数据授权说明,确认授权对象、用途、范围与期限。不要将账户密码、短信验证码交给不明中介。\n\n将所有附件与机构清单逐项比对,保留提交记录,后续按机构通知补充材料。\n\n本节练习:完成材料清单核对,并确认使用的是官方申请渠道。' }
]
},
{
id: 'loan-cost', title: '利率与还款方式入门', desc: '看懂总成本,选好还款节奏', duration: '5 分钟',
tag: '基础知识', image: '/packages/content/static/news-policy.png', topics: ['credit', 'business'],
intro: '把利率、期限和还款方式放在一起理解,学会用统一口径比较融资方案。',
chapters: [
{ title: '比较利率先看口径', body: '比较产品时,优先查看正式披露的年化利率及计算方式。日息、月息或“每万元每天几元”的宣传表达不能直接互相比高低。\n\n实际适用利率通常与审核结果相关,页面显示的最低利率不代表每个人都能获得。\n\n本节练习:找到一份产品说明中的年化利率、适用条件与费用说明。' },
{ title: '认识不同的还款节奏', body: '等额本息通常每期还款额相近;等额本金的本金部分相同,随着剩余本金减少,利息逐步降低。\n\n先息后本需要在到期时集中偿还本金。每月压力较小并不意味着到期压力也小,应提前规划本金来源。\n\n本节练习:对照还款计划,找出金额最大的一期。' },
{ title: '把费用与现金流一起算', body: '核对担保费、服务费与提前还款规则,将必要费用计入总成本。比较方案时还要统一金额与期限。\n\n选择能被自身现金流持续覆盖的还款安排,不只追求额度或最长的期限。最终以合同约定和实际审核结果为准。\n\n本节练习:记录总还款额、每期应还金额及提前还款条件。' }
]
}
]
export function findNewsArticle(id: string, legacyTitle: string): NewsArticle | null {
let articleId = id
if (legacyTitle == 'headline' || legacyTitle == '普惠金融新政解读' || legacyTitle == '普惠金融新政解读:小微企业迎来融资利好') articleId = 'inclusive-finance'
if (legacyTitle == '如何提高个人贷款通过率') articleId = 'personal-credit'
if (legacyTitle == '银行最新贷款产品汇总') articleId = 'loan-comparison'
if (legacyTitle == '小微企业融资材料清单') articleId = 'business-documents'
return newsArticles.find((item: NewsArticle): boolean => item.id == articleId || item.title == legacyTitle) ?? null
}
export function findFinanceLesson(id: string, legacyTitle: string): FinanceLesson | null {
let lessonId = id
if (id == '0' || legacyTitle == '个人信用提升' || legacyTitle == '如何提高个人贷款通过率') lessonId = 'credit-basics'
if (id == '1' || legacyTitle == '小微经营融资' || legacyTitle == '材料准备清单' || legacyTitle == '小微企业融资材料清单') lessonId = 'business-checklist'
if (id == '2' || legacyTitle == '读懂贷款利率与还款方式') lessonId = 'loan-cost'
return financeLessons.find((item: FinanceLesson): boolean => item.id == lessonId || item.title == legacyTitle) ?? null
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB