feat: 完成项目基础功能搭建与样式优化

1.  新增评估选择组件、本地草稿存储与新闻/金融内容服务
2.  重构页面布局与样式:调整按钮阴影、图标尺寸、导航栏样式
3.  更新启动页、报告页、消息中心等页面UI与交互逻辑
4.  替换图标资源,调整禁用态透明度
5.  补充README文档与本地数据模拟实现
This commit is contained in:
hujun
2026-09-18 12:59:43 +08:00
parent b0ee4f9d7e
commit 94a4558290
30 changed files with 1376 additions and 640 deletions
+96 -39
View File
@@ -1,55 +1,112 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="课程详情" />
<scroll-view scroll-y class="lesson-detail-scroll" :style="contentStyle(96, false)"><view class="lesson-detail-content"><image class="lesson-cover" :src="cover" mode="aspectFill"></image><text class="lesson-detail-title">{{ title }}</text><view class="lesson-detail-meta"><text>{{ tag }}</text><text>{{ duration }} · 课程</text></view><view class="lesson-progress"><view class="progress-fill"></view></view><text class="lesson-intro">本课程将从实际申请场景出发,帮助你理解准备材料、评估资质和选择产品的关键步骤。</text><view class="chapter" v-for="(item,index) in chapters" :key="item"><view class="chapter-index">{{ index + 1 }}</view><view class="chapter-copy"><text class="chapter-title">{{ item }}</text><text class="chapter-desc">预计 2 分钟 · 可随时回看</text></view><AppIcon name="chevron-right" :size="28" muted /></view><button class="primary-button start-learning" @click="start">开始学习</button></view></scroll-view>
<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'
const { contentStyle, pageStyle } = usePageLayout()
import { financeLessons, findFinanceLesson, FinanceLesson } from '@/services/content.uts'
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
const title = ref('如何提高个人贷款通过率')
const tag = ref('个人融资')
const duration = ref('6 分钟')
const cover = ref('/static/mobile/images/news-calculator.png')
const chapters = ref(['理解征信评分', '整理收入与负债', '准备申请材料', '选择适合的产品'])
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) return
const lessonId = options['id'] == null ? '' : options['id'] as string
if (lessonId == '1') { title.value = '小微企业融资材料清单'; tag.value = '企业融资'; duration.value = '8 分钟'; cover.value = '/static/mobile/images/news-business.png'; chapters.value = ['确认主体与经营信息', '整理银行流水与纳税记录', '准备企业资质材料', '核对授权与提交清单'] }
else if (lessonId == '2') { title.value = '读懂贷款利率与还款方式'; tag.value = '基础知识'; duration.value = '5 分钟'; cover.value = '/static/mobile/images/news-city.png'; chapters.value = ['年化利率怎么看', '等额本息与先息后本', '期限对总成本的影响', '选择适合现金流的方案'] }
if (options['title'] != null) title.value = options['title'] as string
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 start = () => { uni.showToast({ title: '课程内容已准备好', icon: 'none' }) }
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: '/pages/classroom/classroom' }) }
</script>
<style>
.lesson-detail-scroll { }
.lesson-detail-content { padding: 26rpx 28rpx 90rpx; }
.lesson-cover { width: 100%; height: 290rpx; border-radius: 18rpx; }
.lesson-detail-title {
/* #ifndef APP */
display: block;
/* #endif */
color: #172640; font-size: 36rpx; line-height: 50rpx; font-weight: 800; margin-top: 24rpx; }
.lesson-detail-meta { flex-direction: row; justify-content: space-between; color: #0868f7; font-size: 21rpx; margin-top: 12rpx; }
.lesson-progress { height: 12rpx; margin-top: 22rpx; border-radius: 8rpx; background-color: #e4edf8; overflow: hidden; }
.progress-fill { width: 12%; height: 100%; border-radius: 8rpx; background-color: #0868f7; }
.lesson-intro {
/* #ifndef APP */
display: block;
/* #endif */
color: #5f6d81; font-size: 24rpx; line-height: 42rpx; margin: 26rpx 0; }
.chapter { min-height: 100rpx; padding: 16rpx 0; border-bottom: 1rpx solid #e7edf5; flex-direction: row; align-items: center; }
.chapter-index { width: 48rpx; height: 48rpx; border-radius: 16rpx; background-color: #e8f2ff; color: #0868f7; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 800; margin-right: 16rpx; }
.chapter-copy { flex: 1; }
.chapter-title { color: #23324a; font-size: 25rpx; font-weight: 700; }
.chapter-desc {
/* #ifndef APP */
display: block;
/* #endif */
color: #97a3b2; font-size: 19rpx; margin-top: 7rpx; }
.start-learning { margin-top: 28rpx; }
.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>