feat: 完成项目基础功能搭建与样式优化
1. 新增评估选择组件、本地草稿存储与新闻/金融内容服务 2. 重构页面布局与样式:调整按钮阴影、图标尺寸、导航栏样式 3. 更新启动页、报告页、消息中心等页面UI与交互逻辑 4. 替换图标资源,调整禁用态透明度 5. 补充README文档与本地数据模拟实现
This commit is contained in:
@@ -26,9 +26,9 @@
|
|||||||
.page { background-color: #f5f8fc; }
|
.page { background-color: #f5f8fc; }
|
||||||
.page-white { background-color: #ffffff; }
|
.page-white { background-color: #ffffff; }
|
||||||
.content { padding: 24rpx 28rpx 190rpx; }
|
.content { padding: 24rpx 28rpx 190rpx; }
|
||||||
.primary-button, .orange-button { height: 92rpx; border-radius: 16rpx; color: #ffffff; font-size: 30rpx; font-weight: 700; line-height: 92rpx; text-align: center; box-shadow: 0 14rpx 28rpx rgba(0, 100, 255, 0.20); }
|
.primary-button, .orange-button { height: 92rpx; border-radius: 16rpx; color: #ffffff; font-size: 30rpx; font-weight: 700; line-height: 92rpx; text-align: center; box-shadow: 0 5rpx 14rpx rgba(0, 100, 255, 0.10); }
|
||||||
.primary-button { background-color: #0868f7; }
|
.primary-button { background-color: #0868f7; }
|
||||||
.orange-button { background-color: #ff7a16; box-shadow: 0 14rpx 28rpx rgba(255, 122, 22, 0.18); }
|
.orange-button { background-color: #ff7a16; box-shadow: 0 5rpx 14rpx rgba(255, 122, 22, 0.10); }
|
||||||
.section-title { font-size: 32rpx; font-weight: 700; color: #13213c; }
|
.section-title { font-size: 32rpx; font-weight: 700; color: #13213c; }
|
||||||
.muted { color: #8793a8; }
|
.muted { color: #8793a8; }
|
||||||
.surface { background-color: #ffffff; border: 2rpx solid #e8eef7; border-radius: 16rpx; }
|
.surface { background-color: #ffffff; border: 2rpx solid #e8eef7; border-radius: 16rpx; }
|
||||||
|
|||||||
@@ -36,3 +36,9 @@ $projectPath = (Get-Location).Path
|
|||||||
- 线上使用的轻量位图位于 `static/mobile/`,原始图片保留在 `static/images/` 与 `static/icons-generated/`。箭头继续使用 `static/icons/` 下的 SVG。
|
- 线上使用的轻量位图位于 `static/mobile/`,原始图片保留在 `static/images/` 与 `static/icons-generated/`。箭头继续使用 `static/icons/` 下的 SVG。
|
||||||
- 微信打包忽略原始图片目录和未使用的 `static/logo.png`,规则位于 `manifest.json` 的 `mp-weixin.packOptions.ignore`。新增资源要检查引用与排除规则,最终包体以微信开发者工具结果为准。
|
- 微信打包忽略原始图片目录和未使用的 `static/logo.png`,规则位于 `manifest.json` 的 `mp-weixin.packOptions.ignore`。新增资源要检查引用与排除规则,最终包体以微信开发者工具结果为准。
|
||||||
- API 地址目前为空,表示模拟模式。接入后端时需补齐环境配置、认证、错误契约及真实业务校验,再进行联调。
|
- API 地址目前为空,表示模拟模式。接入后端时需补齐环境配置、认证、错误契约及真实业务校验,再进行联调。
|
||||||
|
|
||||||
|
## 当前前端交互
|
||||||
|
|
||||||
|
个人与企业评估分别保存本机草稿;选图仅供本机预览,资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。
|
||||||
|
|
||||||
|
新闻与课程的数据定义位于 `services/content.uts`,产品定义位于 `services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中 `amount` 为万元数值、`rate` 为年化百分比数值。申请入口目前仅引导准备资料,不向金融机构提交申请。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<view class="header-wrap">
|
<view class="header-wrap">
|
||||||
<view class="status-bar" :style="statusStyle(0)"></view>
|
<view class="status-bar" :style="statusStyle(0)"></view>
|
||||||
<view class="app-header">
|
<view class="app-header">
|
||||||
<view class="header-side" @click="handleBack"><AppIcon v-if="back" name="chevron-right" :size="34" class="back-icon" /></view>
|
<view class="header-side" @click="handleBack"><AppIcon v-if="back" name="chevron-right" :size="48" class="back-icon" /></view>
|
||||||
<text class="header-title">{{ title }}</text>
|
<text class="header-title">{{ title }}</text>
|
||||||
<view class="header-side header-right"><slot></slot></view>
|
<view class="header-side header-right"><slot></slot></view>
|
||||||
</view>
|
</view>
|
||||||
@@ -13,9 +13,11 @@
|
|||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { statusStyle } = usePageLayout()
|
const { statusStyle } = usePageLayout()
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
const props = defineProps({ title: { type: String, default: '' }, back: { type: Boolean, default: true } })
|
const props = defineProps({ title: { type: String, default: '' }, back: { type: Boolean, default: true }, customBack: { type: Boolean, default: false } })
|
||||||
|
const emit = defineEmits(['back'])
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
if (!props.back) return
|
if (!props.back) return
|
||||||
|
if (props.customBack) { emit('back'); return }
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages()
|
||||||
if (pages.length > 1) uni.navigateBack({ delta: 1 })
|
if (pages.length > 1) uni.navigateBack({ delta: 1 })
|
||||||
else uni.reLaunch({ url: '/pages/home/home' })
|
else uni.reLaunch({ url: '/pages/home/home' })
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
.header-wrap { background-color: #ffffff; }
|
.header-wrap { background-color: #ffffff; }
|
||||||
.status-bar { }
|
.status-bar { }
|
||||||
.app-header { height: 96rpx; padding: 0 24rpx; flex-direction: row; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf1f7; }
|
.app-header { height: 96rpx; padding: 0 24rpx; flex-direction: row; align-items: center; justify-content: space-between; border-bottom: 1rpx solid #edf1f7; }
|
||||||
.header-side { width: 100rpx; height: 96rpx; align-items: flex-start; justify-content: center; }
|
.header-side { width: 88rpx; height: 96rpx; align-items: flex-start; justify-content: center; flex-shrink: 0; }
|
||||||
.header-right { align-items: flex-end; }
|
.header-right { align-items: flex-end; }
|
||||||
.back-icon { transform: rotate(180deg); margin-top: 0; }
|
.back-icon { transform: rotate(180deg); margin-top: 0; }
|
||||||
.header-title { font-size: 34rpx; font-weight: 700; color: #111d35; }
|
.header-title { font-size: 34rpx; font-weight: 700; color: #111d35; }
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ display: block;
|
|||||||
/* #endif */
|
/* #endif */
|
||||||
flex-shrink: 0; transform: scale(1.2); }
|
flex-shrink: 0; transform: scale(1.2); }
|
||||||
.vector-icon { transform: none; }
|
.vector-icon { transform: none; }
|
||||||
.muted-icon { opacity: .48; }
|
.muted-icon { opacity: .70; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<view class="select-row" @click="choose"><text class="select-label">{{ label }}</text><text :class="modelValue.length > 0 ? 'select-value' : 'select-value select-placeholder'">{{ modelValue.length > 0 ? modelValue : '请选择' }}</text><view class="select-arrow"><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||||
|
</template>
|
||||||
|
<script setup lang="uts">
|
||||||
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
|
const props = defineProps({ label: { type: String, default: '' }, modelValue: { type: String, default: '' }, options: { type: Array as PropType<string[]>, default: () => [] as string[] } })
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const choose = () => { uni.showActionSheet({ itemList: props.options, success: (result) => { if (result.tapIndex >= 0 && result.tapIndex < props.options.length) emit('update:modelValue', props.options[result.tapIndex]) } }) }
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.select-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
|
.select-label { font-size: 28rpx; color: #29374e; margin-right: 16rpx; flex-shrink: 0; }
|
||||||
|
.select-value { flex: 1; min-width: 0; text-align: right; color: #22324a; font-size: 27rpx; line-height: 38rpx; }
|
||||||
|
.select-placeholder { color: #8995a7; }
|
||||||
|
.select-arrow { width: 32rpx; height: 32rpx; margin-left: 12rpx; align-items: center; justify-content: center; transform: rotate(90deg); }
|
||||||
|
</style>
|
||||||
+16
-10
@@ -1,23 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="bottom-nav">
|
<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="43" :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'"><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/news/news')"><view :class="active == 'news' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="trend-up" :size="43" :muted="active != 'news'" /></view><text :class="active == 'news' ? '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('/pages/messages/messages')"><view :class="active == 'message' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="message" :size="43" :muted="active != 'message'" /></view><text :class="active == 'message' ? '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('/pages/profile/profile')"><view :class="active == 'profile' ? 'nav-icon active-icon' : 'nav-icon'"><AppIcon name="user" :size="43" :muted="active != 'profile'" /></view><text :class="active == 'profile' ? '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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
|
const { bottomStyle } = usePageLayout()
|
||||||
defineProps({ active: { type: String, default: 'home' } })
|
defineProps({ active: { type: String, default: 'home' } })
|
||||||
const go = (url: string) => { uni.redirectTo({ url: url }) }
|
const go = (url: string) => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 0 && '/' + pages[pages.length - 1].route == url) return
|
||||||
|
uni.reLaunch({ url: url })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bottom-nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; height: 130rpx; padding-bottom: env(safe-area-inset-bottom); background-color: #ffffff; border-top: 1rpx solid #e7edf6; flex-direction: row; box-shadow: 0 -10rpx 30rpx rgba(18, 38, 75, 0.06); }
|
.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: 60rpx; height: 60rpx; border-radius: 18rpx; align-items: center; justify-content: center; background-color: #f0f4f9; }
|
.nav-icon { width: 64rpx; height: 60rpx; border-radius: 18rpx; align-items: center; justify-content: center; }
|
||||||
.active-icon { background-color: #e6f0ff; }
|
.active-icon { background-color: #edf5ff; }
|
||||||
.nav-label { margin-top: 7rpx; font-size: 21rpx; color: #7b8799; }
|
.nav-label { margin-top: 7rpx; font-size: 24rpx; color: #64748b; }
|
||||||
.active-label { color: #0868f7; font-weight: 700; }
|
.active-label { color: #0868f7; font-weight: 700; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<view class="analyzing-page" :style="pageStyle()">
|
<view class="analyzing-page" :style="pageStyle()">
|
||||||
<view class="safe" :style="statusStyle(80)"></view>
|
<view class="safe" :style="statusStyle(80)"></view>
|
||||||
<text class="analyzing-title">AI分析中</text>
|
<text class="analyzing-title">AI分析中</text>
|
||||||
<text class="analyzing-sub">正在为您分析最优方案...</text>
|
<text class="analyzing-sub">正在整理融资评估信息…</text>
|
||||||
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
|
<view class="orbital"><view class="orbit orbit-one"></view><view class="orbit orbit-two"></view><view class="ai-core"><image src="/static/mobile/images/robot-cutout.png" mode="aspectFit" class="ai-core-image"></image></view></view>
|
||||||
<view class="analysis-list"><view class="analysis-item" v-for="(item,index) in items" :key="item"><view :class="progress > index ? 'analysis-check done' : 'analysis-check'"><text>{{ progress > index ? '✓' : '·' }}</text></view><text>{{ item }}</text></view></view>
|
<view class="analysis-list"><view class="analysis-item" v-for="(item,index) in items" :key="item"><view :class="progress > index ? 'analysis-check done' : 'analysis-check'"><text class="analysis-mark">{{ progress > index ? '✓' : '·' }}</text></view><text class="analysis-label">{{ item }}</text></view></view>
|
||||||
<text class="estimate">预计需要30-60秒,请稍候...</text>
|
<text class="estimate">正在生成示例报告,请稍候…</text>
|
||||||
<text class="view-now" @click="finish">查看演示结果</text>
|
<text class="view-now" @click="finish">查看演示结果</text>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -47,6 +47,8 @@
|
|||||||
.ai-core-image { width: 185rpx; height: 185rpx; border-radius: 50%; }
|
.ai-core-image { width: 185rpx; height: 185rpx; border-radius: 50%; }
|
||||||
.analysis-list { margin-top: 30rpx; width: 390rpx; }
|
.analysis-list { margin-top: 30rpx; width: 390rpx; }
|
||||||
.analysis-item { flex-direction: row; align-items: center; margin-top: 23rpx; font-size: 26rpx; color: #d8e6ff; }
|
.analysis-item { flex-direction: row; align-items: center; margin-top: 23rpx; font-size: 26rpx; color: #d8e6ff; }
|
||||||
|
.analysis-label { font-size: 27rpx; color: #d8e6ff; }
|
||||||
|
.analysis-mark { font-size: 24rpx; color: #ffffff; }
|
||||||
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
|
.analysis-check { width: 38rpx; height: 38rpx; border-radius: 50%; border: 2rpx solid #4c73a7; align-items: center; justify-content: center; margin-right: 17rpx; }
|
||||||
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
|
.analysis-check.done { background-color: #158aff; border-color: #41d7ff; color: white; }
|
||||||
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
|
.estimate { margin-top: 64rpx; color: #8faeda; font-size: 22rpx; }
|
||||||
|
|||||||
+48
-26
@@ -1,41 +1,63 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="我的融资档案" />
|
<AppHeader title="我的融资档案" />
|
||||||
<view class="archive-content">
|
<scroll-view scroll-y :style="contentStyle(96, false)"><view class="archive-content">
|
||||||
<view class="completion"><view class="completion-head"><text class="completion-title">档案完整度 75%</text><text class="completion-score">75%</text></view><text class="completion-desc">完善信息可获得更精准的融资方案</text><view class="progress-track"><view class="progress-bar"></view></view></view>
|
<view class="archive-switch"><text :class="type == 'personal' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('personal')">个人档案</text><text :class="type == 'enterprise' ? 'switch-item switch-active' : 'switch-item'" @click="switchType('enterprise')">企业档案</text></view>
|
||||||
<view class="archive-list"><view class="archive-row" v-for="item in archiveItems" :key="item.name" @click="edit(item.name)"><view class="archive-icon"><AppIcon :name="item.icon" :size="40" /></view><text class="archive-name">{{ item.name }}</text><text :class="item.done ? 'state done' : 'state pending'">{{ item.done ? '已完善' : '待完善' }}</text><AppIcon name="chevron-right" :size="28" muted /></view></view>
|
<view class="completion"><view class="completion-head"><text class="completion-title">档案完整度</text><text class="completion-score">{{ progress.percent }}<text class="score-unit">%</text></text></view><view class="progress-track"><view class="progress-bar" :style="'width:' + progress.percent + '%;'"></view></view><text class="completion-desc">{{ progress.percent == 100 ? '基础信息已完善,可继续查看评估方案。' : '完善基础信息,让融资评估更贴近您的情况。' }}</text></view>
|
||||||
<button class="primary-button complete-button" @click="complete">完善档案</button>
|
<view class="archive-list"><view class="archive-row" v-for="item in archiveItems" :key="item.step" @click="edit(item.step)"><AppIcon :name="item.icon" :size="48" /><view class="archive-labels"><text class="archive-name">{{ item.name }}</text><text class="archive-note">{{ item.note }}</text></view><text :class="item.done ? 'state state-done' : 'state'">{{ item.done ? (item.step == 3 ? '已选择' : '已完善') : (item.step == 3 ? '选填' : '待完善') }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
|
||||||
</view>
|
<button class="primary-button complete-button" @click="complete">{{ progress.percent == 100 ? '查看并确认信息' : '继续完善档案' }}</button>
|
||||||
|
<text class="local-note">草稿保存在当前设备,资料与授权可自主选择。</text>
|
||||||
|
</view></scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { 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'
|
||||||
type ArchiveItem = { icon: string, name: string, done: boolean }
|
import { AssessmentProgress, activeAssessmentType, assessmentProgress } from '@/services/assessment.uts'
|
||||||
const archiveItems: ArchiveItem[] = [{ icon: 'file-text', name: '基本信息', done: true }, { icon: 'income', name: '经营 / 收入信息', done: true }, { icon: 'asset', name: '资产 / 负债信息', done: true }, { icon: 'authorization', name: '资料授权', done: false }]
|
const { pageStyle, contentStyle } = usePageLayout()
|
||||||
const edit = (name: string) => { uni.showToast({ title: '编辑' + name, icon: 'none' }) }
|
type ArchiveItem = { icon: string, name: string, note: string, done: boolean, step: number }
|
||||||
const complete = () => { uni.navigateTo({ url: '/pages/assessment/assessment' }) }
|
const type = ref('personal')
|
||||||
|
const progress = ref<AssessmentProgress>({ percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false })
|
||||||
|
const archiveItems = computed((): ArchiveItem[] => [
|
||||||
|
{ icon: 'file-text', name: '基本信息', note: type.value == 'enterprise' ? '企业与联系人信息' : '身份与联系方式', done: progress.value.basicDone, step: 0 },
|
||||||
|
{ icon: 'income', name: type.value == 'enterprise' ? '经营信息' : '职业与收入', note: type.value == 'enterprise' ? '经营行业、年限与营收' : '职业、收入与缴纳情况', done: progress.value.incomeDone, step: 1 },
|
||||||
|
{ icon: 'asset', name: '资产与负债', note: '资产情况与信用卡使用', done: progress.value.assetsDone, step: 2 },
|
||||||
|
{ icon: 'authorization', name: '资料与授权', note: '本地资料与数据授权意向', done: progress.value.authorizationSelected, step: 3 }
|
||||||
|
])
|
||||||
|
const refresh = () => { progress.value = assessmentProgress(type.value) }
|
||||||
|
onShow(() => { type.value = activeAssessmentType(); refresh() })
|
||||||
|
const switchType = (next: string) => { type.value = next; uni.setStorageSync('assessmentActiveType', next); refresh() }
|
||||||
|
const edit = (step: number) => { uni.setStorageSync('assessmentEditStep', step); uni.navigateTo({ url: '/pages/assessment/assessment?type=' + type.value }) }
|
||||||
|
const complete = () => {
|
||||||
|
if (progress.value.percent == 100) uni.navigateTo({ url: '/pages/confirm/confirm' })
|
||||||
|
else if (!progress.value.basicDone) edit(0)
|
||||||
|
else if (!progress.value.incomeDone) edit(1)
|
||||||
|
else edit(2)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.archive-content { padding: 26rpx 28rpx; }
|
.archive-content { padding: 26rpx 30rpx 64rpx; }
|
||||||
.completion { padding: 30rpx; border-radius: 18rpx; background: linear-gradient(140deg, #e9f3ff, #f8fbff); border: 1rpx solid #dce9fa; }
|
.archive-switch { flex-direction: row; background-color: #eaf0f8; border-radius: 16rpx; padding: 6rpx; margin-bottom: 24rpx; }
|
||||||
|
.switch-item { flex: 1; text-align: center; font-size: 27rpx; color: #708099; padding: 18rpx 0; border-radius: 12rpx; }
|
||||||
|
.switch-active { background-color: #fff; color: #0868f7; font-weight: 600; }
|
||||||
|
.completion { padding: 30rpx; border-radius: 20rpx; background-color: #edf5ff; }
|
||||||
.completion-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
.completion-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
||||||
.completion-title { font-size: 30rpx; font-weight: 800; }
|
.completion-title { font-size: 30rpx; font-weight: 600; color: #233d64; }
|
||||||
.completion-score { color: #0868f7; font-size: 27rpx; font-weight: 800; }
|
.completion-score { color: #0868f7; font-size: 52rpx; line-height: 64rpx; font-weight: 800; }
|
||||||
.completion-desc { color: #748196; font-size: 22rpx; margin-top: 12rpx; }
|
.score-unit { color: #0868f7; font-size: 28rpx; font-weight: 600; }
|
||||||
.progress-track { height: 15rpx; border-radius: 8rpx; background-color: #dce8f6; margin-top: 24rpx; overflow: hidden; }
|
.completion-desc { color: #6c819d; font-size: 25rpx; line-height: 38rpx; margin-top: 20rpx; }
|
||||||
.progress-bar { width: 75%; height: 15rpx; border-radius: 8rpx; background-color: #0868f7; }
|
.progress-track { height: 12rpx; border-radius: 6rpx; background-color: #d9e6f8; margin-top: 20rpx; overflow: hidden; }
|
||||||
.archive-list { margin-top: 24rpx; background-color: #fff; border: 1rpx solid #e5ecf5; border-radius: 18rpx; padding: 0 24rpx; }
|
.progress-bar { height: 12rpx; border-radius: 6rpx; background-color: #0868f7; }
|
||||||
.archive-row { height: 110rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.archive-list { margin-top: 24rpx; background-color: #fff; border: 1rpx solid #e5ecf5; border-radius: 18rpx; padding: 0 22rpx; }
|
||||||
.archive-icon { width: 54rpx; height: 54rpx; border-radius: 17rpx; background-color: #e8f2ff; color: #0868f7; align-items: center; justify-content: center; font-size: 21rpx; font-weight: 800; margin-right: 17rpx; }
|
.archive-row { min-height: 126rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.archive-name { flex: 1; font-size: 25rpx; }
|
.archive-labels { flex: 1; min-width: 0; margin: 0 18rpx; }
|
||||||
.state { font-size: 22rpx; margin-right: 14rpx; }
|
.archive-name { font-size: 28rpx; color: #26374e; font-weight: 500; }
|
||||||
.done { color: #11aa76; }
|
.archive-note { font-size: 23rpx; color: #8190a4; margin-top: 8rpx; line-height: 34rpx; }
|
||||||
.pending { color: #ff7917; }
|
.state { font-size: 23rpx; color: #8c99ab; margin-right: 10rpx; flex-shrink: 0; }
|
||||||
.chevron { color: #a7b2c1; font-size: 36rpx; }
|
.state-done { color: #16a478; }
|
||||||
.complete-button { margin-top: 34rpx; }
|
.complete-button { margin-top: 32rpx; }
|
||||||
|
.local-note { font-size: 23rpx; color: #8795a8; line-height: 36rpx; margin-top: 22rpx; text-align: center; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+41
-20
@@ -1,32 +1,53 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page page-white" :style="pageStyle()">
|
||||||
<AppHeader title="新闻详情" />
|
<AppHeader title="新闻详情" />
|
||||||
<scroll-view scroll-y class="detail-scroll" :style="contentStyle(96, false)"><view class="detail-content"><text class="detail-title">{{ title }}</text><view class="detail-meta"><text>融资课堂 · 2026-09-08</text><text>6 分钟阅读</text></view><image class="detail-image" src="/static/mobile/images/news-city.png" mode="aspectFill"></image><text class="detail-paragraph">融资申请前,先把自己的征信、收入和负债情况梳理清楚,可以显著减少补充资料的时间,也更容易选择适合自己的产品。</text><text class="detail-heading">一、先看信用记录</text><text class="detail-paragraph">保持按时还款,控制信用卡使用率,并提前处理逾期或异常记录。机构会综合判断信用历史的连续性和稳定性。</text><text class="detail-heading">二、准备稳定的收入证明</text><text class="detail-paragraph">工资流水、纳税记录和经营收款记录都可以帮助机构了解还款能力。资料越完整,评估结果越稳定。</text><text class="detail-heading">三、选择匹配的融资方案</text><text class="detail-paragraph">额度、利率、期限和还款方式需要一起比较,优先选择与自身现金流匹配的产品。</text></view></scroll-view>
|
<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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
import { newsArticles, findNewsArticle, NewsArticle } from '@/services/content.uts'
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
const title = ref('如何提高个人贷款通过率')
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
onLoad((options) => { if (options != null && options['title'] != null) title.value = options['title'] as string })
|
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: '/pages/news/news' }) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.detail-scroll { }
|
.detail-content { padding: 30rpx 30rpx 64rpx; }
|
||||||
.detail-content { padding: 30rpx 28rpx 80rpx; background-color: #fff; }
|
.detail-category { color: #0868f7; font-size: 24rpx; font-weight: 600; margin-bottom: 14rpx; }
|
||||||
.detail-title { color: #172640; font-size: 38rpx; line-height: 54rpx; font-weight: 800; }
|
.detail-title { color: #172640; font-size: 38rpx; line-height: 56rpx; font-weight: 700; }
|
||||||
.detail-meta { flex-direction: row; justify-content: space-between; color: #95a1b1; font-size: 20rpx; margin-top: 16rpx; }
|
.detail-source { color: #66758a; font-size: 24rpx; margin-top: 22rpx; }
|
||||||
.detail-image { width: 100%; height: 300rpx; border-radius: 18rpx; margin-top: 26rpx; }
|
.detail-meta { color: #66758a; font-size: 22rpx; margin-top: 10rpx; }
|
||||||
.detail-heading {
|
.detail-image { width: 100%; height: 300rpx; border-radius: 18rpx; margin-top: 28rpx; }
|
||||||
/* #ifndef APP */
|
.detail-summary { color: #405675; font-size: 27rpx; line-height: 46rpx; padding: 22rpx; margin-top: 24rpx; background-color: #f2f6fc; border-radius: 12rpx; }
|
||||||
display: block;
|
.detail-section { margin-top: 30rpx; }
|
||||||
/* #endif */
|
.detail-heading { color: #172640; font-size: 30rpx; line-height: 44rpx; font-weight: 700; }
|
||||||
color: #22314b; font-size: 28rpx; line-height: 42rpx; font-weight: 800; margin-top: 30rpx; }
|
.detail-paragraph { color: #4d5e75; font-size: 28rpx; line-height: 50rpx; margin-top: 12rpx; }
|
||||||
.detail-paragraph {
|
.detail-note { color: #66758a; font-size: 22rpx; line-height: 36rpx; border-top: 1rpx solid #e8eef6; padding-top: 22rpx; margin-top: 36rpx; }
|
||||||
/* #ifndef APP */
|
.empty-content { padding: 100rpx 40rpx; align-items: center; }
|
||||||
display: block;
|
.empty-title { color: #172640; font-size: 32rpx; font-weight: 700; }
|
||||||
/* #endif */
|
.empty-copy { color: #66758a; font-size: 26rpx; margin-top: 20rpx; }
|
||||||
color: #59677c; font-size: 25rpx; line-height: 46rpx; margin-top: 20rpx; }
|
.empty-button { width: 100%; margin-top: 38rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,120 +1,201 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page page-white" :style="pageStyle()">
|
<view class="page page-white" :style="pageStyle()">
|
||||||
<AppHeader :title="titles[step]" />
|
<AppHeader :title="titles[step]" :customBack="true" @back="back" />
|
||||||
<view class="assessment-content" :style="contentStyle(96, true)">
|
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||||
<view class="step-line"><view v-for="i in 4" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view>
|
<view class="assessment-content">
|
||||||
<text class="step-count">{{ step + 1 }}/4</text>
|
<view class="progress-row"><view class="step-line"><view v-for="i in 5" :key="i" :class="i <= step + 1 ? 'step-segment done' : 'step-segment'"></view></view><text class="step-count">{{ step + 1 }}/5</text></view>
|
||||||
|
<view v-if="step == 0" class="type-row"><text class="type-label">融资类型</text><view class="type-switch"><text :class="assessmentType == 'personal' ? 'type-item type-active' : 'type-item'" @click="switchType('personal')">个人</text><text :class="assessmentType == 'enterprise' ? 'type-item type-active' : 'type-item'" @click="switchType('enterprise')">企业</text></view></view>
|
||||||
|
<text v-else class="section-hint">{{ assessmentType == 'enterprise' ? '企业信用融资' : '个人信用融资' }} · {{ step == 3 ? '资料与授权均为选填' : '请按实际情况填写' }}</text>
|
||||||
<view v-if="step == 0" class="form-list">
|
<view v-if="step == 0" class="form-list">
|
||||||
<view class="form-row"><text class="field-label">姓名</text><input v-model="form.name" class="field-input" placeholder="请输入姓名" placeholder-class="placeholder" /></view>
|
<view v-if="assessmentType == 'enterprise'" class="form-row"><text class="field-label">企业名称</text><input v-model="form.enterpriseName" class="field-input" placeholder="请输入企业名称" /></view>
|
||||||
<view class="form-row"><text class="field-label">身份证号</text><input v-model="form.idCard" class="field-input" placeholder="请输入身份证号" placeholder-class="placeholder" /></view>
|
<view v-if="assessmentType == 'enterprise'" class="form-row"><text class="field-label">信用代码</text><input v-model="form.businessCode" class="field-input" maxlength="18" placeholder="统一社会信用代码" /></view>
|
||||||
<view class="form-row"><text class="field-label">手机号</text><input v-model="form.phone" class="field-input" type="number" placeholder="请输入手机号" placeholder-class="placeholder" /></view>
|
<view class="form-row"><text class="field-label">{{ assessmentType == 'enterprise' ? '联系人' : '姓名' }}</text><input v-model="form.name" class="field-input" placeholder="请输入姓名" /></view>
|
||||||
<view class="form-row"><text class="field-label">所在城市</text><input v-model="form.city" class="field-input" placeholder="请选择所在城市" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<view v-if="assessmentType == 'personal'" class="form-row"><text class="field-label">身份证号</text><input v-model="form.idCard" class="field-input" maxlength="18" placeholder="请输入身份证号" /></view>
|
||||||
<view class="form-row"><text class="field-label">婚姻状况</text><input v-model="form.maritalStatus" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<view class="form-row"><text class="field-label">手机号</text><input v-model="form.phone" class="field-input" type="number" maxlength="11" placeholder="请输入手机号" /></view>
|
||||||
|
<AssessmentSelect label="所在城市" :modelValue="form.city" @update:modelValue="cityChanged" :options="['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市']" />
|
||||||
|
<view v-if="form.city == '其他城市' || customCity.length > 0" class="form-row"><text class="field-label">城市名称</text><input v-model="customCity" class="field-input" placeholder="请输入所在城市" /></view>
|
||||||
|
<AssessmentSelect v-if="assessmentType == 'personal'" label="婚姻状况" v-model="form.maritalStatus" :options="['未婚', '已婚', '离异', '丧偶']" />
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="step == 1" class="form-list">
|
<view v-else-if="step == 1" class="form-list">
|
||||||
<view class="form-row"><text class="field-label">职业类型</text><input v-model="form.occupation" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<template v-if="assessmentType == 'enterprise'">
|
||||||
<view class="form-row"><text class="field-label">工作单位</text><input v-model="form.company" class="field-input" placeholder="请输入工作单位" placeholder-class="placeholder" /></view>
|
<AssessmentSelect label="经营行业" v-model="form.occupation" :options="['商贸零售', '制造业', '科技服务', '餐饮住宿', '专业服务', '其他行业']" />
|
||||||
<view class="form-row"><text class="field-label">月收入(元)</text><input v-model="form.monthlyIncome" class="field-input" type="number" placeholder="请输入月收入" placeholder-class="placeholder" /></view>
|
<AssessmentSelect label="经营年限" v-model="form.operatingYears" :options="['不足 1 年', '1–3 年', '3–5 年', '5 年以上']" />
|
||||||
<view class="form-row"><text class="field-label">社保缴纳</text><input v-model="form.socialSecurity" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<view class="form-row"><text class="field-label">年营收(万元)</text><input v-model="form.annualRevenue" class="field-input" type="digit" placeholder="请输入年营业收入" /></view>
|
||||||
<view class="form-row"><text class="field-label">公积金缴纳</text><input v-model="form.housingFund" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AssessmentSelect label="职业类型" v-model="form.occupation" :options="['企业职员', '事业单位', '公务员', '个体经营', '自由职业', '其他']" />
|
||||||
|
<view class="form-row"><text class="field-label">工作单位</text><input v-model="form.company" class="field-input" placeholder="请输入工作单位" /></view>
|
||||||
|
<view class="form-row"><text class="field-label">月收入(元)</text><input v-model="form.monthlyIncome" class="field-input" type="digit" placeholder="请输入月收入" /></view>
|
||||||
|
<AssessmentSelect label="社保缴纳" v-model="form.socialSecurity" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||||
|
<AssessmentSelect label="公积金缴纳" v-model="form.housingFund" :options="['连续缴纳 6 个月以上', '缴纳不足 6 个月', '未缴纳']" />
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
<view v-else-if="step == 2" class="form-list">
|
<view v-else-if="step == 2" class="form-list">
|
||||||
<view class="form-row"><text class="field-label">房产情况</text><input v-model="form.house" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<AssessmentSelect label="房产情况" v-model="form.house" :options="['无房产', '全款房产', '按揭房产']" />
|
||||||
<view class="form-row"><text class="field-label">车辆情况</text><input v-model="form.car" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<AssessmentSelect label="车辆情况" v-model="form.car" :options="['无车辆', '全款车辆', '按揭车辆']" />
|
||||||
<view class="form-row"><text class="field-label">存款情况</text><input v-model="form.savings" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<AssessmentSelect label="存款情况" v-model="form.savings" :options="['5 万元以下', '5–20 万元', '20–50 万元', '50 万元以上']" />
|
||||||
<view class="form-row"><text class="field-label">信用卡使用情况</text><input v-model="form.creditCard" class="field-input" placeholder="请选择" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<AssessmentSelect label="信用卡使用" v-model="form.creditCard" :options="['无信用卡', '正常使用,无逾期', '当前存在逾期', '历史有逾期,已结清']" />
|
||||||
<view class="form-row"><text class="field-label">其他资产</text><input v-model="form.otherAssets" class="field-input" placeholder="选填" placeholder-class="placeholder" /><text class="field-arrow">⌄</text></view>
|
<view class="form-row"><text class="field-label">其他资产</text><input v-model="form.otherAssets" class="field-input" placeholder="选填" /></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="upload-area">
|
<view v-else class="upload-area">
|
||||||
<text class="upload-title">上传资料(选填)</text>
|
<text class="upload-title">资料选择 <text class="optional">(选填)</text></text>
|
||||||
<view class="upload-grid"><view class="upload-card" v-for="item in uploads" :key="item" @click="chooseImage(item)" :class="item == uploads[uploads.length - 1] ? 'upload-last' : ''"><view class="upload-icon"><AppIcon name="upload" :size="38" /></view><text>{{ item }}</text><text class="upload-state">{{ uploaded[item] == true ? '已选择' : '点击上传' }}</text></view></view>
|
<text class="upload-desc">支持照片,仅在本机预览,尚未上传。</text>
|
||||||
<text class="upload-title other-title">其他资料</text>
|
<view class="upload-grid"><view class="upload-card" v-for="(item, index) in uploadNames" :key="item" :class="index == 2 ? 'upload-last' : ''" @click="documentAction(item)"><image v-if="documentPath(item).length > 0" class="document-thumb" :src="documentPath(item)" mode="aspectFill" @error="documentExpired(item)" /><AppIcon v-else name="upload" :size="52" /><text class="upload-name">{{ item }}</text><text :class="documentPath(item).length > 0 ? 'upload-state selected-state' : 'upload-state'">{{ documentPath(item).length > 0 ? '已选择 · 管理' : '点击选择' }}</text></view></view>
|
||||||
<view class="upload-grid"><view class="upload-card small-card" v-for="item in otherUploads" :key="item" @click="chooseImage(item)" :class="item == otherUploads[otherUploads.length - 1] ? 'upload-last' : ''"><view class="file-icon"><AppIcon name="file-text" :size="30" /></view><text>{{ item }}</text></view></view>
|
<view class="consent"><view class="check-hit" @click="documentConsent = !documentConsent"><view :class="documentConsent ? 'checkbox checked' : 'checkbox'"><text v-if="documentConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('资料使用协议')">《资料使用协议》</text></text></view>
|
||||||
<view class="consent" @click="consented = !consented"><view :class="consented ? 'checkbox checked' : 'checkbox'"><text v-if="consented">✓</text></view><text>我已阅读并同意《授权协议》</text></view>
|
<view class="data-section"><text class="upload-title">数据授权 <text class="optional">(可选)</text></text><text class="upload-desc">自主选择授权项目,可随时取消选择。</text><view class="grant-list"><view v-for="grant in authorizations" :key="grant.key" :class="grant.selected ? 'grant-card grant-selected' : 'grant-card'" @click="grant.selected = !grant.selected"><text :class="grant.selected ? 'grant-name grant-name-active' : 'grant-name'">{{ grant.name }}</text><view :class="grant.selected ? 'checkbox checked' : 'checkbox'"><text v-if="grant.selected" class="check-mark">✓</text></view></view></view><view class="consent"><view class="check-hit" @click="dataConsent = !dataConsent"><view :class="dataConsent ? 'checkbox checked' : 'checkbox'"><text v-if="dataConsent" class="check-mark">✓</text></view></view><text class="consent-copy">我已阅读并同意<text class="consent-link" @click="showAgreement('数据授权协议')">《数据授权协议》</text></text></view><text class="local-note">当前仅保存选择意向,不查询外部数据。</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" @click="next">{{ step == 3 ? '确认信息' : '继续' }}</button></view>
|
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="bottom-action" :style="bottomStyle(20)"><button v-if="step > 0" class="back-button" @click="previous">上一步</button><button class="primary-button next-button" @click="next">{{ step == 3 ? '确认信息' : '下一步' }}</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
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 AssessmentSelect from '@/components/AssessmentSelect.uvue'
|
||||||
|
import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice } from '@/services/assessment.uts'
|
||||||
|
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||||
|
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
||||||
const step = ref(0)
|
const step = ref(0)
|
||||||
const assessmentType = ref('personal')
|
const assessmentType = ref('personal')
|
||||||
const consented = ref(true)
|
const form = ref<AssessmentForm>(emptyAssessmentForm())
|
||||||
|
const customCity = ref('')
|
||||||
|
const cityChanged = (city: string) => { form.value.city = city; if (city != '其他城市') customCity.value = '' }
|
||||||
|
const documentConsent = ref(false)
|
||||||
|
const dataConsent = ref(false)
|
||||||
|
const documents = ref<SelectedDocument[]>([])
|
||||||
|
const authorizations = ref<DataAuthorization[]>([{ key: 'credit', name: '征信查询', selected: false }, { key: 'business', name: '经营数据', selected: false }, { key: 'fund', name: '公积金数据', selected: false }])
|
||||||
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
|
const titles = ['基本信息', '经营 / 收入信息', '资产 / 负债信息', '资料上传 / 授权']
|
||||||
const uploads = ['身份证正面', '银行卡', '收入证明']
|
const uploadNames = computed((): string[] => assessmentType.value == 'enterprise' ? ['营业执照', '银行流水', '经营证明'] : ['身份证正面', '银行卡', '收入证明'])
|
||||||
const otherUploads = ['房产证', '营业执照', '其他文件']
|
let initialized = false
|
||||||
type AssessmentForm = { name: string, idCard: string, phone: string, city: string, maritalStatus: string, occupation: string, company: string, monthlyIncome: string, socialSecurity: string, housingFund: string, house: string, car: string, savings: string, creditCard: string, otherAssets: string }
|
const saveDraft = () => {
|
||||||
const form = reactive<AssessmentForm>({ name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '' })
|
if (!initialized) return
|
||||||
const uploaded = reactive({ '身份证正面': false, '银行卡': false, '收入证明': false, '房产证': false, '营业执照': false, '其他文件': false })
|
const source = form.value
|
||||||
const saveDraft = () => { uni.setStorageSync('assessmentDraft', { type: assessmentType.value, name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus, occupation: form.occupation, company: form.company, monthlyIncome: form.monthlyIncome, socialSecurity: form.socialSecurity, housingFund: form.housingFund, house: form.house, car: form.car, savings: form.savings, creditCard: form.creditCard, otherAssets: form.otherAssets, consented: consented.value, uploaded: uploaded }) }
|
const draftForm: AssessmentForm = { name: source.name, idCard: source.idCard, phone: source.phone, city: source.city, maritalStatus: source.maritalStatus, occupation: source.occupation, company: source.company, monthlyIncome: source.monthlyIncome, socialSecurity: source.socialSecurity, housingFund: source.housingFund, house: source.house, car: source.car, savings: source.savings, creditCard: source.creditCard, otherAssets: source.otherAssets, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears }
|
||||||
const fillDraft = (saved: UTSJSONObject) => {
|
if (draftForm.city == '其他城市' && customCity.value.trim().length > 0) draftForm.city = customCity.value.trim()
|
||||||
const read = (key: string): string => { const value = saved[key]; return value == null ? '' : value as string }
|
saveAssessmentDraft(assessmentType.value, step.value, draftForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value)
|
||||||
form.name = read('name'); form.idCard = read('idCard'); form.phone = read('phone'); form.city = read('city'); form.maritalStatus = read('maritalStatus'); form.occupation = read('occupation'); form.company = read('company'); form.monthlyIncome = read('monthlyIncome'); form.socialSecurity = read('socialSecurity'); form.housingFund = read('housingFund'); form.house = read('house'); form.car = read('car'); form.savings = read('savings'); form.creditCard = read('creditCard'); form.otherAssets = read('otherAssets')
|
|
||||||
const savedConsent = saved['consented']; if (savedConsent != null) consented.value = savedConsent as boolean
|
|
||||||
}
|
}
|
||||||
|
const restore = (type: string) => {
|
||||||
|
assessmentType.value = type; step.value = 0; form.value = emptyAssessmentForm(); customCity.value = ''; documents.value = []; documentConsent.value = false; dataConsent.value = false
|
||||||
|
authorizations.value.forEach((item) => { item.selected = false })
|
||||||
|
const saved = loadAssessmentDraft(type)
|
||||||
|
if (saved != null) {
|
||||||
|
form.value = formFromDraft(saved); documents.value = documentsFromDraft(saved)
|
||||||
|
const savedStep = saved['step']; if (typeof savedStep == 'number') step.value = Math.max(0, Math.min(3, savedStep as number))
|
||||||
|
documentConsent.value = saved['documentConsent'] == true; dataConsent.value = saved['dataConsent'] == true
|
||||||
|
const grants = saved['authorizations'] as string[] | null
|
||||||
|
if (grants != null) authorizations.value.forEach((item) => { item.selected = grants.includes(item.key) })
|
||||||
|
if (form.value.city.length > 0 && !['深圳市', '广州市', '北京市', '上海市', '杭州市', '其他城市'].includes(form.value.city)) { customCity.value = form.value.city; form.value.city = '其他城市' }
|
||||||
|
}
|
||||||
|
initialized = true
|
||||||
|
saveDraft()
|
||||||
|
}
|
||||||
|
const switchType = (type: string) => { if (assessmentType.value == type) return; saveDraft(); restore(type); step.value = 0; saveDraft() }
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
const routeType = options != null && options['type'] == 'enterprise' ? 'enterprise' : ''
|
let type = activeAssessmentType()
|
||||||
const storedDraft = uni.getStorageSync('assessmentDraft')
|
if (options != null && (options['type'] == 'enterprise' || options['type'] == 'personal')) type = options['type'] as string
|
||||||
if (routeType.length > 0) assessmentType.value = routeType
|
restore(type)
|
||||||
if (storedDraft != null && typeof storedDraft == 'object') {
|
|
||||||
const saved = storedDraft as UTSJSONObject
|
|
||||||
const savedType = saved['type'] == null ? '' : saved['type'] as string
|
|
||||||
if (savedType.length > 0 && (routeType.length == 0 || savedType == routeType)) fillDraft(saved)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
const requestedStep = uni.getStorageSync('assessmentEditStep')
|
const requestedStep = uni.getStorageSync('assessmentEditStep')
|
||||||
if (typeof requestedStep == 'number') {
|
if (typeof requestedStep == 'number') { step.value = Math.max(0, Math.min(3, requestedStep as number)); uni.removeStorageSync('assessmentEditStep'); saveDraft() }
|
||||||
const targetStep = requestedStep as number
|
|
||||||
if (targetStep >= 0 && targetStep < 4) step.value = targetStep
|
|
||||||
uni.removeStorageSync('assessmentEditStep')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const chooseImage = (name: string) => { uploaded[name] = true; saveDraft(); uni.showToast({ title: name + '上传入口已预留', icon: 'none' }) }
|
onHide(() => saveDraft())
|
||||||
const previous = () => { saveDraft(); if (step.value > 0) step.value-- }
|
onUnload(() => saveDraft())
|
||||||
|
// 系统返回与顶部返回均先保存;页内返回优先回到上一步。
|
||||||
|
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 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 documentPath = (name: string): string => { const item = documents.value.find((file) => file.name == name); return item == null ? '' : item.path }
|
||||||
|
const documentExpired = (name: string) => {
|
||||||
|
documents.value = documents.value.filter((file) => file.name != name); saveDraft()
|
||||||
|
uni.showToast({ title: name + '已失效,请重新选择', icon: 'none' })
|
||||||
|
}
|
||||||
|
const chooseDocument = (name: string) => {
|
||||||
|
uni.chooseImage({ count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'], success: (result) => {
|
||||||
|
if (result.tempFilePaths.length == 0) return
|
||||||
|
documents.value = documents.value.filter((file) => file.name != name)
|
||||||
|
const document: SelectedDocument = { name, path: result.tempFilePaths[0] }
|
||||||
|
documents.value.push(document); saveDraft()
|
||||||
|
}, fail: (error) => { if (!error.errMsg.includes('cancel')) uni.showToast({ title: '选择失败,请重试或检查相册权限', icon: 'none' }) } })
|
||||||
|
}
|
||||||
|
const documentAction = (name: string) => {
|
||||||
|
const path = documentPath(name)
|
||||||
|
if (path.length == 0) { chooseDocument(name); return }
|
||||||
|
uni.showActionSheet({ itemList: ['预览图片', '重新选择', '移除资料'], success: (result) => {
|
||||||
|
if (result.tapIndex == 0) uni.previewImage({ urls: [path], fail: () => { uni.showToast({ title: '本地图片已失效,请重新选择', icon: 'none' }) } })
|
||||||
|
else if (result.tapIndex == 1) chooseDocument(name)
|
||||||
|
else if (result.tapIndex == 2) { documents.value = documents.value.filter((file) => file.name != name); saveDraft() }
|
||||||
|
} })
|
||||||
|
}
|
||||||
const validateStep = (): boolean => {
|
const validateStep = (): boolean => {
|
||||||
|
let message = ''
|
||||||
if (step.value == 0) {
|
if (step.value == 0) {
|
||||||
if (form.name.trim().length < 2) { uni.showToast({ title: '请输入正确姓名', icon: 'none' }); return false }
|
if (assessmentType.value == 'enterprise' && form.value.enterpriseName.trim().length < 2) message = '请输入企业名称'
|
||||||
if (form.idCard.trim().length < 15) { uni.showToast({ title: '请输入正确身份证号', icon: 'none' }); return false }
|
else if (assessmentType.value == 'enterprise' && !/^[0-9A-Z]{18}$/.test(form.value.businessCode.trim().toUpperCase())) message = '请输入 18 位统一社会信用代码'
|
||||||
if (form.phone.trim().length != 11 || !form.phone.startsWith('1')) { uni.showToast({ title: '请输入正确手机号', icon: 'none' }); return false }
|
else if (form.value.name.trim().length < 2) message = '请输入正确姓名'
|
||||||
if (form.city.trim().length == 0) { uni.showToast({ title: '请选择所在城市', icon: 'none' }); return false }
|
else if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(form.value.idCard.trim())) message = '请输入正确身份证号'
|
||||||
|
else if (!/^1[3-9][0-9]{9}$/.test(form.value.phone)) message = '请输入正确手机号'
|
||||||
|
else if (form.value.city.length == 0 || (form.value.city == '其他城市' && customCity.value.trim().length == 0)) message = '请选择或填写所在城市'
|
||||||
}
|
}
|
||||||
if (step.value == 1 && form.monthlyIncome.trim().length == 0) { uni.showToast({ title: '请输入月收入', icon: 'none' }); return false }
|
if (step.value == 1) {
|
||||||
return true
|
const amount = assessmentType.value == 'enterprise' ? form.value.annualRevenue : form.value.monthlyIncome
|
||||||
|
if (!/^[0-9]+([.][0-9]{1,2})?$/.test(amount) || parseFloat(amount) <= 0) message = assessmentType.value == 'enterprise' ? '请输入有效的年营业收入' : '请输入有效的月收入'
|
||||||
}
|
}
|
||||||
const next = () => { if (!validateStep()) return; saveDraft(); if (step.value < 3) step.value++; else if (!consented.value) uni.showToast({ title: '请同意授权协议', icon: 'none' }); else uni.navigateTo({ url: '/pages/confirm/confirm' }) }
|
if (step.value == 3 && documents.value.length > 0 && !documentConsent.value) message = '请阅读并同意资料使用协议,或移除资料'
|
||||||
|
if (step.value == 3 && authorizations.value.some((item) => item.selected) && !dataConsent.value) message = '请阅读并同意数据授权协议,或取消授权选择'
|
||||||
|
if (message.length == 0) return true
|
||||||
|
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' }) } }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.assessment-content { padding: 24rpx 30rpx 190rpx; position: relative; }
|
.assessment-content { padding: 26rpx 30rpx 0; }
|
||||||
.step-line { height: 8rpx; flex-direction: row; margin-right: 70rpx; }
|
.progress-row { flex-direction: row; align-items: center; }
|
||||||
|
.step-line { height: 8rpx; flex: 1; flex-direction: row; }
|
||||||
.step-segment { flex: 1; height: 8rpx; border-radius: 8rpx; background-color: #e8edf4; margin-right: 8rpx; }
|
.step-segment { flex: 1; height: 8rpx; border-radius: 8rpx; background-color: #e8edf4; margin-right: 8rpx; }
|
||||||
.step-segment.done { background-color: #0868f7; }
|
.done { background-color: #0868f7; }
|
||||||
.step-count { position: absolute; top: 16rpx; right: 30rpx; color: #7f8da2; font-size: 22rpx; }
|
.step-count { color: #758297; font-size: 24rpx; margin-left: 20rpx; }
|
||||||
.form-list { margin-top: 42rpx; }
|
.type-row { margin-top: 30rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.form-row { min-height: 104rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.type-label { font-size: 28rpx; color: #29374e; }
|
||||||
.field-label { width: 230rpx; font-size: 27rpx; color: #27354b; }
|
.type-switch { flex-direction: row; padding: 5rpx; border-radius: 14rpx; background-color: #eef4fd; }
|
||||||
.field-input { flex: 1; text-align: right; font-size: 26rpx; color: #18253d; }
|
.type-item { width: 130rpx; padding: 13rpx 0; border-radius: 10rpx; text-align: center; color: #68809f; font-size: 26rpx; }
|
||||||
.placeholder { color: #b0bac9; }
|
.type-active { color: #0868f7; font-weight: 600; background-color: #fff; }
|
||||||
.field-arrow { color: #a5b0c0; margin-left: 10rpx; }
|
.section-hint { color: #738198; font-size: 24rpx; line-height: 36rpx; margin-top: 30rpx; }
|
||||||
.upload-title { font-size: 29rpx; font-weight: 700; margin-top: 40rpx; }
|
.form-list { margin-top: 14rpx; }
|
||||||
|
.form-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
|
.field-label { font-size: 28rpx; color: #29374e; margin-right: 18rpx; flex-shrink: 0; }
|
||||||
|
.field-input { flex: 1; min-width: 0; text-align: right; font-size: 27rpx; color: #22324a; }
|
||||||
|
.upload-title { font-size: 30rpx; font-weight: 600; color: #20304a; margin-top: 28rpx; }
|
||||||
|
.optional { font-size: 24rpx; color: #748198; font-weight: 400; }
|
||||||
|
.upload-desc { font-size: 24rpx; line-height: 36rpx; color: #748198; margin-top: 10rpx; }
|
||||||
.upload-grid { flex-direction: row; margin-top: 22rpx; }
|
.upload-grid { flex-direction: row; margin-top: 22rpx; }
|
||||||
.upload-card { flex: 1; height: 210rpx; margin-right: 14rpx; border-radius: 16rpx; background-color: #f2f7fd; align-items: center; justify-content: center; color: #35445a; font-size: 22rpx; }
|
.upload-card { flex: 1; min-width: 0; height: 200rpx; margin-right: 14rpx; border-radius: 16rpx; background-color: #f1f6fd; align-items: center; justify-content: center; }
|
||||||
.upload-last { margin-right: 0; }
|
.upload-last { margin-right: 0; }
|
||||||
.upload-icon { width: 70rpx; height: 70rpx; border-radius: 18rpx; background-color: #dfeeff; align-items: center; justify-content: center; margin-bottom: 14rpx; }
|
.document-thumb { width: 74rpx; height: 62rpx; border-radius: 8rpx; }
|
||||||
.upload-state { color: #9ca8b9; font-size: 19rpx; margin-top: 8rpx; }
|
.upload-name { font-size: 25rpx; color: #304260; margin-top: 16rpx; }
|
||||||
.other-title { margin-top: 42rpx; }
|
.upload-state { color: #7a889e; font-size: 22rpx; margin-top: 9rpx; }
|
||||||
.small-card { height: 150rpx; }
|
.selected-state { color: #0868f7; }
|
||||||
.file-icon { width: 54rpx; height: 54rpx; border-radius: 14rpx; background-color: #e6f0ff; align-items: center; justify-content: center; margin-bottom: 10rpx; }
|
.consent { margin-top: 24rpx; flex-direction: row; align-items: flex-start; }
|
||||||
.consent { margin-top: 34rpx; flex-direction: row; align-items: center; color: #67758a; font-size: 22rpx; }
|
.check-hit { width: 44rpx; height: 42rpx; flex-shrink: 0; justify-content: center; }
|
||||||
.checkbox { width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid #aeb9c8; align-items: center; justify-content: center; margin-right: 12rpx; }
|
.checkbox { width: 30rpx; height: 30rpx; border-radius: 8rpx; border: 2rpx solid #9aa8ba; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
.checkbox.checked { background-color: #0868f7; border-color: #0868f7; color: white; }
|
.checked { background-color: #0868f7; border-color: #0868f7; }
|
||||||
|
.check-mark { color: #fff; font-size: 22rpx; }
|
||||||
|
.consent-copy { flex: 1; min-width: 0; color: #687991; font-size: 24rpx; line-height: 40rpx; }
|
||||||
|
.consent-link { color: #0868f7; font-size: 24rpx; line-height: 40rpx; }
|
||||||
|
.data-section { margin-top: 24rpx; padding-top: 4rpx; border-top: 1rpx solid #e9eef6; }
|
||||||
|
.grant-list { margin-top: 18rpx; }
|
||||||
|
.grant-card { min-height: 78rpx; padding: 0 22rpx; border: 1rpx solid #e3eaf4; border-radius: 14rpx; margin-top: 12rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
|
.grant-selected { border-color: #c3d9ff; background-color: #f1f6ff; }
|
||||||
|
.grant-name { font-size: 27rpx; color: #344761; }
|
||||||
|
.grant-name-active { color: #0868f7; }
|
||||||
|
.local-note { color: #8290a4; font-size: 22rpx; line-height: 34rpx; margin-top: 14rpx; }
|
||||||
|
.scroll-spacer { min-height: 160rpx; }
|
||||||
.bottom-action { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #edf1f6; flex-direction: row; }
|
.bottom-action { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx; background-color: #fff; border-top: 1rpx solid #edf1f6; flex-direction: row; }
|
||||||
.back-button { width: 180rpx; height: 92rpx; line-height: 92rpx; margin-right: 18rpx; border-radius: 16rpx; background-color: #eef3f9; color: #526075; font-size: 27rpx; }
|
.back-button { width: 180rpx; height: 92rpx; line-height: 92rpx; margin-right: 18rpx; border-radius: 16rpx; background-color: #eef3f9; color: #526075; font-size: 28rpx; }
|
||||||
.next-button { flex: 1; margin: 0; }
|
.next-button { flex: 1; margin: 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
<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>您好!我是AI融资助手,可以帮您解答融资相关问题。</text></view></view>
|
<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="quick-questions">
|
<view class="quick-questions">
|
||||||
<view class="question" v-for="item in questions" :key="item" @click="ask(item)"><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="/static/mobile/images/robot.png" mode="aspectFit"></image>
|
||||||
<view :class="item.role == 'user' ? 'message-bubble user-bubble' : 'message-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="errorMessage.length > 0" class="assistant-error"><text>{{ errorMessage }}</text><text @click="retry" class="assistant-error-text-last-child">重试</text></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="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>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="chat-input" :style="bottomStyle(17)"><input v-model="draft" confirm-type="send" @confirm="send" placeholder="请输入您的问题..." class="chat-input-input" /><button @click="send" class="chat-input-button"><AppIcon name="send" :size="34" /></button></view>
|
<view class="chat-input" :style="bottomStyle(17)"><input v-model="draft" confirm-type="send" @confirm="send" placeholder="请输入您的问题..." class="chat-input-input" /><button @click="send" :disabled="sending || draft.trim().length == 0" :class="sending || draft.trim().length == 0 ? 'chat-input-button send-disabled' : 'chat-input-button'"><AppIcon name="send" :size="40" /></button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,25 +27,37 @@
|
|||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
import { sendAssistantMessage } from '@/services/api.uts'
|
import { sendAssistantMessage } from '@/services/api.uts'
|
||||||
const questions = ['我的融资额度受哪些因素影响?', '如何提高贷款通过率?', '企业融资需要哪些材料?', '有哪些适合我的方案?']
|
const questions = ['我的融资额度受哪些因素影响?', '如何提高贷款通过率?', '企业融资需要哪些材料?', '有哪些适合我的方案?']
|
||||||
const messages = ref<UTSJSONObject[]>([])
|
type ChatMessage = { role: string, text: string }
|
||||||
|
const messages = ref<ChatMessage[]>([])
|
||||||
const draft = ref('')
|
const draft = ref('')
|
||||||
const lastMessageId = ref('')
|
const lastMessageId = ref('')
|
||||||
const lastQuestion = ref('')
|
const lastQuestion = ref('')
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const send = async () => {
|
const scrollToEnd = async () => {
|
||||||
|
lastMessageId.value = ''
|
||||||
|
await nextTick()
|
||||||
|
lastMessageId.value = 'chat-end'
|
||||||
|
}
|
||||||
|
const requestAnswer = async (text: string) => {
|
||||||
|
sending.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
scrollToEnd()
|
||||||
|
try { const answer = await sendAssistantMessage(text); const response: ChatMessage = { role: 'assistant', text: answer }; messages.value.push(response) } catch (error) { errorMessage.value = '暂未收到回复,请重试' }
|
||||||
|
sending.value = false
|
||||||
|
scrollToEnd()
|
||||||
|
}
|
||||||
|
const send = () => {
|
||||||
const text = draft.value.trim()
|
const text = draft.value.trim()
|
||||||
if (text.length == 0 || sending.value) return
|
if (text.length == 0 || sending.value) return
|
||||||
lastQuestion.value = text
|
lastQuestion.value = text
|
||||||
messages.value.push({ role: 'user', text: text })
|
const message: ChatMessage = { role: 'user', text: text }
|
||||||
|
messages.value.push(message)
|
||||||
draft.value = ''
|
draft.value = ''
|
||||||
sending.value = true
|
requestAnswer(text)
|
||||||
errorMessage.value = ''
|
|
||||||
try { const answer = await sendAssistantMessage(text); messages.value.push({ role: 'assistant', text: answer }); lastMessageId.value = 'message-' + (messages.value.length - 1) } catch (error) { errorMessage.value = '消息发送失败,请稍后重试' }
|
|
||||||
sending.value = false
|
|
||||||
}
|
}
|
||||||
const ask = (text: string) => { draft.value = text; send() }
|
const ask = (text: string) => { if (sending.value) return; draft.value = text; send() }
|
||||||
const retry = () => { draft.value = lastQuestion.value; errorMessage.value = ''; send() }
|
const retry = () => { if (!sending.value && lastQuestion.value.length > 0) requestAnswer(lastQuestion.value) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -61,7 +75,18 @@
|
|||||||
.user-bubble { color: #fff; background-color: #0868f7; border-color: #0868f7; border-radius: 22rpx 8rpx 22rpx 22rpx; }
|
.user-bubble { color: #fff; background-color: #0868f7; border-color: #0868f7; border-radius: 22rpx 8rpx 22rpx 22rpx; }
|
||||||
.chat-input { position: fixed; left: 0; right: 0; bottom: 0; min-height: 114rpx; padding: 17rpx 24rpx; background-color: #fff; border-top: 1rpx solid #e3eaf4; flex-direction: row; align-items: center; }
|
.chat-input { position: fixed; left: 0; right: 0; bottom: 0; min-height: 114rpx; padding: 17rpx 24rpx; background-color: #fff; border-top: 1rpx solid #e3eaf4; flex-direction: row; align-items: center; }
|
||||||
.chat-input-input { flex: 1; height: 78rpx; border-radius: 39rpx; background-color: #f2f5f9; padding: 0 28rpx; font-size: 25rpx; }
|
.chat-input-input { flex: 1; height: 78rpx; border-radius: 39rpx; background-color: #f2f5f9; padding: 0 28rpx; font-size: 25rpx; }
|
||||||
.chat-input-button { width: 76rpx; height: 76rpx; line-height: 76rpx; border-radius: 50%; margin-left: 15rpx; padding: 0; background-color: #0868f7; color: #fff; font-size: 29rpx; }
|
.chat-input-button { width: 76rpx; height: 76rpx; line-height: 76rpx; border-radius: 50%; margin-left: 15rpx; padding: 0; background-color: #0868f7; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||||
|
.send-disabled { opacity: .55; }
|
||||||
|
/* #ifndef APP */
|
||||||
|
.chat-input-button { display: flex; }
|
||||||
|
/* #endif */
|
||||||
|
.bubble-text { font-size: 27rpx; line-height: 43rpx; color: #344259; }
|
||||||
|
.user-text { color: #ffffff; }
|
||||||
|
.question-text { flex: 1; margin-right: 12rpx; color: #0868f7; font-size: 24rpx; line-height: 38rpx; }
|
||||||
|
.sending-text { font-size: 25rpx; line-height: 40rpx; color: #75869d; }
|
||||||
|
.error-text { flex: 1; color: #bb4857; font-size: 23rpx; line-height: 36rpx; }
|
||||||
|
.retry-text { color: #0868f7; font-size: 25rpx; padding: 0 6rpx; }
|
||||||
|
.chat-end { height: 12rpx; }
|
||||||
.assistant-error { margin: 18rpx 0 0 94rpx; color: #e05a5a; font-size: 21rpx; flex-direction: row; justify-content: space-between; }
|
.assistant-error { margin: 18rpx 0 0 94rpx; color: #e05a5a; font-size: 21rpx; flex-direction: row; justify-content: space-between; }
|
||||||
.assistant-error-text-last-child { color: #0868f7; margin-left: 18rpx; }
|
.assistant-error-text-last-child { color: #0868f7; margin-left: 18rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="融资课堂" />
|
<AppHeader title="融资课堂" />
|
||||||
<scroll-view scroll-y class="classroom-scroll" :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-title">融资课堂</text><text class="hero-subtitle">把融资知识,变成申请底气</text><text class="hero-desc">从准备材料到优化信用,分阶段掌握实用方法</text></view><image src="/static/mobile/images/news-policy.png" mode="aspectFill" class="classroom-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="/static/mobile/images/news-policy.png" mode="aspectFill" class="hero-image"></image></view>
|
||||||
<view class="lesson-section"><view class="section-head"><text class="section-title">热门课程</text><text class="lesson-count">12 节课程</text></view><view class="lesson-card" v-for="(item,index) in lessons" :key="item.title" @click="openLesson(index)"><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>{{ item.duration }}</text><text class="lesson-tag">{{ item.tag }}</text></view></view><AppIcon name="chevron-right" :size="28" muted /></view></view>
|
<view class="topic-section">
|
||||||
<view class="topic-section"><text class="section-title">按场景学习</text><view class="topic-grid"><view class="topic-card blue-topic" @click="openTopic('个人信用提升')"><AppIcon name="user" :size="46" /><text class="topic-card-text-1">个人信用提升</text><text class="topic-card-text-2">3 节课程</text></view><view class="topic-card orange-topic" @click="openTopic('小微经营融资')"><AppIcon name="building" :size="46" /><text class="topic-card-text-1">小微经营融资</text><text class="topic-card-text-2">4 节课程</text></view><view class="topic-card green-topic" @click="openTopic('材料准备清单')"><AppIcon name="file-text" :size="46" /><text class="topic-card-text-1">材料准备清单</text><text class="topic-card-text-2">5 节课程</text></view></view></view>
|
<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>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
@@ -13,45 +28,53 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
import { financeLessons, FinanceLesson } from '@/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'
|
||||||
type LessonSummary = { title: string, desc: string, duration: string, tag: string, image: string }
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
const lessons: LessonSummary[] = [
|
type LessonTopic = { id: string, title: string, desc: string, icon: string }
|
||||||
{ title: '如何提高个人贷款通过率', desc: '征信、负债和收入的申请准备方法', duration: '6 分钟', tag: '个人融资', image: '/static/mobile/images/news-calculator.png' },
|
const topics: LessonTopic[] = [
|
||||||
{ title: '小微企业融资材料清单', desc: '一次准备齐全,减少补充资料时间', duration: '8 分钟', tag: '企业融资', image: '/static/mobile/images/news-business.png' },
|
{ id: 'credit', title: '个人信用提升', desc: '了解信用记录与还款能力', icon: 'user' },
|
||||||
{ title: '读懂贷款利率与还款方式', desc: '选对期限,降低长期资金成本', duration: '5 分钟', tag: '基础知识', image: '/static/mobile/images/news-city.png' }
|
{ id: 'business', title: '小微经营融资', desc: '梳理经营材料与融资成本', icon: 'building' },
|
||||||
|
{ id: 'documents', title: '材料准备清单', desc: '按申请场景,逐项检查资料', icon: 'file-text' }
|
||||||
]
|
]
|
||||||
const openLesson = (index: number) => { uni.navigateTo({ url: '/pages/lesson/lesson?id=' + index }) }
|
const activeTopic = ref('')
|
||||||
const openTopic = (title: string) => { uni.navigateTo({ url: '/pages/lesson/lesson?title=' + title }) }
|
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: '/pages/lesson/lesson?id=' + id }) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.classroom-scroll { }
|
.classroom-content { padding: 24rpx 28rpx 48rpx; }
|
||||||
.classroom-content { padding: 26rpx 28rpx 70rpx; }
|
.classroom-hero { border-radius: 20rpx; background-color: #e8f2ff; flex-direction: row; overflow: hidden; align-items: center; }
|
||||||
.classroom-hero { height: 242rpx; border-radius: 20rpx; background: linear-gradient(135deg, #0b2a66, #087aff); padding: 28rpx; overflow: hidden; position: relative; }
|
.hero-copy { flex: 1; padding: 28rpx 12rpx 28rpx 26rpx; }
|
||||||
.hero-copy { width: 68%; z-index: 2; }
|
.hero-eyebrow { color: #0868f7; font-size: 22rpx; font-weight: 600; }
|
||||||
.hero-title { color: #fff; font-size: 38rpx; line-height: 50rpx; font-weight: 800; }
|
.hero-title { color: #14335f; font-size: 37rpx; line-height: 51rpx; font-weight: 700; margin-top: 10rpx; }
|
||||||
.hero-subtitle { color: #fff; font-size: 27rpx; line-height: 40rpx; font-weight: 700; margin-top: 10rpx; }
|
.hero-title-next { margin-top: 0; }
|
||||||
.hero-desc { color: #bfd7ff; font-size: 21rpx; line-height: 32rpx; margin-top: 12rpx; }
|
.hero-desc { color: #4b6586; font-size: 23rpx; line-height: 36rpx; margin-top: 14rpx; }
|
||||||
.classroom-hero-image { position: absolute; right: -18rpx; top: 0; width: 220rpx; height: 242rpx; opacity: .78; }
|
.hero-image { width: 230rpx; height: 280rpx; flex-shrink: 0; }
|
||||||
.lesson-section { margin-top: 30rpx; }
|
.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; }
|
.section-head { flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.lesson-count { color: #8b98aa; font-size: 21rpx; }
|
.all-lessons { padding: 12rpx 0 12rpx 20rpx; }
|
||||||
.lesson-card { min-height: 150rpx; margin-top: 16rpx; padding: 18rpx; border-radius: 16rpx; background-color: #fff; border: 1rpx solid #e4ebf5; flex-direction: row; align-items: center; }
|
.lesson-count { color: #0868f7; font-size: 23rpx; }
|
||||||
.lesson-card-image { width: 142rpx; height: 114rpx; border-radius: 12rpx; margin-right: 18rpx; flex-shrink: 0; }
|
.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-copy { flex: 1; }
|
||||||
.lesson-title { color: #1a2941; font-size: 26rpx; line-height: 36rpx; font-weight: 700; }
|
.lesson-title { color: #172640; font-size: 30rpx; line-height: 42rpx; font-weight: 600; }
|
||||||
.lesson-desc { color: #778499; font-size: 21rpx; line-height: 31rpx; margin-top: 7rpx; }
|
.lesson-desc { color: #66758a; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
|
||||||
.lesson-meta { flex-direction: row; align-items: center; margin-top: 10rpx; color: #99a5b4; font-size: 18rpx; }
|
.lesson-meta { flex-direction: row; align-items: center; margin-top: 12rpx; }
|
||||||
.lesson-tag { margin-left: 14rpx; color: #0868f7; }
|
.lesson-duration { color: #66758a; font-size: 22rpx; }
|
||||||
.topic-section { margin-top: 32rpx; }
|
.lesson-tag { margin-left: 16rpx; color: #0868f7; font-size: 22rpx; }
|
||||||
.topic-grid { flex-direction: row; flex-wrap: wrap; justify-content: space-between; margin-top: 16rpx; }
|
|
||||||
.topic-card { width: 48.5%; min-height: 150rpx; margin-bottom: 14rpx; padding: 20rpx; border-radius: 16rpx; border: 1rpx solid #e4ebf5; }
|
|
||||||
.topic-card .app-icon { margin-bottom: 9rpx; }
|
|
||||||
.topic-card-text-1 { color: #23324a; font-size: 23rpx; font-weight: 700; }
|
|
||||||
.topic-card-text-2 { color: #8190a4; font-size: 19rpx; margin-top: 6rpx; }
|
|
||||||
.blue-topic { background-color: #eef6ff; }
|
|
||||||
.orange-topic { background-color: #fff3e9; }
|
|
||||||
.green-topic { background-color: #eaf8f1; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+100
-29
@@ -1,50 +1,121 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page page-white" :style="pageStyle()">
|
<view class="page page-white" :style="pageStyle()">
|
||||||
<AppHeader title="确认信息" />
|
<AppHeader title="确认信息" />
|
||||||
|
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||||
<view class="confirm-content">
|
<view class="confirm-content">
|
||||||
<view class="confirm-tip"><view class="tip-icon"><text>✓</text></view><view><text class="tip-title">信息填写完成</text><text class="tip-desc">请确认以下信息准确无误</text></view></view>
|
<view class="confirm-progress"><view class="progress-line"><view class="progress-segment" v-for="i in 5" :key="i"></view></view><text class="progress-count">5/5</text></view>
|
||||||
<view class="info-section" v-for="(section, sectionIndex) in sections" :key="section.title"><view class="section-bar"><text>{{ section.title }}</text><text class="edit" @click="edit(sectionIndex)">编辑</text></view><view class="info-row" v-for="row in section.rows" :key="row.label"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view></view>
|
<view class="confirm-tip"><view class="tip-icon"><text class="tip-mark">✓</text></view><view class="tip-copy"><text class="tip-title">{{ assessmentType == 'enterprise' ? '企业融资信息' : '个人融资信息' }}</text><text class="tip-desc">请确认后再开始评估,可随时返回修改。</text></view></view>
|
||||||
<button class="primary-button submit-button" :disabled="submitting" @click="submit">{{ submitting ? '提交中...' : '确认提交' }}</button>
|
<text v-if="!hasDraft" class="empty-hint">还没有评估信息,请先完成信息填写。</text>
|
||||||
|
<view class="info-section" v-for="section in sections" :key="section.title"><view class="section-bar"><text class="section-name">{{ section.title }}</text><text class="edit" @click="edit(section.step)">编辑</text></view><template v-for="(row, index) in section.rows" :key="row.label"><view v-if="index < 3 || section.expanded" class="info-row"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view></template><text v-if="section.rows.length > 3" class="expand-link" @click="section.expanded = !section.expanded">{{ section.expanded ? '收起详情' : '查看其余信息' }}</text></view>
|
||||||
|
<text v-if="isDemo" class="demo-note">本次为演示评估,不会发起真实融资申请。</text>
|
||||||
|
<view class="scroll-spacer" :style="bottomStyle(24)"></view>
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="submit-bar" :style="bottomStyle(20)"><button class="primary-button submit-button" :disabled="submitting" @click="submit">{{ !hasDraft ? '去填写信息' : (submitting ? '提交中…' : '确认并开始评估') }}</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { pageStyle } = usePageLayout()
|
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
import { submitAssessment } from '@/services/api.uts'
|
import { API_BASE_URL, submitAssessment } from '@/services/api.uts'
|
||||||
const submitting = ref(false)
|
import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft } from '@/services/assessment.uts'
|
||||||
|
const { pageStyle, contentStyle, bottomStyle } = usePageLayout()
|
||||||
type InfoRow = { label: string, value: string }
|
type InfoRow = { label: string, value: string }
|
||||||
type InfoSection = { title: string, rows: InfoRow[] }
|
type InfoSection = { title: string, step: number, rows: InfoRow[], expanded: boolean }
|
||||||
const sections = ref<InfoSection[]>([{ title: '基本信息', rows: [{ label: '姓名', value: '未填写' }, { label: '身份证号', value: '未填写' }, { label: '手机号', value: '未填写' }, { label: '所在城市', value: '未填写' }, { label: '婚姻状况', value: '未填写' }] }, { title: '经营 / 收入', rows: [{ label: '职业', value: '未填写' }, { label: '工作单位', value: '未填写' }, { label: '月收入', value: '未填写' }, { label: '社保 / 公积金', value: '未填写' }] }, { title: '资产 / 负债', rows: [{ label: '房产情况', value: '未填写' }, { label: '车辆情况', value: '未填写' }, { label: '存款情况', value: '未填写' }, { label: '信用卡', value: '未填写' }, { label: '其他资产', value: '未填写' }] }])
|
const submitting = ref(false)
|
||||||
|
const hasDraft = ref(false)
|
||||||
|
const isDemo = API_BASE_URL.length == 0
|
||||||
|
const assessmentType = ref('personal')
|
||||||
|
const sections = ref<InfoSection[]>([])
|
||||||
const payload = ref<UTSJSONObject>({ source: 'mobile', confirmed: false })
|
const payload = ref<UTSJSONObject>({ source: 'mobile', confirmed: false })
|
||||||
const mask = (value: string, left: number, right: number): string => { if (value.length <= left + right) return value; return value.substring(0, left) + '****' + value.substring(value.length - right) }
|
const mask = (value: string, left: number, right: number): string => { if (value.length <= left + right) return value; return value.substring(0, left) + '****' + value.substring(value.length - right) }
|
||||||
const read = (saved: UTSJSONObject, key: string): string => { const value = saved[key]; return value == null ? '' : value as string }
|
const displayValue = (saved: UTSJSONObject, key: string): string => { const value = draftText(saved, key); return value.length > 0 ? value : '未填写' }
|
||||||
|
const appendRow = (rows: InfoRow[], label: string, value: string) => { const row: InfoRow = { label, value }; rows.push(row) }
|
||||||
const loadDraft = () => {
|
const loadDraft = () => {
|
||||||
const storedDraft = uni.getStorageSync('assessmentDraft')
|
assessmentType.value = activeAssessmentType()
|
||||||
if (storedDraft == null || typeof storedDraft != 'object') return
|
const saved = loadAssessmentDraft(assessmentType.value)
|
||||||
const saved = storedDraft as UTSJSONObject
|
if (saved == null) { hasDraft.value = false; sections.value = []; return }
|
||||||
const name = read(saved, 'name'); const idCard = read(saved, 'idCard'); const phone = read(saved, 'phone'); const city = read(saved, 'city'); const marital = read(saved, 'maritalStatus'); const occupation = read(saved, 'occupation'); const company = read(saved, 'company'); const income = read(saved, 'monthlyIncome'); const social = read(saved, 'socialSecurity'); const fund = read(saved, 'housingFund'); const house = read(saved, 'house'); const car = read(saved, 'car'); const savings = read(saved, 'savings'); const card = read(saved, 'creditCard'); const other = read(saved, 'otherAssets')
|
hasDraft.value = true
|
||||||
sections.value = [{ title: '基本信息', rows: [{ label: '姓名', value: name.length > 0 ? name : '未填写' }, { label: '身份证号', value: idCard.length > 0 ? mask(idCard, 4, 4) : '未填写' }, { label: '手机号', value: phone.length > 0 ? mask(phone, 3, 4) : '未填写' }, { label: '所在城市', value: city.length > 0 ? city : '未填写' }, { label: '婚姻状况', value: marital.length > 0 ? marital : '未填写' }] }, { title: '经营 / 收入', rows: [{ label: '职业', value: occupation.length > 0 ? occupation : '未填写' }, { label: '工作单位', value: company.length > 0 ? company : '未填写' }, { label: '月收入', value: income.length > 0 ? income + '元' : '未填写' }, { label: '社保 / 公积金', value: (social.length > 0 ? social : '未填写') + ' / ' + (fund.length > 0 ? fund : '未填写') }] }, { title: '资产 / 负债', rows: [{ label: '房产情况', value: house.length > 0 ? house : '未填写' }, { label: '车辆情况', value: car.length > 0 ? car : '未填写' }, { label: '存款情况', value: savings.length > 0 ? savings : '未填写' }, { label: '信用卡', value: card.length > 0 ? card : '未填写' }, { label: '其他资产', value: other.length > 0 ? other : '未填写' }] }]
|
const enterprise = assessmentType.value == 'enterprise'
|
||||||
|
const basicRows: InfoRow[] = []
|
||||||
|
if (enterprise) {
|
||||||
|
appendRow(basicRows, '企业名称', displayValue(saved, 'enterpriseName'))
|
||||||
|
appendRow(basicRows, '信用代码', mask(displayValue(saved, 'businessCode'), 4, 4))
|
||||||
|
}
|
||||||
|
appendRow(basicRows, enterprise ? '联系人' : '姓名', displayValue(saved, 'name'))
|
||||||
|
appendRow(basicRows, '手机号', mask(displayValue(saved, 'phone'), 3, 4))
|
||||||
|
appendRow(basicRows, '所在城市', displayValue(saved, 'city'))
|
||||||
|
if (!enterprise) {
|
||||||
|
appendRow(basicRows, '身份证号', mask(displayValue(saved, 'idCard'), 4, 4))
|
||||||
|
appendRow(basicRows, '婚姻状况', displayValue(saved, 'maritalStatus'))
|
||||||
|
}
|
||||||
|
const incomeRows: InfoRow[] = enterprise ? [
|
||||||
|
{ label: '经营行业', value: displayValue(saved, 'occupation') }, { label: '经营年限', value: displayValue(saved, 'operatingYears') }, { label: '年营业收入', value: draftText(saved, 'annualRevenue').length > 0 ? draftText(saved, 'annualRevenue') + ' 万元' : '未填写' }
|
||||||
|
] : [
|
||||||
|
{ label: '职业类型', value: displayValue(saved, 'occupation') }, { label: '月收入', value: draftText(saved, 'monthlyIncome').length > 0 ? draftText(saved, 'monthlyIncome') + ' 元' : '未填写' }, { label: '工作单位', value: displayValue(saved, 'company') }, { label: '社保缴纳', value: displayValue(saved, 'socialSecurity') }, { label: '公积金缴纳', value: displayValue(saved, 'housingFund') }
|
||||||
|
]
|
||||||
|
const files = documentsFromDraft(saved)
|
||||||
|
const fileNames = files.map((file) => file.name).join('、')
|
||||||
|
const grants = saved['authorizations'] as string[] | null
|
||||||
|
const grantNames: string[] = []
|
||||||
|
if (grants != null) { if (grants.includes('credit')) grantNames.push('征信'); if (grants.includes('business')) grantNames.push('经营'); if (grants.includes('fund')) grantNames.push('公积金') }
|
||||||
|
sections.value = [
|
||||||
|
{ title: '基本信息', step: 0, rows: basicRows, expanded: false },
|
||||||
|
{ title: enterprise ? '经营信息' : '职业与收入', step: 1, rows: incomeRows, expanded: false },
|
||||||
|
{ title: '资产与负债', step: 2, rows: [{ label: '房产情况', value: displayValue(saved, 'house') }, { label: '车辆情况', value: displayValue(saved, 'car') }, { label: '存款情况', value: displayValue(saved, 'savings') }, { label: '信用卡使用', value: displayValue(saved, 'creditCard') }, { label: '其他资产', value: displayValue(saved, 'otherAssets') }], expanded: false },
|
||||||
|
{ title: '资料与授权', step: 3, rows: [{ label: '本地资料', value: fileNames.length > 0 ? fileNames : '未选择' }, { label: '授权意向', value: grantNames.length > 0 ? grantNames.join('、') : '未选择' }, { label: '当前状态', value: '资料未上传 · 数据未查询' }], expanded: false }
|
||||||
|
]
|
||||||
payload.value = { ...saved, source: 'mobile', confirmed: true }
|
payload.value = { ...saved, source: 'mobile', confirmed: true }
|
||||||
}
|
}
|
||||||
onLoad(() => loadDraft())
|
onShow(() => loadDraft())
|
||||||
const edit = (sectionIndex: number) => { uni.setStorageSync('assessmentEditStep', sectionIndex); if (getCurrentPages().length > 1) uni.navigateBack({ delta: 1 }); else uni.navigateTo({ url: '/pages/assessment/assessment' }) }
|
const edit = (step: number) => {
|
||||||
const submit = async () => { if (submitting.value) return; submitting.value = true; try { await submitAssessment(payload.value); uni.redirectTo({ url: '/pages/analyzing/analyzing' }) } catch (e) { submitting.value = false; uni.showToast({ title: '提交失败,请重试', icon: 'none' }) } }
|
uni.setStorageSync('assessmentEditStep', step)
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (pages.length > 1 && pages[pages.length - 2].route == 'pages/assessment/assessment') uni.navigateBack({ delta: 1 })
|
||||||
|
else uni.navigateTo({ url: '/pages/assessment/assessment?type=' + assessmentType.value })
|
||||||
|
}
|
||||||
|
const submit = async () => {
|
||||||
|
if (!hasDraft.value) { edit(0); return }
|
||||||
|
if (submitting.value) return
|
||||||
|
const saved = payload.value
|
||||||
|
const amount = draftText(saved, assessmentType.value == 'enterprise' ? 'annualRevenue' : 'monthlyIncome')
|
||||||
|
if (draftText(saved, 'name').length < 2 || !/^1[3-9][0-9]{9}$/.test(draftText(saved, 'phone')) || draftText(saved, 'city').length == 0) { uni.showToast({ title: '请先完善基本信息', icon: 'none' }); edit(0); return }
|
||||||
|
if (assessmentType.value == 'personal' && !/(^[0-9]{15}$)|(^[0-9]{17}[0-9Xx]$)/.test(draftText(saved, 'idCard'))) { uni.showToast({ title: '请补全身份证信息', icon: 'none' }); edit(0); return }
|
||||||
|
if (assessmentType.value == 'enterprise' && (draftText(saved, 'enterpriseName').length < 2 || !/^[0-9A-Z]{18}$/.test(draftText(saved, 'businessCode').toUpperCase()))) { uni.showToast({ title: '请补全企业信息', icon: 'none' }); edit(0); return }
|
||||||
|
if (amount.length == 0 || parseFloat(amount) <= 0 || isNaN(parseFloat(amount))) { uni.showToast({ title: '请先完善收入信息', icon: 'none' }); edit(1); return }
|
||||||
|
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 }
|
||||||
|
submitting.value = true
|
||||||
|
try { await submitAssessment(payload.value); uni.redirectTo({ url: '/pages/analyzing/analyzing' }) }
|
||||||
|
catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) }
|
||||||
|
finally { submitting.value = false }
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.confirm-content { padding: 26rpx 30rpx 80rpx; }
|
.confirm-content { padding: 26rpx 30rpx 0; }
|
||||||
.confirm-tip { padding: 26rpx; border-radius: 16rpx; background-color: #edf6ff; flex-direction: row; align-items: center; }
|
.confirm-progress { flex-direction: row; align-items: center; margin-bottom: 24rpx; }
|
||||||
.tip-icon { width: 64rpx; height: 64rpx; border-radius: 50%; background-color: #0868f7; color: white; align-items: center; justify-content: center; font-size: 30rpx; margin-right: 20rpx; }
|
.progress-line { flex: 1; flex-direction: row; }
|
||||||
.tip-title { font-size: 28rpx; font-weight: 700; }
|
.progress-segment { flex: 1; height: 8rpx; margin-right: 8rpx; border-radius: 8rpx; background-color: #0868f7; }
|
||||||
.tip-desc { color: #718096; font-size: 22rpx; margin-top: 5rpx; }
|
.progress-count { font-size: 24rpx; color: #758297; margin-left: 20rpx; }
|
||||||
.info-section { margin-top: 28rpx; border-top: 1rpx solid #edf1f6; }
|
.confirm-tip { padding: 24rpx; border-radius: 18rpx; background-color: #eff6ff; flex-direction: row; align-items: center; }
|
||||||
.section-bar { height: 82rpx; flex-direction: row; align-items: center; justify-content: space-between; font-size: 28rpx; font-weight: 700; }
|
.tip-icon { width: 56rpx; height: 56rpx; border-radius: 28rpx; background-color: #0868f7; align-items: center; justify-content: center; margin-right: 18rpx; flex-shrink: 0; }
|
||||||
.edit { color: #0868f7; font-size: 22rpx; font-weight: 500; }
|
.tip-mark { color: #fff; font-size: 28rpx; }
|
||||||
.info-row { min-height: 72rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
.tip-copy { flex: 1; min-width: 0; }
|
||||||
.info-label { color: #778499; font-size: 24rpx; }
|
.tip-title { color: #22324b; font-size: 29rpx; font-weight: 600; }
|
||||||
.info-value { color: #1a273f; font-size: 25rpx; font-weight: 600; }
|
.tip-desc { color: #738198; font-size: 24rpx; line-height: 36rpx; margin-top: 7rpx; }
|
||||||
.submit-button { margin-top: 50rpx; }
|
.empty-hint { font-size: 28rpx; line-height: 44rpx; color: #778499; margin-top: 40rpx; }
|
||||||
|
.info-section { margin-top: 18rpx; border-bottom: 1rpx solid #e9eef5; padding-bottom: 14rpx; }
|
||||||
|
.section-bar { height: 74rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
|
.section-name { font-size: 29rpx; font-weight: 600; color: #22324b; }
|
||||||
|
.edit { padding: 16rpx 0 16rpx 24rpx; color: #0868f7; font-size: 25rpx; }
|
||||||
|
.info-row { min-height: 62rpx; flex-direction: row; align-items: flex-start; padding: 9rpx 0; }
|
||||||
|
.info-label { color: #758297; font-size: 25rpx; line-height: 38rpx; width: 176rpx; flex-shrink: 0; margin-right: 16rpx; }
|
||||||
|
.info-value { flex: 1; min-width: 0; text-align: right; color: #26374f; font-size: 26rpx; line-height: 38rpx; }
|
||||||
|
.expand-link { color: #7387a3; font-size: 24rpx; padding: 15rpx 0 10rpx; }
|
||||||
|
.demo-note { font-size: 23rpx; line-height: 36rpx; color: #8490a3; margin-top: 24rpx; }
|
||||||
|
.scroll-spacer { min-height: 152rpx; }
|
||||||
|
.submit-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 20rpx 30rpx; border-top: 1rpx solid #edf1f6; background-color: #fff; }
|
||||||
|
.submit-button { margin: 0; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+12
-13
@@ -6,7 +6,7 @@
|
|||||||
<view class="intro-copy">
|
<view class="intro-copy">
|
||||||
<text class="intro-title">{{ type == 'enterprise' ? '企业信用融资' : '个人信用融资' }}</text>
|
<text class="intro-title">{{ type == 'enterprise' ? '企业信用融资' : '个人信用融资' }}</text>
|
||||||
<text class="intro-sub">{{ type == 'enterprise' ? '适合企业 / 个体工商户' : '适合有稳定收入的个人用户' }}</text>
|
<text class="intro-sub">{{ type == 'enterprise' ? '适合企业 / 个体工商户' : '适合有稳定收入的个人用户' }}</text>
|
||||||
<view class="benefit" v-for="item in benefits" :key="item"><AppIcon name="check-circle" :size="30" /><text>{{ item }}</text></view>
|
<view class="benefit" v-for="item in benefits" :key="item"><view class="benefit-dot"></view><text class="benefit-text">{{ item }}</text></view>
|
||||||
</view>
|
</view>
|
||||||
<image class="entry-image" :src="type == 'enterprise' ? '/static/mobile/images/enterprise-building-cutout.png' : '/static/mobile/images/personal-building-cutout.png'" mode="aspectFit"></image>
|
<image class="entry-image" :src="type == 'enterprise' ? '/static/mobile/images/enterprise-building-cutout.png' : '/static/mobile/images/personal-building-cutout.png'" mode="aspectFit"></image>
|
||||||
</view>
|
</view>
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
<view class="process-block">
|
<view class="process-block">
|
||||||
<text class="section-title">评估流程预览</text>
|
<text class="section-title">评估流程预览</text>
|
||||||
<view class="process-row">
|
<view class="process-row">
|
||||||
<view class="process-item"><view class="process-icon"><AppIcon name="file-text" :size="42" /></view><text>填写信息</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
<view class="process-item"><view class="process-icon"><AppIcon name="file-text" :size="50" /></view><text class="process-label">填写信息</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
||||||
<view class="process-item"><view class="process-icon"><AppIcon name="sparkles" :size="42" /></view><text>AI分析</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
<view class="process-item"><view class="process-icon"><AppIcon name="sparkles" :size="50" /></view><text class="process-label">AI分析</text></view><view class="arrow"><AppIcon name="arrow-right" :size="32" /></view>
|
||||||
<view class="process-item"><view class="process-icon"><AppIcon name="building" :size="42" /></view><text>匹配方案</text></view>
|
<view class="process-item"><view class="process-icon"><AppIcon name="building" :size="50" /></view><text class="process-label">匹配方案</text></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="privacy-tip"><text>信息将加密传输,仅用于本次融资评估</text></view>
|
<view class="privacy-tip"><text class="privacy-text">自主选择授权范围,随时补充评估资料</text></view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
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 type = ref('personal')
|
const type = ref('personal')
|
||||||
const benefits = ref(['最高可借额度', '审批快 放款快', '多种产品任你选'])
|
const benefits = ref(['了解可选融资额度', '在线评估,便捷省时', '多家机构方案对比'])
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
if (options != null && options['type'] == 'enterprise') {
|
if (options != null && options['type'] == 'enterprise') {
|
||||||
type.value = 'enterprise'
|
type.value = 'enterprise'
|
||||||
benefits.value = ['最高可借额度', '企业信用贷款', '多种融资方案']
|
benefits.value = ['匹配企业资金需求', '多维度经营资质评估', '多种融资方案对比']
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const start = () => { uni.navigateTo({ url: '/pages/assessment/assessment?type=' + type.value }) }
|
const start = () => { uni.navigateTo({ url: '/pages/assessment/assessment?type=' + type.value }) }
|
||||||
@@ -50,9 +50,9 @@
|
|||||||
.intro-sub { font-size: 23rpx; color: #5f6d82; margin-top: 8rpx; margin-bottom: 30rpx; }
|
.intro-sub { font-size: 23rpx; color: #5f6d82; margin-top: 8rpx; margin-bottom: 30rpx; }
|
||||||
.benefit { flex-direction: row; align-items: center; margin-top: 18rpx; color: #23324a; font-size: 25rpx; font-weight: 600; }
|
.benefit { flex-direction: row; align-items: center; margin-top: 18rpx; color: #23324a; font-size: 25rpx; font-weight: 600; }
|
||||||
.benefit .app-icon { margin-right: 12rpx; }
|
.benefit .app-icon { margin-right: 12rpx; }
|
||||||
/* #ifndef APP */
|
.benefit-text { color: #34445c; font-size: 25rpx; line-height: 38rpx; }
|
||||||
.enterprise-page .benefit .app-icon { filter: hue-rotate(330deg) saturate(1.7); }
|
.benefit-dot { width: 10rpx; height: 10rpx; border-radius: 5rpx; background-color: #0868f7; margin-right: 14rpx; flex-shrink: 0; }
|
||||||
/* #endif */
|
.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: -35rpx; bottom: -22rpx; }
|
||||||
.start-button { margin-top: 30rpx; }
|
.start-button { margin-top: 30rpx; }
|
||||||
@@ -61,9 +61,8 @@
|
|||||||
.process-item { align-items: center; color: #455369; font-size: 22rpx; }
|
.process-item { align-items: center; color: #455369; font-size: 22rpx; }
|
||||||
.process-icon { width: 88rpx; height: 88rpx; border-radius: 26rpx; background-color: #e9f2ff; align-items: center; justify-content: center; margin-bottom: 14rpx; }
|
.process-icon { width: 88rpx; height: 88rpx; border-radius: 26rpx; background-color: #e9f2ff; align-items: center; justify-content: center; margin-bottom: 14rpx; }
|
||||||
.enterprise-page .process-icon { background-color: #fff0e5; }
|
.enterprise-page .process-icon { background-color: #fff0e5; }
|
||||||
/* #ifndef APP */
|
.process-label { font-size: 24rpx; color: #455369; }
|
||||||
.enterprise-page .process-icon .app-icon { filter: hue-rotate(330deg) saturate(1.7); }
|
.privacy-text { font-size: 22rpx; color: #748398; text-align: center; line-height: 34rpx; }
|
||||||
/* #endif */
|
|
||||||
|
|
||||||
.arrow { margin-top: -28rpx; }
|
.arrow { margin-top: -28rpx; }
|
||||||
.privacy-tip { margin-top: 55rpx; align-items: center; color: #97a3b4; font-size: 21rpx; }
|
.privacy-tip { margin-top: 55rpx; align-items: center; color: #97a3b4; font-size: 21rpx; }
|
||||||
|
|||||||
+26
-17
@@ -1,6 +1,6 @@
|
|||||||
<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 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('/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>
|
||||||
<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('/pages/assistant/assistant')">
|
||||||
@@ -12,14 +12,15 @@
|
|||||||
<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 green"><AppIcon name="trend-up" :size="46" /></view><text>融资课堂</text></view>
|
<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('/pages/assistant/assistant')"><view class="shortcut-icon purple"><AppIcon name="sparkles" :size="46" /></view><text>AI融资助手</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('/pages/entry/entry?type=personal')"><view class="shortcut-icon blue"><AppIcon name="calculator" :size="46" /></view><text>融资测算</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('/pages/points/points')"><view class="shortcut-icon orange"><AppIcon name="gift" :size="46" /></view><text>邀请有礼</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>
|
</view>
|
||||||
<view class="section-head"><text class="section-title">我的融资方案</text><text class="more" @click="go('/pages/matches/matches')">查看全部 ›</text></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="plan-card" @click="go('/pages/matches/matches')"><view class="plan-main"><text class="plan-name">信用优享方案</text><text class="plan-meta">3家机构可申请 · 最高50万</text></view><view class="score"><text class="score-number">85</text><text>综合分</text></view></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><text class="more" @click="go('/pages/classroom/classroom')">进入课堂 ›</text></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="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="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('/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>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -33,6 +34,8 @@
|
|||||||
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'
|
||||||
|
const hasUnread = ref(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('/pages/entry/entry?type=' + type) }
|
||||||
</script>
|
</script>
|
||||||
@@ -46,24 +49,28 @@
|
|||||||
.red-dot { position: absolute; width: 13rpx; height: 13rpx; right: 2rpx; top: 0; background-color: #ff4d4f; border-radius: 50%; border: 3rpx solid #fff; }
|
.red-dot { position: absolute; width: 13rpx; height: 13rpx; right: 2rpx; top: 0; background-color: #ff4d4f; border-radius: 50%; border: 3rpx solid #fff; }
|
||||||
.home-scroll { }
|
.home-scroll { }
|
||||||
.home-content { padding: 18rpx 28rpx 180rpx; }
|
.home-content { padding: 18rpx 28rpx 180rpx; }
|
||||||
.ai-banner { height: 260rpx; border-radius: 20rpx; padding: 32rpx; overflow: hidden; flex-direction: row; align-items: center; background: linear-gradient(125deg, #1263f0 0%, #0788ff 100%); box-shadow: 0 16rpx 36rpx rgba(0, 100, 237, .20); }
|
.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%); }
|
||||||
.banner-copy { z-index: 2; }
|
.banner-copy { z-index: 2; }
|
||||||
.banner-title { color: #ffffff; font-size: 39rpx; font-weight: 800; }
|
.banner-title { color: #ffffff; font-size: 39rpx; font-weight: 800; }
|
||||||
.banner-subtitle { color: #ffffff; font-size: 30rpx; font-weight: 700; margin-top: 12rpx; }
|
.banner-subtitle { color: #ffffff; font-size: 30rpx; font-weight: 700; margin-top: 12rpx; }
|
||||||
.banner-desc { color: #dceeff; font-size: 22rpx; margin-top: 14rpx; }
|
.banner-desc { color: #dceeff; font-size: 22rpx; margin-top: 14rpx; }
|
||||||
.robot-img { position: absolute; right: 5rpx; width: 270rpx; height: 235rpx; }
|
.robot-img { position: absolute; right: 0; width: 240rpx; height: 220rpx; }
|
||||||
.entry-row { flex-direction: row; margin-top: 22rpx; }
|
.entry-row { flex-direction: row; margin-top: 22rpx; }
|
||||||
.finance-entry { flex: 1; height: 180rpx; border-radius: 18rpx; padding: 26rpx 22rpx; flex-direction: row; overflow: hidden; position: relative; }
|
.finance-entry { flex: 1; height: 180rpx; border-radius: 18rpx; padding: 26rpx 22rpx; flex-direction: row; overflow: hidden; position: relative; }
|
||||||
.entry-following { margin-left: 18rpx; }
|
.entry-following { margin-left: 18rpx; }
|
||||||
.personal { background-color: #eaf3ff; }
|
.personal { background-color: #126df1; }
|
||||||
.enterprise { background-color: #fff1e7; }
|
.enterprise { background-color: #ff821c; }
|
||||||
.finance-entry-image { position: absolute; width: 150rpx; height: 135rpx; right: -8rpx; bottom: -8rpx; }
|
.finance-entry-image { position: absolute; width: 150rpx; height: 135rpx; right: -8rpx; bottom: -8rpx; }
|
||||||
.entry-title { font-size: 29rpx; font-weight: 800; }
|
.entry-title { font-size: 29rpx; font-weight: 800; }
|
||||||
.personal .entry-title { color: #0868f7; }
|
.personal .entry-title { color: #ffffff; }
|
||||||
.enterprise .entry-title { color: #ff7115; }
|
.enterprise .entry-title { color: #ffffff; }
|
||||||
.entry-subtitle { margin-top: 10rpx; color: #65738a; font-size: 21rpx; }
|
.entry-subtitle { margin-top: 10rpx; color: #ffffff; font-size: 21rpx; }
|
||||||
.shortcut-row { height: 160rpx; margin-top: 24rpx; flex-direction: row; align-items: center; justify-content: space-between; background-color: #fff; border-radius: 18rpx; padding: 0 8rpx; border: 1rpx solid #eaf0f7; }
|
.shortcut-row { height: 160rpx; margin-top: 24rpx; flex-direction: row; align-items: center; justify-content: space-between; background-color: #fff; border-radius: 18rpx; padding: 0 8rpx; border: 1rpx solid #eaf0f7; }
|
||||||
.shortcut { flex: 1; align-items: center; color: #536176; font-size: 18rpx; white-space: nowrap; text-align: center; }
|
.shortcut { flex: 1; align-items: center; }
|
||||||
|
.shortcut-label { font-size: 23rpx; color: #34445c; text-align: center; }
|
||||||
|
.archive-entry { margin-top: 20rpx; padding: 20rpx 24rpx; flex-direction: row; align-items: center; background-color: #ffffff; border: 1rpx solid #e6edf7; border-radius: 16rpx; }
|
||||||
|
.archive-label { flex: 1; margin-left: 16rpx; font-size: 27rpx; font-weight: 600; color: #243650; }
|
||||||
|
.archive-action { color: #0868f7; font-size: 23rpx; margin-right: 8rpx; }
|
||||||
.shortcut-icon { width: 76rpx; height: 76rpx; border-radius: 24rpx; align-items: center; justify-content: center; margin-bottom: 10rpx; }
|
.shortcut-icon { width: 76rpx; height: 76rpx; border-radius: 24rpx; align-items: center; justify-content: center; margin-bottom: 10rpx; }
|
||||||
.green { background-color: #e7f8f1; }
|
.green { background-color: #e7f8f1; }
|
||||||
.purple { background-color: #efebff; }
|
.purple { background-color: #efebff; }
|
||||||
@@ -71,10 +78,12 @@
|
|||||||
.orange { background-color: #fff0e8; }
|
.orange { background-color: #fff0e8; }
|
||||||
.section-head { margin-top: 34rpx; margin-bottom: 18rpx; flex-direction: row; justify-content: space-between; align-items: center; }
|
.section-head { margin-top: 34rpx; margin-bottom: 18rpx; flex-direction: row; justify-content: space-between; align-items: center; }
|
||||||
.more { color: #1673ed; font-size: 22rpx; }
|
.more { color: #1673ed; font-size: 22rpx; }
|
||||||
|
.more-link { flex-direction: row; align-items: center; }
|
||||||
.plan-card { height: 150rpx; background-color: #fff; border: 1rpx solid #e6edf7; border-radius: 16rpx; padding: 26rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
.plan-card { height: 150rpx; background-color: #fff; border: 1rpx solid #e6edf7; border-radius: 16rpx; padding: 26rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.plan-name { font-size: 29rpx; font-weight: 700; }
|
.plan-name { font-size: 29rpx; font-weight: 700; }
|
||||||
.plan-meta { margin-top: 12rpx; color: #8290a5; font-size: 22rpx; }
|
.plan-meta { margin-top: 12rpx; color: #8290a5; font-size: 22rpx; }
|
||||||
.score { align-items: center; color: #18aa78; font-size: 19rpx; }
|
.score { align-items: center; }
|
||||||
|
.score-label { color: #718198; font-size: 21rpx; }
|
||||||
.score-number { font-size: 48rpx; line-height: 52rpx; color: #0868f7; font-weight: 800; }
|
.score-number { font-size: 48rpx; line-height: 52rpx; color: #0868f7; font-weight: 800; }
|
||||||
.news-preview { min-height: 130rpx; background-color: #fff; border-radius: 16rpx; padding: 24rpx; flex-direction: row; justify-content: space-between; align-items: center; border: 1rpx solid #e6edf7; }
|
.news-preview { min-height: 130rpx; background-color: #fff; border-radius: 16rpx; padding: 24rpx; flex-direction: row; justify-content: space-between; align-items: center; border: 1rpx solid #e6edf7; }
|
||||||
.news-copy { flex: 1; padding-right: 16rpx; }
|
.news-copy { flex: 1; padding-right: 16rpx; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<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="/static/mobile/images/shield.png" mode="aspectFit"></image>
|
||||||
<view class="launch-line"><view class="launch-dot"></view><text>开启智能融资新体验</text></view>
|
<view class="launch-line"><view class="launch-dot"></view><text class="launch-text">开启智能融资新体验</text></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -38,4 +38,5 @@
|
|||||||
.splash-art { width: 550rpx; height: 420rpx; margin-top: 35rpx; }
|
.splash-art { width: 550rpx; height: 420rpx; margin-top: 35rpx; }
|
||||||
.launch-line { position: absolute; bottom: 82rpx; flex-direction: row; align-items: center; color: #9aa7ba; font-size: 22rpx; }
|
.launch-line { position: absolute; bottom: 82rpx; flex-direction: row; align-items: center; color: #9aa7ba; font-size: 22rpx; }
|
||||||
.launch-dot { width: 9rpx; height: 9rpx; border-radius: 50%; background-color: #2d8cff; margin-right: 12rpx; box-shadow: 0 0 18rpx rgba(45, 140, 255, .7); }
|
.launch-dot { width: 9rpx; height: 9rpx; border-radius: 50%; background-color: #2d8cff; margin-right: 12rpx; box-shadow: 0 0 18rpx rgba(45, 140, 255, .7); }
|
||||||
|
.launch-text { color: #7d8ca3; font-size: 24rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+96
-39
@@ -1,55 +1,112 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="课程详情" />
|
<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>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.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 AppHeader from '@/components/AppHeader.uvue'
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
const title = ref('如何提高个人贷款通过率')
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
const tag = ref('个人融资')
|
const lesson = ref<FinanceLesson>(financeLessons[0])
|
||||||
const duration = ref('6 分钟')
|
const available = ref(true)
|
||||||
const cover = ref('/static/mobile/images/news-calculator.png')
|
const completed = ref<boolean[]>([])
|
||||||
const chapters = ref(['理解征信评分', '整理收入与负债', '准备申请材料', '选择适合的产品'])
|
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) => {
|
onLoad((options) => {
|
||||||
if (options == null) return
|
if (options != null) {
|
||||||
const lessonId = options['id'] == null ? '' : options['id'] as string
|
const id = 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 = ['确认主体与经营信息', '整理银行流水与纳税记录', '准备企业资质材料', '核对授权与提交清单'] }
|
const legacyTitle = options['title'] == null ? '' : options['title'] as string
|
||||||
else if (lessonId == '2') { title.value = '读懂贷款利率与还款方式'; tag.value = '基础知识'; duration.value = '5 分钟'; cover.value = '/static/mobile/images/news-city.png'; chapters.value = ['年化利率怎么看', '等额本息与先息后本', '期限对总成本的影响', '选择适合现金流的方案'] }
|
if (id != '' || legacyTitle != '') {
|
||||||
if (options['title'] != null) title.value = options['title'] as string
|
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>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.lesson-detail-scroll { }
|
.lesson-detail-content { padding: 24rpx 28rpx 48rpx; }
|
||||||
.lesson-detail-content { padding: 26rpx 28rpx 90rpx; }
|
.lesson-cover { width: 100%; height: 260rpx; border-radius: 18rpx; }
|
||||||
.lesson-cover { width: 100%; height: 290rpx; border-radius: 18rpx; }
|
.lesson-detail-title { color: #172640; font-size: 36rpx; line-height: 50rpx; font-weight: 700; margin-top: 24rpx; }
|
||||||
.lesson-detail-title {
|
.lesson-detail-meta { flex-direction: row; justify-content: space-between; align-items: center; margin-top: 14rpx; }
|
||||||
/* #ifndef APP */
|
.lesson-tag { color: #0868f7; font-size: 24rpx; }
|
||||||
display: block;
|
.lesson-duration { color: #66758a; font-size: 23rpx; }
|
||||||
/* #endif */
|
.lesson-intro { color: #66758a; font-size: 26rpx; line-height: 44rpx; margin-top: 22rpx; }
|
||||||
color: #172640; font-size: 36rpx; line-height: 50rpx; font-weight: 800; margin-top: 24rpx; }
|
.progress-head { flex-direction: row; align-items: center; justify-content: space-between; margin-top: 26rpx; }
|
||||||
.lesson-detail-meta { flex-direction: row; justify-content: space-between; color: #0868f7; font-size: 21rpx; margin-top: 12rpx; }
|
.progress-label { color: #172640; font-size: 24rpx; font-weight: 600; }
|
||||||
.lesson-progress { height: 12rpx; margin-top: 22rpx; border-radius: 8rpx; background-color: #e4edf8; overflow: hidden; }
|
.progress-value { color: #0868f7; font-size: 23rpx; }
|
||||||
.progress-fill { width: 12%; height: 100%; border-radius: 8rpx; background-color: #0868f7; }
|
.lesson-progress { height: 10rpx; margin-top: 12rpx; border-radius: 6rpx; background-color: #e1eafa; overflow: hidden; }
|
||||||
.lesson-intro {
|
.progress-fill { height: 100%; border-radius: 6rpx; background-color: #0868f7; }
|
||||||
/* #ifndef APP */
|
.start-learning { margin-top: 24rpx; }
|
||||||
display: block;
|
.chapter-heading { color: #172640; font-size: 30rpx; font-weight: 700; margin-top: 32rpx; margin-bottom: 14rpx; }
|
||||||
/* #endif */
|
.chapter-card { border: 1rpx solid #e5ecf5; background-color: #ffffff; border-radius: 16rpx; margin-bottom: 14rpx; overflow: hidden; }
|
||||||
color: #5f6d81; font-size: 24rpx; line-height: 42rpx; margin: 26rpx 0; }
|
.chapter-row { min-height: 116rpx; padding: 20rpx; flex-direction: row; align-items: center; }
|
||||||
.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: 14rpx; background-color: #eaf2ff; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
|
||||||
.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; }
|
.index-text { color: #0868f7; font-size: 24rpx; font-weight: 700; }
|
||||||
.chapter-copy { flex: 1; }
|
.completed-index { background-color: #eaf7ef; }
|
||||||
.chapter-title { color: #23324a; font-size: 25rpx; font-weight: 700; }
|
.completed-text { color: #168454; }
|
||||||
.chapter-desc {
|
.chapter-copy { flex: 1; padding-right: 8rpx; }
|
||||||
/* #ifndef APP */
|
.chapter-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; }
|
||||||
display: block;
|
.chapter-state { color: #66758a; font-size: 22rpx; margin-top: 6rpx; }
|
||||||
/* #endif */
|
.completed-state { color: #168454; }
|
||||||
color: #97a3b2; font-size: 19rpx; margin-top: 7rpx; }
|
.chapter-arrow { transform: rotate(90deg); }
|
||||||
.start-learning { margin-top: 28rpx; }
|
.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>
|
</style>
|
||||||
|
|||||||
+67
-88
@@ -1,101 +1,85 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="login-page page-white" :style="pageStyle()">
|
<view class="login-page page-white" :style="pageStyle()">
|
||||||
<view class="status-space" :style="statusStyle(90)"></view>
|
<view :style="statusStyle(44)"></view>
|
||||||
|
<scroll-view scroll-y :style="contentStyle(44, false)">
|
||||||
<view class="login-content">
|
<view class="login-content">
|
||||||
<BrandLogo />
|
<BrandLogo />
|
||||||
<text class="welcome">欢迎登录 AI融资</text>
|
<text class="welcome">欢迎登录 AI融资</text>
|
||||||
<view class="login-tabs">
|
<text class="login-subtitle">智能评估,让融资更简单</text>
|
||||||
<view :class="mode == 'phone' ? 'login-tab selected' : 'login-tab'" @click="switchMode('phone')"><text>手机号登录</text></view>
|
<text class="form-heading">{{ mode == 'phone' ? '手机号登录 / 注册' : '密码登录' }}</text>
|
||||||
<view :class="mode == 'password' ? 'login-tab selected' : 'login-tab'" @click="switchMode('password')"><text>密码登录</text></view>
|
<view class="input-box"><text class="input-prefix">+86</text><input v-model="phone" :disabled="loading" type="number" maxlength="11" placeholder="请输入手机号" class="field-input" /></view>
|
||||||
</view>
|
<view v-if="mode == 'password'" class="input-box"><input v-model="password" :disabled="loading" password placeholder="请输入登录密码" class="field-input" /></view>
|
||||||
<view class="input-box"><text class="input-prefix">+86</text><input v-model="phone" type="number" maxlength="11" placeholder="请输入手机号" class="input-box-input" /></view>
|
<view v-if="mode == 'phone' && smsSent" class="input-box"><input v-model="smsCode" type="number" maxlength="6" placeholder="请输入 6 位验证码" class="field-input" /><text class="countdown" @click="resendSms">{{ countdown > 0 ? countdown.toString() + 's 后重试' : '重新获取' }}</text></view>
|
||||||
<view v-if="mode == 'password'" class="input-box"><text class="input-prefix">密码</text><input v-model="password" password placeholder="请输入登录密码" class="input-box-input" /></view>
|
|
||||||
<view v-if="mode == 'phone' && smsSent" class="input-box"><text class="input-prefix">验证码</text><input v-model="smsCode" type="number" maxlength="6" placeholder="请输入 6 位验证码" class="input-box-input" /><text class="countdown" @click="resendSms">{{ countdown > 0 ? countdown.toString() + 's' : '重新获取' }}</text></view>
|
|
||||||
<text v-if="errorMessage.length > 0" class="error-message">{{ errorMessage }}</text>
|
<text v-if="errorMessage.length > 0" class="error-message">{{ errorMessage }}</text>
|
||||||
<button class="login-button" :disabled="loading" @click="login" :class="loading ? 'login-disabled' : ''">{{ loading ? '处理中...' : (mode == 'phone' ? (smsSent ? '登录' : '获取验证码') : '登录') }}</button>
|
<button class="login-button" :disabled="loading" @click="login">{{ loading ? '处理中…' : (mode == 'phone' ? (smsSent ? '登录' : '获取验证码') : '登录') }}</button>
|
||||||
<view class="other-login"><view class="line"></view><text>其他登录方式</text><view class="line"></view></view>
|
<view class="mode-row"><text class="mode-link" @click="switchMode">{{ mode == 'phone' ? '密码登录' : '使用验证码登录' }}</text><text v-if="isDemo" class="demo-label">演示模式</text></view>
|
||||||
<view class="social-row">
|
<text v-if="isDemo && smsSent" class="demo-hint">未发送真实短信,可输入任意 6 位数字体验。</text>
|
||||||
<view class="social wechat"><AppIcon name="message" :size="38" /></view><view class="social apple"><AppIcon name="lock" :size="38" /></view><view class="social phone"><AppIcon name="user" :size="38" /></view>
|
<view class="other-login"><view class="line"></view><text class="other-text">其他登录方式</text><view class="line"></view></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="agreement" @click="agreed = !agreed"><view :class="agreed ? 'check checked' : 'check'"><text v-if="agreed">✓</text></view><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>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { statusStyle, pageStyle } = usePageLayout()
|
|
||||||
import BrandLogo from '@/components/BrandLogo.uvue'
|
import BrandLogo from '@/components/BrandLogo.uvue'
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
import { requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/services/api.uts'
|
import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/services/api.uts'
|
||||||
|
import { showAgreementNotice } from '@/services/assessment.uts'
|
||||||
|
const { statusStyle, contentStyle, pageStyle } = usePageLayout()
|
||||||
const mode = ref('phone')
|
const mode = ref('phone')
|
||||||
const phone = ref('13800138000')
|
const phone = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const smsCode = ref('')
|
const smsCode = ref('')
|
||||||
const smsSent = ref(false)
|
const smsSent = ref(false)
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const agreed = ref(true)
|
const agreed = ref(false)
|
||||||
|
const isDemo = API_BASE_URL.length == 0
|
||||||
|
const showAgreement = (title: string) => { showAgreementNotice(title) }
|
||||||
let countdownTimer = 0
|
let countdownTimer = 0
|
||||||
|
watch(phone, () => {
|
||||||
const switchMode = (nextMode: string) => {
|
smsSent.value = false; smsCode.value = ''; countdown.value = 0; errorMessage.value = ''
|
||||||
mode.value = nextMode
|
clearInterval(countdownTimer)
|
||||||
errorMessage.value = ''
|
})
|
||||||
}
|
const switchMode = () => { if (loading.value) return; mode.value = mode.value == 'phone' ? 'password' : 'phone'; errorMessage.value = '' }
|
||||||
|
const socialLogin = (provider: string) => { uni.showModal({ title: provider + '暂未开放', content: '请先使用手机号登录。该登录方式将在正式服务接入后开放。', showCancel: false, confirmText: '我知道了' }) }
|
||||||
const validPhone = (): boolean => {
|
const validPhone = (): boolean => {
|
||||||
if (!/^1[3-9][0-9]{9}$/.test(phone.value)) {
|
if (!/^1[3-9][0-9]{9}$/.test(phone.value)) { errorMessage.value = '请输入正确的 11 位手机号'; return false }
|
||||||
errorMessage.value = '请输入正确的 11 位手机号'
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const startCountdown = () => {
|
const startCountdown = () => {
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
clearInterval(countdownTimer)
|
clearInterval(countdownTimer)
|
||||||
countdownTimer = setInterval(() => {
|
countdownTimer = setInterval(() => {
|
||||||
if (countdown.value <= 1) {
|
if (countdown.value <= 1) { countdown.value = 0; clearInterval(countdownTimer) } else countdown.value -= 1
|
||||||
countdown.value = 0
|
|
||||||
clearInterval(countdownTimer)
|
|
||||||
countdownTimer = 0
|
|
||||||
} else {
|
|
||||||
countdown.value -= 1
|
|
||||||
}
|
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
const finishLogin = (token: string) => {
|
const finishLogin = (token: string) => { saveSession(token, phone.value); uni.redirectTo({ url: '/pages/home/home' }) }
|
||||||
saveSession(token, phone.value)
|
|
||||||
uni.redirectTo({ url: '/pages/home/home' })
|
|
||||||
}
|
|
||||||
const resendSms = async () => {
|
const resendSms = async () => {
|
||||||
if (loading.value || countdown.value > 0 || !validPhone()) return
|
if (loading.value || countdown.value > 0 || !validPhone()) return
|
||||||
loading.value = true
|
if (!agreed.value) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return }
|
||||||
errorMessage.value = ''
|
loading.value = true; errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
if (!(await requestSmsCode(phone.value))) throw new Error('验证码发送失败,请稍后重试')
|
if (!(await requestSmsCode(phone.value))) throw new Error('验证码发送失败,请稍后重试')
|
||||||
startCountdown()
|
startCountdown()
|
||||||
uni.showToast({ title: '验证码已重新发送', icon: 'none' })
|
uni.showToast({ title: isDemo ? '演示验证码已就绪' : '验证码已重新发送', icon: 'none' })
|
||||||
} catch (error) {
|
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' } finally { loading.value = false }
|
||||||
errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
if (!agreed.value) { errorMessage.value = '请先同意用户协议'; return }
|
if (!agreed.value) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return }
|
||||||
if (!validPhone()) return
|
if (!validPhone() || loading.value) return
|
||||||
if (loading.value) return
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
if (mode.value == 'phone') {
|
if (mode.value == 'phone') {
|
||||||
if (!smsSent.value) {
|
if (!smsSent.value) {
|
||||||
if (countdown.value > 0) return
|
if (!(await requestSmsCode(phone.value))) throw new Error('验证码发送失败,请稍后重试')
|
||||||
const sent = await requestSmsCode(phone.value)
|
smsSent.value = true; startCountdown()
|
||||||
if (!sent) throw new Error('验证码发送失败,请稍后重试')
|
uni.showToast({ title: isDemo ? '演示验证码已就绪' : '验证码已发送', icon: 'none' })
|
||||||
smsSent.value = true
|
|
||||||
startCountdown()
|
|
||||||
uni.showToast({ title: '验证码已发送', icon: 'none' })
|
|
||||||
} else {
|
} else {
|
||||||
if (!/^[0-9]{6}$/.test(smsCode.value)) throw new Error('请输入 6 位数字验证码')
|
if (!/^[0-9]{6}$/.test(smsCode.value)) throw new Error('请输入 6 位数字验证码')
|
||||||
finishLogin(await loginWithSms(phone.value, smsCode.value))
|
finishLogin(await loginWithSms(phone.value, smsCode.value))
|
||||||
@@ -104,42 +88,37 @@
|
|||||||
if (password.value.length < 6) throw new Error('密码至少需要 6 位')
|
if (password.value.length < 6) throw new Error('密码至少需要 6 位')
|
||||||
finishLogin(await loginWithPassword(phone.value, password.value))
|
finishLogin(await loginWithPassword(phone.value, password.value))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试' } finally { loading.value = false }
|
||||||
errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试'
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onUnmounted(() => { clearInterval(countdownTimer) })
|
onUnmounted(() => { clearInterval(countdownTimer) })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.login-page { }
|
.login-content { padding: 20rpx 40rpx 48rpx; }
|
||||||
.status-space { }
|
.welcome { font-size: 42rpx; line-height: 60rpx; font-weight: 800; margin-top: 38rpx; color: #152443; }
|
||||||
.login-content { padding: 0 54rpx; }
|
.login-subtitle { font-size: 26rpx; color: #748198; line-height: 40rpx; margin-top: 8rpx; }
|
||||||
.welcome { font-size: 48rpx; line-height: 70rpx; font-weight: 800; margin-top: 60rpx; color: #122347; }
|
.form-heading { margin-top: 54rpx; font-size: 28rpx; font-weight: 600; color: #22324a; }
|
||||||
.login-tabs { flex-direction: row; margin-top: 52rpx; border-bottom: 2rpx solid #eaf0f7; }
|
.input-box { height: 96rpx; margin-top: 22rpx; border: 1rpx solid #dce4ef; border-radius: 16rpx; padding: 0 24rpx; flex-direction: row; align-items: center; background-color: #fafcff; }
|
||||||
.login-tab { margin-right: 58rpx; padding: 0 4rpx 18rpx; color: #7d899d; font-size: 28rpx; }
|
.input-prefix { font-size: 28rpx; color: #31415a; margin-right: 22rpx; flex-shrink: 0; }
|
||||||
.login-tab.selected { color: #0868f7; font-weight: 700; border-bottom: 5rpx solid #0868f7; }
|
.field-input { flex: 1; min-width: 0; font-size: 28rpx; color: #17243c; }
|
||||||
.input-box { height: 96rpx; margin-top: 28rpx; border: 2rpx solid #dfe7f2; border-radius: 14rpx; padding: 0 24rpx; flex-direction: row; align-items: center; background-color: #fbfdff; }
|
.countdown { text-align: right; color: #0868f7; font-size: 24rpx; flex-shrink: 0; margin-left: 12rpx; }
|
||||||
.input-prefix { font-size: 25rpx; color: #3c4a61; margin-right: 24rpx; }
|
.error-message { margin-top: 16rpx; color: #d64040; font-size: 24rpx; line-height: 36rpx; }
|
||||||
.input-box-input { flex: 1; font-size: 28rpx; color: #17243c; }
|
.login-button { height: 92rpx; line-height: 92rpx; margin-top: 28rpx; border-radius: 16rpx; color: #fff; font-size: 30rpx; font-weight: 600; background-color: #0868f7; }
|
||||||
.countdown { min-width: 90rpx; text-align: right; color: #0868f7; font-size: 24rpx; }
|
.mode-row { flex-direction: row; justify-content: space-between; align-items: center; margin-top: 12rpx; }
|
||||||
.error-message {
|
.mode-link { padding: 16rpx 0; color: #31415a; font-size: 26rpx; }
|
||||||
/* #ifndef APP */
|
.demo-label { color: #8591a2; font-size: 22rpx; }
|
||||||
display: block;
|
.demo-hint { color: #65748a; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
|
||||||
/* #endif */
|
.other-login { margin-top: 66rpx; flex-direction: row; align-items: center; justify-content: center; }
|
||||||
margin-top: 16rpx; color: #e55353; font-size: 23rpx; }
|
.other-text { font-size: 24rpx; color: #8490a3; flex-shrink: 0; }
|
||||||
.login-button { height: 92rpx; line-height: 92rpx; margin-top: 32rpx; border-radius: 14rpx; color: white; font-size: 29rpx; font-weight: 700; background-color: #0868f7; box-shadow: 0 16rpx 30rpx rgba(8, 104, 247, .2); }
|
.line { height: 1rpx; background-color: #e5eaf1; flex: 1; margin: 0 20rpx; }
|
||||||
.login-disabled { opacity: .6; }
|
.social-row { flex-direction: row; justify-content: center; margin-top: 28rpx; }
|
||||||
.other-login { margin-top: 62rpx; flex-direction: row; align-items: center; justify-content: center; color: #a0a9b7; font-size: 22rpx; }
|
.social-button { width: 150rpx; min-height: 112rpx; align-items: center; justify-content: center; margin: 0 26rpx; }
|
||||||
.line { height: 1rpx; background-color: #e5eaf1; width: 120rpx; margin: 0 20rpx; }
|
.social-label { color: #58677e; font-size: 24rpx; margin-top: 12rpx; }
|
||||||
.social-row { flex-direction: row; justify-content: center; margin-top: 40rpx; }
|
.agreement { margin-top: 52rpx; flex-direction: row; align-items: flex-start; }
|
||||||
.social { width: 84rpx; height: 84rpx; border-radius: 50%; align-items: center; justify-content: center; margin: 0 25rpx; font-size: 28rpx; font-weight: 800; }
|
.check-hit { width: 44rpx; min-height: 44rpx; flex-shrink: 0; justify-content: center; }
|
||||||
.wechat { background-color: #e9f8ef; color: #12ad58; }
|
.check { width: 30rpx; height: 30rpx; border: 2rpx solid #9aa8ba; border-radius: 6rpx; align-items: center; justify-content: center; }
|
||||||
.apple { background-color: #f2f4f7; color: #111; }
|
.checked { background-color: #0868f7; border-color: #0868f7; }
|
||||||
.phone { background-color: #fff0ea; color: #ff6530; }
|
.check-mark { font-size: 23rpx; color: #fff; }
|
||||||
.agreement { margin-top: 62rpx; flex-direction: row; align-items: center; justify-content: center; color: #8793a8; font-size: 21rpx; }
|
.agreement-copy { flex: 1; min-width: 0; color: #748198; font-size: 24rpx; line-height: 42rpx; }
|
||||||
.check { width: 28rpx; height: 28rpx; border: 2rpx solid #aeb8c7; border-radius: 6rpx; align-items: center; justify-content: center; margin-right: 10rpx; }
|
.agreement-link { color: #0868f7; font-size: 24rpx; line-height: 42rpx; }
|
||||||
.checked { color: white; background-color: #0868f7; border-color: #0868f7; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+62
-39
@@ -1,18 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="为您匹配到 3 个方案" />
|
<AppHeader title="匹配融资方案" />
|
||||||
<view class="filter-bar"><view :class="filter == 'all' ? 'filter active' : 'filter'" @click="filter='all'"><text>综合排序</text></view><view :class="filter == 'amount' ? 'filter active' : 'filter'" @click="filter='amount'"><text>额度高</text></view><view :class="filter == 'rate' ? 'filter active' : 'filter'" @click="filter='rate'"><text>利率低</text></view></view>
|
<view class="filter-bar"><view v-for="item in filters" :key="item.id" :class="filter == item.id ? 'filter active' : 'filter'" @click="filter = item.id"><text :class="filter == item.id ? 'filter-label selected' : 'filter-label'">{{ item.label }}</text></view></view>
|
||||||
<scroll-view scroll-y class="match-scroll" :style="contentStyle(318, false)">
|
<scroll-view scroll-y :style="contentStyle(314, false)">
|
||||||
<view class="match-content">
|
<view class="match-content">
|
||||||
<view v-if="loading" class="state-panel"><text>正在为您匹配方案...</text></view>
|
<view v-if="loading" class="state-panel"><text class="state-text">正在匹配融资方案…</text></view>
|
||||||
<view v-else-if="errorMessage.length > 0" class="state-panel error-panel"><text>{{ errorMessage }}</text><button class="retry-button" @click="loadMatches">重新加载</button></view>
|
<view v-else-if="errorMessage.length > 0" class="state-panel"><text class="state-text">{{ errorMessage }}</text><button class="retry-button" @click="loadMatches">重新加载</button></view>
|
||||||
<view class="match-card" v-for="item in matches" :key="item.rank">
|
<view v-else-if="matches.length == 0" class="state-panel"><text class="state-text">暂无可展示方案,请完善评估资料后重试</text></view>
|
||||||
<view :class="'rank rank-' + item.rank"><text>{{ item.rank }}</text></view>
|
<view v-else>
|
||||||
<view class="bank-logo"><AppIcon name="building" :size="40" /></view>
|
<text class="result-summary">共 {{ matches.length }} 个方案 · {{ isDemo ? '示例数据' : '按您的评估信息匹配' }}</text>
|
||||||
<view class="match-main"><text class="bank-name">{{ item.bank }}</text><text class="match-tag">个人信用贷 · 资料简</text><view class="metrics"><view class="metrics-view"><text class="metric-value">{{ item.amount }}</text><text class="metric-label">最高额度</text></view><view class="metrics-view"><text class="metric-value rate-value">{{ item.rate }}</text><text class="metric-label">年利率</text></view><view class="metrics-view"><text class="metric-value">{{ item.term }}</text><text class="metric-label">贷款期限</text></view></view></view>
|
<view class="match-card" v-for="item in sortedMatches" :key="item.id" @click="openProduct(item.id)">
|
||||||
<button class="detail-button" @click="openProduct(item.rank)">查看详情</button>
|
<view class="bank-row"><view class="bank-logo" :style="'background-color:' + item.color"><text class="bank-initial">{{ item.initials }}</text></view><view class="bank-copy"><text class="bank-name">{{ item.bank }}</text><text class="match-tag">个人信用融资</text></view><text class="match-score">{{ item.score }}%匹配</text></view>
|
||||||
|
<view class="metrics"><view class="metric"><text class="metric-value">{{ item.amount }}<text class="metric-unit"> 万</text></text><text class="metric-label">最高额度</text></view><view class="metric"><text class="metric-value rate-value">{{ item.rate.toFixed(2) }}%</text><text class="metric-label">年化利率(起)</text></view><view class="metric term-metric"><text class="term-value">{{ item.term }}</text><text class="metric-label">贷款期限</text></view></view>
|
||||||
|
<view class="card-footer"><text class="footer-hint">{{ item.advantages.join(' · ') }}</text><view class="detail-link"><text class="detail-label">查看详情</text><AppIcon name="chevron-right" :size="26" /></view></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="list-tip">匹配结果基于您的评估信息,实际额度以机构审批为准</text>
|
</view>
|
||||||
|
<text class="list-tip">展示参数仅供参考,实际额度、利率及条件以机构审批为准。银行以文字简称标识。</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<BottomNav active="" />
|
<BottomNav active="" />
|
||||||
@@ -21,48 +24,68 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
import { FinanceProduct } from '@/services/finance.uts'
|
||||||
|
import { fetchMatches, API_BASE_URL } from '@/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 { fetchMatches } from '@/services/api.uts'
|
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import AppIcon from '@/components/AppIcon.uvue'
|
||||||
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
|
type MatchFilter = { id: string, label: string }
|
||||||
|
const filters: MatchFilter[] = [{ id: 'all', label: '综合排序' }, { id: 'amount', label: '额度优先' }, { id: 'rate', label: '低息优先' }]
|
||||||
const filter = ref('all')
|
const filter = ref('all')
|
||||||
const matches = ref<UTSJSONObject[]>([])
|
const matches = ref<FinanceProduct[]>([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
|
const isDemo = API_BASE_URL.length == 0
|
||||||
|
const sortedMatches = computed((): FinanceProduct[] => {
|
||||||
|
const items = matches.value.slice()
|
||||||
|
items.sort((a: FinanceProduct, b: FinanceProduct): number => {
|
||||||
|
if (filter.value == 'amount') return b.amount - a.amount
|
||||||
|
if (filter.value == 'rate') return a.rate - b.rate
|
||||||
|
return b.score - a.score
|
||||||
|
})
|
||||||
|
return items
|
||||||
|
})
|
||||||
const loadMatches = async () => {
|
const loadMatches = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
try { matches.value = await fetchMatches() } catch (error) { errorMessage.value = '匹配方案加载失败,请重试' }
|
try { matches.value = await fetchMatches() } catch (error) { errorMessage.value = '方案加载失败,请重试' }
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
onMounted(() => { loadMatches() })
|
onMounted(() => { loadMatches() })
|
||||||
const openProduct = (rank: any | null) => { if (rank != null) uni.navigateTo({ url: '/pages/product/product?id=' + rank.toString() }) }
|
const openProduct = (id: string) => { uni.navigateTo({ url: '/pages/product/product?id=' + id }) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.filter-bar { height: 92rpx; background-color: #fff; flex-direction: row; align-items: center; padding: 0 30rpx; border-bottom: 1rpx solid #e9eef6; }
|
.filter-bar { height: 88rpx; background-color: #fff; flex-direction: row; padding: 0 28rpx; border-bottom: 1rpx solid #e9eef6; }
|
||||||
.filter { flex: 1; height: 92rpx; align-items: center; justify-content: center; color: #758298; font-size: 24rpx; }
|
.filter { flex: 1; align-items: center; justify-content: center; }
|
||||||
.filter.active { color: #0868f7; font-weight: 700; border-bottom: 4rpx solid #0868f7; }
|
.active { border-bottom: 4rpx solid #0868f7; }
|
||||||
.match-scroll { }
|
.filter-label { color: #718098; font-size: 25rpx; }
|
||||||
.match-content { padding: 24rpx 28rpx 150rpx; }
|
.selected { color: #0868f7; font-weight: 700; }
|
||||||
.match-card { min-height: 310rpx; border-radius: 18rpx; margin-bottom: 22rpx; background-color: #fff; border: 1rpx solid #e4ebf5; padding: 26rpx 22rpx 22rpx; position: relative; flex-direction: row; box-shadow: 0 8rpx 24rpx rgba(24, 54, 95, .05); }
|
.match-content { padding: 24rpx 28rpx 90rpx; }
|
||||||
.rank { position: absolute; left: -7rpx; top: 20rpx; width: 48rpx; height: 48rpx; color: white; border-radius: 0 24rpx 24rpx 0; align-items: center; justify-content: center; font-weight: 800; }
|
.result-summary { color: #6f7f96; font-size: 22rpx; margin-bottom: 20rpx; }
|
||||||
.rank-1 { background-color: #ff8a13; }
|
.match-card { border-radius: 18rpx; margin-bottom: 20rpx; background-color: #fff; border: 1rpx solid #e4ebf5; padding: 24rpx; }
|
||||||
.rank-2 { background-color: #16b48a; }
|
.bank-row { flex-direction: row; align-items: center; }
|
||||||
.rank-3 { background-color: #ee3333; }
|
.bank-logo { width: 64rpx; height: 64rpx; border-radius: 20rpx; align-items: center; justify-content: center; margin-right: 16rpx; flex-shrink: 0; }
|
||||||
.bank-logo { width: 78rpx; height: 78rpx; border-radius: 50%; background-color: #e41e2b; color: white; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 800; margin-left: 22rpx; margin-right: 18rpx; }
|
.bank-initial { color: #fff; font-size: 30rpx; font-weight: 700; }
|
||||||
.match-main { flex: 1; }
|
.bank-copy { flex: 1; }
|
||||||
.bank-name { font-size: 28rpx; font-weight: 800; }
|
.bank-name { font-size: 27rpx; font-weight: 700; color: #20324d; }
|
||||||
.match-tag { color: #8794a7; font-size: 20rpx; margin-top: 8rpx; }
|
.match-tag { font-size: 21rpx; color: #7a899f; margin-top: 6rpx; }
|
||||||
.metrics { flex-direction: row; justify-content: space-between; margin-top: 30rpx; }
|
.match-score { color: #178461; font-size: 20rpx; }
|
||||||
.metrics-view { flex: 1; }
|
.metrics { flex-direction: row; margin-top: 26rpx; margin-bottom: 22rpx; }
|
||||||
.metric-value { font-size: 26rpx; font-weight: 800; color: #1c2a41; }
|
.metric { flex: 1; }
|
||||||
|
.term-metric { align-items: flex-end; }
|
||||||
|
.metric-value { color: #20324d; font-size: 35rpx; font-weight: 800; line-height: 44rpx; }
|
||||||
|
.metric-unit { font-size: 23rpx; }
|
||||||
.rate-value { color: #0868f7; }
|
.rate-value { color: #0868f7; }
|
||||||
.metric-label { margin-top: 7rpx; font-size: 18rpx; color: #95a0b1; }
|
.term-value { color: #20324d; font-size: 25rpx; font-weight: 600; line-height: 44rpx; }
|
||||||
.detail-button { position: absolute; right: 22rpx; bottom: 18rpx; width: 160rpx; height: 62rpx; line-height: 62rpx; border-radius: 12rpx; margin: 0; padding: 0; background-color: #0868f7; color: white; font-size: 22rpx; font-weight: 700; }
|
.metric-label { margin-top: 7rpx; font-size: 21rpx; color: #7c8aa0; }
|
||||||
.list-tip { text-align: center; color: #9ca8b8; font-size: 20rpx; line-height: 34rpx; margin: 12rpx 30rpx; }
|
.card-footer { border-top: 1rpx solid #edf1f6; padding-top: 18rpx; flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.state-panel { min-height: 260rpx; align-items: center; justify-content: center; color: #8793a6; font-size: 24rpx; }
|
.footer-hint { flex: 1; margin-right: 16rpx; font-size: 20rpx; line-height: 32rpx; color: #78889f; }
|
||||||
.error-panel { color: #e05a5a; }
|
.detail-link { flex-direction: row; align-items: center; flex-shrink: 0; }
|
||||||
.retry-button { margin-top: 20rpx; height: 64rpx; line-height: 64rpx; border-radius: 12rpx; color: #0868f7; background-color: #e8f2ff; font-size: 22rpx; }
|
.detail-label { font-size: 24rpx; color: #0868f7; font-weight: 600; margin-right: 6rpx; }
|
||||||
|
.list-tip { color: #7a899e; font-size: 21rpx; line-height: 34rpx; margin: 8rpx 8rpx 0; }
|
||||||
|
.state-panel { padding: 70rpx 20rpx; align-items: center; }
|
||||||
|
.state-text { font-size: 26rpx; line-height: 42rpx; color: #728197; text-align: center; }
|
||||||
|
.retry-button { margin-top: 20rpx; color: #0868f7; background-color: #e8f2ff; font-size: 24rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="消息中心" :back="false" />
|
<AppHeader title="消息中心" :back="false" />
|
||||||
<view class="message-tabs"><view v-for="item in tabs" :key="item" :class="tab == item ? 'message-tab active' : 'message-tab'" @click="tab=item"><text>{{ item }}</text></view></view>
|
<view class="message-tabs"><view v-for="item in tabs" :key="item" :class="tab == item ? 'message-tab active-tab' : 'message-tab'" @click="tab = item"><text :class="tab == item ? 'tab-text active-text' : 'tab-text'">{{ item }}</text></view></view>
|
||||||
<scroll-view scroll-y class="messages-scroll" :style="contentStyle(314, false)">
|
<scroll-view scroll-y :style="contentStyle(314, false)">
|
||||||
<view class="messages-content">
|
<view class="messages-content">
|
||||||
<view class="notice" v-for="item in notices" :key="item.title"><view :class="'notice-icon ' + item.color"><AppIcon :name="item.icon" :size="44" /></view><view class="notice-copy"><view class="notice-head"><text class="notice-title">{{ item.title }}</text><text class="notice-time">{{ item.time }}</text></view><text class="notice-desc">{{ item.desc }}</text></view><view v-if="item.unread" class="unread"></view></view>
|
<view class="message-summary"><text class="summary-text">{{ unreadCount > 0 ? unreadCount + ' 条未读消息' : '消息已全部阅读' }}</text><view class="read-all" @click="markAllRead"><text class="read-all-text">全部已读</text></view></view>
|
||||||
|
<view class="notice-list">
|
||||||
|
<view class="notice" v-for="(item, index) in filteredNotices" :key="item.id" :class="index == filteredNotices.length - 1 ? 'last-notice' : ''" @click="openNotice(item)">
|
||||||
|
<view class="notice-icon"><AppIcon :name="item.icon" :size="58" /><view v-if="item.unread" class="unread"></view></view>
|
||||||
|
<view class="notice-copy"><view class="notice-head"><text class="notice-title">{{ item.title }}</text><text class="notice-time">{{ item.time }}</text></view><text class="notice-desc">{{ item.desc }}</text><text class="notice-category">{{ item.category }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="message-footer">点击消息查看完整内容</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<BottomNav active="message" />
|
<BottomNav active="message" />
|
||||||
@@ -13,37 +20,62 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
|
||||||
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'
|
||||||
const tabs = ['全部', '系统通知', '审批进度', '活动消息']
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
|
const tabs: string[] = ['全部', '系统通知', '服务通知', '活动消息']
|
||||||
const tab = ref('全部')
|
const tab = ref('全部')
|
||||||
type Notice = { icon: string, color: string, title: string, desc: string, time: string, unread: boolean }
|
type Notice = { id: string, icon: string, category: string, title: string, desc: string, body: string, time: string, unread: boolean }
|
||||||
const notices: Notice[] = [
|
const notices = ref<Notice[]>([
|
||||||
{ icon: 'approval', color: 'blue', title: '审核进度更新', desc: '您的融资评估已完成,匹配到3个方案。', time: '10:30', unread: true },
|
{ id: 'assessment-guide', icon: 'approval', category: '服务通知', title: '融资评估使用指南', desc: '完成资料填写,了解自己的融资需求。', body: '从首页选择个人或企业融资入口,按步骤填写基本信息、经营或收入、资产与负债情况。提交前请核对资料,评估结果仅供参考,具体申请条件以机构审核为准。', time: '10:30', unread: true },
|
||||||
{ icon: 'payout', color: 'purple', title: '放款成功', desc: '您的招商银行闪电贷已成功放款。', time: '09:15', unread: false },
|
{ id: 'repayment-guide', icon: 'payout', category: '服务通知', title: '还款安排提醒', desc: '申请前,记得核对每期应还金额与日期。', body: '比较融资方案时,除利率外还应核对还款方式、每期应还金额与到期本金。正式签约后,请以机构提供的合同和还款计划为准,通过官方渠道查询账户状态。', time: '09:15', unread: false },
|
||||||
{ icon: 'gift', color: 'orange', title: '活动通知', desc: '邀请好友赢积分活动进行中。', time: '08:42', unread: false },
|
{ id: 'invite-guide', icon: 'gift', category: '活动消息', title: '邀请积分说明', desc: '邀请前先了解积分规则与适用范围。', body: '可在积分中心查看邀请入口与积分说明。积分发放条件、有效期及可用权益以活动页面公布的规则为准,请勿向他人承诺固定收益或贷款结果。', time: '08:42', unread: false },
|
||||||
{ icon: 'system-bell', color: 'green', title: '系统通知', desc: '隐私政策已更新,请及时查看。', time: '昨天', unread: false }
|
{ id: 'privacy-guide', icon: 'system-bell', category: '系统通知', title: '个人信息保护提醒', desc: '留意授权范围,保护账号与验证码。', body: '提交资料或授权数据前,请阅读相应说明,确认使用目的、数据范围和授权期限。不要向他人泄露账户密码、短信验证码或银行支付信息。', time: '昨天', unread: false }
|
||||||
]
|
])
|
||||||
|
const filteredNotices = computed((): Notice[] => tab.value == '全部' ? notices.value : notices.value.filter((item: Notice): boolean => item.category == tab.value))
|
||||||
|
const unreadCount = computed((): number => notices.value.filter((item: Notice): boolean => item.unread).length)
|
||||||
|
onShow(() => {
|
||||||
|
notices.value.forEach((item: Notice): void => {
|
||||||
|
if (uni.getStorageSync('finance.notice.read.' + item.id) == true) item.unread = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const markRead = (id: string): void => {
|
||||||
|
const item = notices.value.find((value: Notice): boolean => value.id == id)
|
||||||
|
if (item == null) return
|
||||||
|
item.unread = false
|
||||||
|
uni.setStorageSync('finance.notice.read.' + id, true)
|
||||||
|
}
|
||||||
|
const openNotice = (item: Notice): void => {
|
||||||
|
markRead(item.id)
|
||||||
|
uni.showModal({ title: item.title, content: item.body, showCancel: false, confirmText: '知道了' })
|
||||||
|
}
|
||||||
|
const markAllRead = (): void => {
|
||||||
|
notices.value.forEach((item: Notice): void => { markRead(item.id) })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.message-tabs { height: 88rpx; flex-direction: row; background-color: #fff; border-bottom: 1rpx solid #e6edf6; padding: 0 18rpx; }
|
.message-tabs { height: 88rpx; flex-direction: row; background-color: #ffffff; border-bottom: 1rpx solid #e8eef6; padding: 0 18rpx; }
|
||||||
.message-tab { flex: 1; align-items: center; justify-content: center; color: #7b8799; font-size: 22rpx; }
|
.message-tab { flex: 1; align-items: center; justify-content: center; border-bottom: 4rpx solid transparent; }
|
||||||
.message-tab.active { color: #0868f7; font-weight: 700; border-bottom: 4rpx solid #0868f7; }
|
.active-tab { border-bottom-color: #0868f7; }
|
||||||
.messages-scroll { }
|
.tab-text { color: #66758a; font-size: 25rpx; }
|
||||||
.messages-content { padding: 10rpx 28rpx 150rpx; }
|
.active-text { color: #0868f7; font-weight: 700; }
|
||||||
.notice { min-height: 150rpx; background-color: #fff; border-bottom: 1rpx solid #e8eef6; flex-direction: row; align-items: center; position: relative; }
|
.messages-content { padding: 14rpx 28rpx 90rpx; }
|
||||||
.notice-icon { width: 78rpx; height: 78rpx; border-radius: 25rpx; align-items: center; justify-content: center; color: #fff; font-size: 24rpx; font-weight: 800; margin-right: 20rpx; }
|
.message-summary { flex-direction: row; align-items: center; justify-content: space-between; min-height: 66rpx; }
|
||||||
.blue { background-color: #3b7df1; }
|
.summary-text { color: #66758a; font-size: 23rpx; }
|
||||||
.purple { background-color: #b74ae8; }
|
.read-all { padding: 14rpx 0 14rpx 24rpx; }
|
||||||
.orange { background-color: #ff8431; }
|
.read-all-text { color: #0868f7; font-size: 23rpx; }
|
||||||
.green { background-color: #18ae82; }
|
.notice-list { background-color: #ffffff; border-radius: 18rpx; padding: 0 20rpx; }
|
||||||
|
.notice { padding: 26rpx 0; border-bottom: 1rpx solid #e8eef6; flex-direction: row; align-items: flex-start; }
|
||||||
|
.last-notice { border-bottom-width: 0; }
|
||||||
|
.notice-icon { width: 70rpx; height: 70rpx; align-items: center; justify-content: center; margin-right: 18rpx; position: relative; flex-shrink: 0; }
|
||||||
.notice-copy { flex: 1; }
|
.notice-copy { flex: 1; }
|
||||||
.notice-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
.notice-head { flex-direction: row; justify-content: space-between; align-items: flex-start; }
|
||||||
.notice-title { font-size: 27rpx; font-weight: 700; }
|
.notice-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; flex: 1; }
|
||||||
.notice-time { color: #9ca7b7; font-size: 19rpx; }
|
.notice-time { color: #66758a; font-size: 22rpx; line-height: 40rpx; margin-left: 12rpx; flex-shrink: 0; }
|
||||||
.notice-desc { color: #778499; font-size: 22rpx; margin-top: 11rpx; }
|
.notice-desc { color: #66758a; font-size: 25rpx; line-height: 38rpx; margin-top: 10rpx; }
|
||||||
.unread { position: absolute; width: 13rpx; height: 13rpx; border-radius: 50%; background-color: #ff4d4f; right: 2rpx; top: 40rpx; }
|
.notice-category { color: #66758a; font-size: 22rpx; line-height: 32rpx; margin-top: 10rpx; }
|
||||||
|
.unread { position: absolute; width: 15rpx; height: 15rpx; border-radius: 8rpx; background-color: #f04452; border: 2rpx solid #ffffff; right: 0; top: 0; }
|
||||||
|
.message-footer { color: #66758a; font-size: 22rpx; text-align: center; margin-top: 28rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+49
-39
@@ -1,56 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="新闻" />
|
<AppHeader title="新闻" :back="false" />
|
||||||
<view class="news-tabs"><view v-for="item in tabs" :key="item" :class="tab == item ? 'news-tab active' : 'news-tab'" @click="tab=item"><text>{{ item }}</text></view></view>
|
<view class="news-tabs">
|
||||||
<scroll-view scroll-y class="news-scroll" :style="contentStyle(184, false)">
|
<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 class="news-content">
|
||||||
<view class="headline" @click="openArticle('headline')"><image class="headline-art" src="/static/mobile/images/news-city.png" mode="aspectFill"></image><view class="headline-copy"><text class="headline-tag">政策速递</text><text class="headline-title">普惠金融新政解读:小微企业迎来融资利好</text><text class="headline-date">2026-09-08</text></view></view>
|
<view v-if="tab == '推荐'" class="headline" @click="openArticle(headline.id)">
|
||||||
<view v-if="filteredArticles.length > 0"><view class="article" v-for="item in filteredArticles" :key="item.title" @click="openArticle(item.title)"><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 class="headline-copy"><text class="headline-tag">政策观察</text><text class="headline-title">{{ headline.title }}</text><text class="headline-date">{{ headline.date }}</text></view>
|
||||||
<view v-else class="empty-state"><AppIcon name="search" :size="48" muted /><text class="empty-state-text">暂无该分类新闻</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>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
<BottomNav active="news" />
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
import { newsArticles, NewsArticle } from '@/services/content.uts'
|
||||||
import AppHeader from '@/components/AppHeader.uvue'
|
import AppHeader from '@/components/AppHeader.uvue'
|
||||||
import AppIcon from '@/components/AppIcon.uvue'
|
import BottomNav from '@/components/BottomNav.uvue'
|
||||||
const tabs = ['推荐', '企业融资', '个人融资', '政策解读']
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
|
const tabs: string[] = ['推荐', '企业融资', '个人融资', '政策解读']
|
||||||
const tab = ref('推荐')
|
const tab = ref('推荐')
|
||||||
type NewsSummary = { title: string, summary: string, category: string, date: string, image: string }
|
const headline: NewsArticle = newsArticles[0]
|
||||||
const articles: NewsSummary[] = [
|
const filteredArticles = computed((): NewsArticle[] => {
|
||||||
{ title: '如何提高个人贷款通过率', summary: '从征信、负债和收入三个维度做好申请准备。', category: '个人融资', date: '2026-09-05', image: '/static/mobile/images/news-calculator.png' },
|
if (tab.value == '推荐') return newsArticles.filter((item: NewsArticle): boolean => item.id != headline.id)
|
||||||
{ title: '银行最新贷款产品汇总', summary: '本月值得关注的低息信用融资产品。', category: '个人融资', date: '2026-09-03', image: '/static/mobile/images/news-city.png' },
|
return newsArticles.filter((item: NewsArticle): boolean => item.category == tab.value)
|
||||||
{ title: '小微企业融资材料清单', summary: '一次准备齐全,减少补充资料时间。', category: '企业融资', date: '2026-08-30', image: '/static/mobile/images/news-business.png' },
|
|
||||||
{ title: '普惠金融新政解读', summary: '了解小微企业融资支持政策和申请重点。', category: '政策解读', date: '2026-08-28', image: '/static/mobile/images/news-policy.png' }
|
|
||||||
]
|
|
||||||
const filteredArticles = computed(() => {
|
|
||||||
if (tab.value == '推荐') return articles
|
|
||||||
return articles.filter((item) => item.category == tab.value)
|
|
||||||
})
|
})
|
||||||
const openArticle = (title: string) => { uni.navigateTo({ url: '/pages/article/article?title=' + title }) }
|
const openArticle = (id: string): void => { uni.navigateTo({ url: '/pages/article/article?id=' + id }) }
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.news-tabs { height: 88rpx; flex-direction: row; align-items: center; background-color: #fff; border-bottom: 1rpx solid #e8eef6; padding: 0 22rpx; }
|
.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; color: #78869a; font-size: 23rpx; }
|
.news-tab { flex: 1; height: 88rpx; justify-content: center; align-items: center; border-bottom: 4rpx solid transparent; }
|
||||||
.news-tab.active { color: #0868f7; font-weight: 700; border-bottom: 4rpx solid #0868f7; }
|
.active-tab { border-bottom-color: #0868f7; }
|
||||||
.news-scroll { }
|
.tab-text { color: #66758a; font-size: 26rpx; }
|
||||||
.news-content { padding: 24rpx 28rpx 70rpx; }
|
.active-text { color: #0868f7; font-weight: 700; }
|
||||||
.headline { height: 260rpx; padding: 28rpx; border-radius: 18rpx; background-color: #0b2a66; flex-direction: row; overflow: hidden; position: relative; }
|
.news-content { padding: 24rpx 28rpx 90rpx; }
|
||||||
.headline-copy { width: 65%; z-index: 2; }
|
.headline { min-height: 256rpx; border-radius: 20rpx; background-color: #123c7b; flex-direction: row; overflow: hidden; }
|
||||||
.headline-tag { color: #75caff; font-size: 21rpx; font-weight: 700; }
|
.headline-copy { flex: 1; padding: 26rpx 20rpx 26rpx 26rpx; }
|
||||||
.headline-title { color: #fff; font-size: 31rpx; line-height: 45rpx; font-weight: 800; margin-top: 16rpx; }
|
.headline-tag { color: #b8ddff; font-size: 22rpx; font-weight: 600; }
|
||||||
.headline-date { color: #bfd7ff; font-size: 20rpx; margin-top: 18rpx; }
|
.headline-title { color: #ffffff; font-size: 31rpx; line-height: 45rpx; font-weight: 700; margin-top: 12rpx; }
|
||||||
.headline-art { position: absolute; right: 0; top: 0; width: 56%; height: 100%; opacity: .78; }
|
.headline-date { color: #c4d7ed; font-size: 22rpx; margin-top: 16rpx; }
|
||||||
.article { min-height: 185rpx; padding: 28rpx 0; flex-direction: row; border-bottom: 1rpx solid #e6edf6; }
|
.headline-art { width: 212rpx; height: 282rpx; flex-shrink: 0; align-self: center; }
|
||||||
.article-copy { flex: 1; padding-right: 20rpx; }
|
.list-head { flex-direction: row; justify-content: space-between; align-items: center; margin: 30rpx 0 16rpx; }
|
||||||
.article-title { color: #1b2941; font-size: 28rpx; line-height: 41rpx; font-weight: 700; }
|
.list-title { color: #172640; font-size: 30rpx; font-weight: 700; }
|
||||||
.article-summary { color: #778499; font-size: 22rpx; line-height: 34rpx; margin-top: 10rpx; }
|
.list-count { color: #66758a; font-size: 22rpx; }
|
||||||
.article-meta { color: #a0aaba; font-size: 19rpx; margin-top: 12rpx; }
|
.article-list { background-color: #ffffff; border-radius: 18rpx; padding: 0 22rpx; }
|
||||||
.article-art { width: 150rpx; height: 120rpx; border-radius: 12rpx; flex-shrink: 0; }
|
.article { padding: 26rpx 0; flex-direction: row; border-bottom: 1rpx solid #e8eef6; align-items: flex-start; }
|
||||||
.empty-state { min-height: 250rpx; align-items: center; justify-content: center; color: #94a0b1; font-size: 23rpx; }
|
.last-article { border-bottom-width: 0; }
|
||||||
.empty-state-text { margin-top: 15rpx; }
|
.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>
|
</style>
|
||||||
|
|||||||
+114
-37
@@ -1,12 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="积分中心" />
|
<AppHeader title="积分中心" />
|
||||||
<scroll-view scroll-y class="points-scroll" :style="contentStyle(96, false)">
|
<scroll-view scroll-y :style="contentStyle(96, false)">
|
||||||
<view class="points-content">
|
<view class="points-content">
|
||||||
<view class="points-hero"><text class="points-label">我的积分</text><text class="points-number">2680</text><button @click="showRules" class="points-hero-button">积分规则</button><view class="points-shine"></view></view>
|
<view class="points-hero">
|
||||||
<view class="task-grid"><view class="task" v-for="item in tasks" :key="item.name" @click="doTask(item.name)"><view class="task-icon"><AppIcon :name="item.icon" :size="42" /></view><text class="task-name">{{ item.name }}</text><text class="task-score">+{{ item.score }}</text></view></view>
|
<view class="points-hero-top"><text class="points-label">我的体验积分</text><button @click="showRules" class="points-hero-button">积分规则</button></view>
|
||||||
<view class="invite-panel"><text class="section-title">邀请有礼</text><text class="invite-copy">每邀请1位好友,双方都可获得积分</text><view class="invite-code"><text>我的邀请码</text><text class="code">AI2680</text><text class="copy" @click="copyCode">复制</text></view><button class="primary-button invite-button" @click="invite">立即邀请</button></view>
|
<text class="points-number">{{ totalPoints }}</text><text class="points-note">本地体验 · 仅在本机记录</text>
|
||||||
<view class="records"><view class="record-head"><text class="section-title">积分明细</text><text class="more">全部 ›</text></view><view class="record" v-for="item in records" :key="item.name"><view><text class="record-name">{{ item.name }}</text><text class="record-date">{{ item.date }}</text></view><text class="record-score">+{{ item.score }}</text></view></view>
|
</view>
|
||||||
|
<view class="task-grid">
|
||||||
|
<view class="task" v-for="item in tasks" :key="item.id" @click="doTask(item.id)">
|
||||||
|
<AppIcon :name="item.icon" :size="52" />
|
||||||
|
<text class="task-name">{{ item.id == 'checkin' && signedToday ? '已签到' : item.name }}</text>
|
||||||
|
<text :class="item.id == 'checkin' && signedToday ? 'task-caption completed-caption' : 'task-caption'">{{ item.id == 'checkin' && signedToday ? '明天再来' : item.caption }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="invite-panel">
|
||||||
|
<text class="panel-title">与好友一起了解融资</text><text class="invite-copy">分享实用知识,提前做好申请准备。</text>
|
||||||
|
<view class="invite-code"><text class="code-label">体验邀请码</text><text class="code">AI2680</text><view class="copy-action" @click="copyCode"><text class="copy-text">复制</text></view></view>
|
||||||
|
<button class="primary-button invite-button" @click="invite">复制邀请文案</button><text class="invite-note">当前为本地体验,复制或分享不会发放积分。</text>
|
||||||
|
</view>
|
||||||
|
<view class="records">
|
||||||
|
<view class="record-head" @click="recordsExpanded = !recordsExpanded"><text class="panel-title">积分明细</text><view class="record-toggle"><text class="more">{{ recordsExpanded ? '收起' : '展开' }}</text><AppIcon name="chevron-right" :size="26" :class="recordsExpanded ? 'record-arrow expanded-arrow' : 'record-arrow'" muted /></view></view>
|
||||||
|
<view v-if="recordsExpanded">
|
||||||
|
<view v-if="records.length == 0" class="empty-records"><text class="empty-record-title">还没有积分记录</text><text class="empty-record-copy">完成今日签到,获得 10 体验积分。</text></view>
|
||||||
|
<view class="record" v-for="(item, index) in records" :key="item.date" :class="index == records.length - 1 ? 'last-record' : ''"><view class="record-copy"><text class="record-name">每日签到</text><text class="record-date">{{ item.date }} · 本地体验</text></view><text class="record-score">+{{ item.score }}</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
@@ -14,42 +33,100 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { 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'
|
||||||
type PointsTask = { icon: string, name: string, score: number }
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
const tasks: PointsTask[] = [{ icon: 'calendar-check', name: '签到', score: 10 }, { icon: 'share', name: '分享', score: 20 }, { icon: 'edit-file', name: '完善任务', score: 50 }, { icon: 'gift', name: '邀请好友', score: 100 }]
|
type PointsTask = { id: string, icon: string, name: string, caption: string }
|
||||||
type PointsRecord = { name: string, date: string, score: number }
|
type PointsRecord = { date: string, score: number }
|
||||||
const records: PointsRecord[] = [{ name: '每日签到', date: '今天 09:20', score: 10 }, { name: '完成融资评估', date: '昨天 18:36', score: 50 }, { name: '邀请好友', date: '09-02 14:20', score: 100 }]
|
const tasks: PointsTask[] = [
|
||||||
const showRules = () => { uni.showModal({ title: '积分规则', content: '签到、评估、分享和邀请好友均可获得积分。', showCancel: false }) }
|
{ id: 'checkin', icon: 'calendar-check', name: '签到', caption: '每日 +10' },
|
||||||
const doTask = (name: string) => { uni.showToast({ title: name + '任务入口', icon: 'none' }) }
|
{ id: 'share', icon: 'share', name: '分享', caption: '复制文案' },
|
||||||
const copyCode = () => { uni.setClipboardData({ data: 'AI2680' }) }
|
{ id: 'profile', icon: 'edit-file', name: '完善资料', caption: '查看档案' },
|
||||||
const invite = () => { uni.showToast({ title: '分享入口已预留', icon: 'none' }) }
|
{ id: 'invite', icon: 'gift', name: '邀请好友', caption: '复制邀请' }
|
||||||
|
]
|
||||||
|
const signDays = ref<string[]>([])
|
||||||
|
const today = ref('')
|
||||||
|
const recordsExpanded = ref(true)
|
||||||
|
const totalPoints = computed((): number => signDays.value.length * 10)
|
||||||
|
const signedToday = computed((): boolean => signDays.value.includes(today.value))
|
||||||
|
const records = computed((): PointsRecord[] => signDays.value.map((date: string): PointsRecord => ({ date, score: 10 })))
|
||||||
|
const localDate = (): string => {
|
||||||
|
const date = new Date()
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
return date.getFullYear() + '-' + (month < 10 ? '0' : '') + month + '-' + (day < 10 ? '0' : '') + day
|
||||||
|
}
|
||||||
|
const restore = (): void => {
|
||||||
|
today.value = localDate()
|
||||||
|
const stored = uni.getStorageSync('finance.points.checkin.days')
|
||||||
|
if (typeof stored != 'string' || stored == '') { signDays.value = []; return }
|
||||||
|
const restored = (stored as string).split(',')
|
||||||
|
const unique: string[] = []
|
||||||
|
restored.forEach((day: string): void => { if (/^\d{4}-\d{2}-\d{2}$/.test(day) && !unique.includes(day)) unique.push(day) })
|
||||||
|
signDays.value = unique
|
||||||
|
}
|
||||||
|
onShow(() => { restore() })
|
||||||
|
const checkIn = (): void => {
|
||||||
|
restore()
|
||||||
|
if (signedToday.value) { uni.showToast({ title: '今天已签到,明天再来', icon: 'none' }); return }
|
||||||
|
const next = signDays.value.slice()
|
||||||
|
next.unshift(today.value)
|
||||||
|
uni.setStorageSync('finance.points.checkin.days', next.join(','))
|
||||||
|
signDays.value = next
|
||||||
|
recordsExpanded.value = true
|
||||||
|
uni.showToast({ title: '签到完成,体验积分 +10', icon: 'none' })
|
||||||
|
}
|
||||||
|
const showRules = (): void => {
|
||||||
|
uni.showModal({ title: '体验积分规则', content: '当前积分仅供本地体验,不代表真实账户权益。每天可签到一次,每次增加 10 体验积分。分享、邀请和完善资料暂不发放积分;清除应用数据后,本地记录也会清除。', showCancel: false, confirmText: '知道了' })
|
||||||
|
}
|
||||||
|
const copyText = (value: string, message: string): void => {
|
||||||
|
uni.setClipboardData({ data: value, success: () => { uni.showToast({ title: message, icon: 'none' }) }, fail: () => { uni.showToast({ title: '复制失败,请稍后重试', icon: 'none' }) } })
|
||||||
|
}
|
||||||
|
const copyCode = (): void => { copyText('AI2680', '体验邀请码已复制') }
|
||||||
|
const invite = (): void => { copyText('邀请你一起了解 AI融资:学习融资知识,梳理申请资料,提前做好融资准备。体验邀请码:AI2680。', '邀请文案已复制,可自行分享') }
|
||||||
|
const doTask = (id: string): void => {
|
||||||
|
if (id == 'checkin') { checkIn(); return }
|
||||||
|
if (id == 'profile') { uni.navigateTo({ url: '/pages/archive/archive' }); return }
|
||||||
|
if (id == 'invite') { invite(); return }
|
||||||
|
copyText('分享一个融资准备方法:先梳理信用、收入与负债,再比较适合自己的产品。可以在 AI融资的融资课堂中学习相关知识。', '分享文案已复制,可自行分享')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.points-scroll { }
|
.points-content { padding: 24rpx 28rpx 48rpx; }
|
||||||
.points-content { padding: 26rpx 28rpx 70rpx; }
|
.points-hero { padding: 28rpx; border-radius: 20rpx; background-color: #5148d8; }
|
||||||
.points-hero { height: 260rpx; padding: 36rpx; border-radius: 20rpx; color: white; background: linear-gradient(125deg, #5140e8, #9c56f6); overflow: hidden; position: relative; }
|
.points-hero-top { flex-direction: row; align-items: center; justify-content: space-between; }
|
||||||
.points-label { font-size: 24rpx; color: #e7e1ff; }
|
.points-label { font-size: 26rpx; color: #f0efff; }
|
||||||
.points-number { font-size: 72rpx; line-height: 90rpx; font-weight: 900; }
|
.points-number { font-size: 74rpx; line-height: 94rpx; font-weight: 700; color: #ffffff; margin-top: 12rpx; }
|
||||||
.points-hero-button { position: absolute; right: 28rpx; top: 32rpx; width: 150rpx; height: 58rpx; line-height: 58rpx; padding: 0; margin: 0; border-radius: 29rpx; color: #6348de; background-color: #fff; font-size: 21rpx; }
|
.points-note { color: #e5e1ff; font-size: 22rpx; line-height: 34rpx; margin-top: 4rpx; }
|
||||||
.points-shine { position: absolute; width: 260rpx; height: 260rpx; border-radius: 50%; background-color: rgba(255,255,255,.1); right: -60rpx; bottom: -110rpx; }
|
.points-hero-button { width: 144rpx; height: 56rpx; line-height: 56rpx; padding: 0; margin: 0; border-radius: 28rpx; color: #5148d8; background-color: #ffffff; font-size: 23rpx; }
|
||||||
.task-grid { height: 180rpx; flex-direction: row; background-color: #fff; border: 1rpx solid #e5ecf5; border-radius: 18rpx; margin-top: 22rpx; }
|
.task-grid { min-height: 186rpx; flex-direction: row; background-color: #ffffff; border-radius: 18rpx; margin-top: 20rpx; }
|
||||||
.task { flex: 1; align-items: center; justify-content: center; }
|
.task { flex: 1; align-items: center; justify-content: center; padding: 24rpx 0; }
|
||||||
.task-icon { width: 56rpx; height: 56rpx; border-radius: 18rpx; color: #0868f7; background-color: #e9f2ff; align-items: center; justify-content: center; font-size: 22rpx; font-weight: 800; }
|
.task-name { margin-top: 14rpx; font-size: 24rpx; color: #172640; font-weight: 600; }
|
||||||
.task-name { margin-top: 10rpx; font-size: 21rpx; color: #3d4b61; }
|
.task-caption { margin-top: 7rpx; font-size: 22rpx; color: #66758a; }
|
||||||
.task-score { margin-top: 4rpx; font-size: 18rpx; color: #8e9aab; }
|
.completed-caption { color: #168454; }
|
||||||
.invite-panel, .records { margin-top: 24rpx; padding: 28rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e5ecf5; }
|
.invite-panel, .records { margin-top: 22rpx; padding: 24rpx; border-radius: 18rpx; background-color: #ffffff; }
|
||||||
.invite-copy { margin-top: 12rpx; color: #7a8799; font-size: 22rpx; }
|
.panel-title { color: #172640; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
|
||||||
.invite-code { height: 92rpx; padding: 0 22rpx; margin-top: 24rpx; background-color: #f3f6fa; border-radius: 13rpx; flex-direction: row; align-items: center; color: #788599; font-size: 22rpx; }
|
.invite-copy { margin-top: 12rpx; color: #66758a; font-size: 25rpx; line-height: 38rpx; }
|
||||||
.code { flex: 1; margin-left: 22rpx; color: #1b2940; font-size: 30rpx; font-weight: 800; }
|
.invite-code { min-height: 92rpx; padding: 12rpx 18rpx; margin-top: 22rpx; background-color: #f4f7fb; border-radius: 12rpx; flex-direction: row; align-items: center; }
|
||||||
.copy { color: #0868f7; }
|
.code-label { color: #66758a; font-size: 23rpx; }
|
||||||
|
.code { flex: 1; margin-left: 16rpx; color: #172640; font-size: 29rpx; font-weight: 700; }
|
||||||
|
.copy-action { padding: 12rpx 0 12rpx 16rpx; }
|
||||||
|
.copy-text { color: #0868f7; font-size: 24rpx; }
|
||||||
.invite-button { margin-top: 22rpx; }
|
.invite-button { margin-top: 22rpx; }
|
||||||
.record-head { flex-direction: row; justify-content: space-between; align-items: center; }
|
.invite-note { color: #66758a; font-size: 22rpx; line-height: 34rpx; margin-top: 14rpx; }
|
||||||
.more { color: #0868f7; font-size: 22rpx; }
|
.record-head { flex-direction: row; justify-content: space-between; align-items: center; min-height: 52rpx; }
|
||||||
.record { min-height: 100rpx; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.record-toggle { flex-direction: row; align-items: center; padding-left: 20rpx; }
|
||||||
.record-name { font-size: 24rpx; color: #29374d; }
|
.more { color: #0868f7; font-size: 23rpx; margin-right: 6rpx; }
|
||||||
.record-date { color: #9ba6b6; font-size: 19rpx; margin-top: 8rpx; }
|
.record-arrow { transform: rotate(90deg); }
|
||||||
.record-score { color: #0daa74; font-size: 27rpx; font-weight: 700; }
|
.expanded-arrow { transform: rotate(-90deg); }
|
||||||
|
.record { min-height: 116rpx; padding: 18rpx 0; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
|
.last-record { border-bottom-width: 0; }
|
||||||
|
.record-copy { flex: 1; }
|
||||||
|
.record-name { font-size: 27rpx; color: #172640; }
|
||||||
|
.record-date { color: #66758a; font-size: 22rpx; line-height: 34rpx; margin-top: 8rpx; }
|
||||||
|
.record-score { color: #168454; font-size: 30rpx; font-weight: 700; margin-left: 16rpx; }
|
||||||
|
.empty-records { padding: 28rpx 0 12rpx; }
|
||||||
|
.empty-record-title { color: #66758a; font-size: 26rpx; }
|
||||||
|
.empty-record-copy { color: #66758a; font-size: 23rpx; line-height: 36rpx; margin-top: 10rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+76
-34
@@ -1,53 +1,95 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="产品详情"><text class="share">分享</text></AppHeader>
|
<AppHeader title="产品详情"><text v-if="available" class="share" @click="shareProduct">分享</text></AppHeader>
|
||||||
<scroll-view scroll-y class="product-scroll" :style="contentStyle(220, false)">
|
<scroll-view scroll-y :style="contentStyle(224, false)">
|
||||||
<view class="product-content">
|
<view class="product-content">
|
||||||
<view class="bank-head"><view class="bank-mark"><AppIcon name="building" :size="42" /></view><view><text class="product-name">招商银行·闪电贷</text><text class="product-tags">个人信用贷 线上申请 快速放款</text></view></view>
|
<view v-if="loading" class="state"><text class="state-text">正在加载产品…</text></view>
|
||||||
<view class="product-metrics"><view class="product-metrics-view"><text class="metric-label">最高额度</text><text class="amount">50万</text></view><view class="product-metrics-view"><text class="metric-label">年利率</text><text class="rate">3.85%</text></view><view class="product-metrics-view product-metrics-view-last-child"><text class="metric-label">贷款期限</text><text class="term">12-36期</text></view></view>
|
<view v-else-if="!available" class="state"><text class="state-title">{{ loadFailed ? '产品加载失败' : '暂未找到该产品' }}</text><text class="state-text">请重试或返回方案列表选择其他产品</text><button class="retry-button" @click="loadProduct">重新加载</button></view>
|
||||||
<view class="detail-section"><text class="section-title">产品优势</text><view class="advantage-grid"><view class="advantage" v-for="item in advantages" :key="item"><AppIcon name="check-circle" :size="30" /><text>{{ item }}</text></view></view></view>
|
<view v-else>
|
||||||
<view class="detail-section"><text class="section-title">申请条件</text><view class="condition"><text>年龄要求</text><text class="condition-text-last-child">22-55周岁</text></view><view class="condition"><text>工作要求</text><text class="condition-text-last-child">现单位满6个月</text></view><view class="condition"><text>信用要求</text><text class="condition-text-last-child">近两年无严重逾期</text></view></view>
|
<view class="bank-head"><view class="bank-mark" :style="'background-color:' + product.color"><text class="bank-initial">{{ product.initials }}</text></view><view class="bank-copy"><text class="product-name">{{ product.bank }}</text><text class="product-tags">个人信用融资 · {{ isDemo ? '示例产品' : '产品信息' }}</text></view></view>
|
||||||
<view class="detail-section"><text class="section-title">所需材料</text><text class="material">身份证 · 银行卡 · 收入证明</text></view>
|
<view class="product-metrics"><view class="metric"><text class="metric-label">最高额度</text><text class="amount">{{ product.amount }}万</text></view><view class="metric"><text class="metric-label">年化利率(起)</text><text class="rate">{{ product.rate.toFixed(2) }}%</text></view><view class="metric last-metric"><text class="metric-label">贷款期限</text><text class="term">{{ product.term }}</text></view></view>
|
||||||
|
<view class="detail-section"><text class="section-title">产品优势</text><view class="advantage-grid"><view class="advantage" v-for="item in product.advantages" :key="item"><AppIcon name="check-circle" :size="30" /><text class="advantage-label">{{ item }}</text></view></view></view>
|
||||||
|
<view class="detail-section"><text class="section-title">申请条件</text><view class="condition"><text class="condition-label">年龄要求</text><text class="condition-value">{{ product.age }}</text></view><view class="condition"><text class="condition-label">工作要求</text><text class="condition-value">{{ product.work }}</text></view><view class="condition"><text class="condition-label">信用要求</text><text class="condition-value">{{ product.credit }}</text></view></view>
|
||||||
|
<view class="detail-section"><text class="section-title">所需材料</text><text class="material">{{ product.materials }}</text></view>
|
||||||
|
<text class="product-note">额度、利率及申请条件仅供参考,以机构实际审批为准。{{ isDemo ? '当前可体验申请准备流程,尚未向机构提交资料。' : '' }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<view class="apply-bar" :style="bottomStyle(18)"><button class="primary-button apply-button" @click="apply">立即申请</button></view>
|
<view v-if="available" class="apply-bar" :style="bottomStyle(18)"><button class="primary-button apply-button" @click="apply">准备申请</button></view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
import { FinanceProduct, demoProducts } from '@/services/finance.uts'
|
||||||
|
import { fetchProduct, API_BASE_URL } from '@/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 advantages = ['线上申请', '额度高', '审批快速', '利率低', '无抵押', '无担保']
|
const { bottomStyle, contentStyle, pageStyle } = usePageLayout()
|
||||||
const apply = () => { uni.showModal({ title: '申请已记录', content: '机构申请接口已预留,接入后将提交您的授权资料。', showCancel: false }) }
|
const product = ref<FinanceProduct>(demoProducts[0])
|
||||||
|
const available = ref(false)
|
||||||
|
const productId = ref('')
|
||||||
|
const loading = ref(true)
|
||||||
|
const loadFailed = ref(false)
|
||||||
|
const isDemo = API_BASE_URL.length == 0
|
||||||
|
const loadProduct = async () => {
|
||||||
|
loading.value = true
|
||||||
|
loadFailed.value = false
|
||||||
|
available.value = false
|
||||||
|
try {
|
||||||
|
const selected = await fetchProduct(productId.value)
|
||||||
|
if (selected != null) { product.value = selected; available.value = true }
|
||||||
|
} catch (error) { loadFailed.value = true }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
onLoad((options) => {
|
||||||
|
productId.value = options?.['id'] ?? ''
|
||||||
|
loadProduct()
|
||||||
|
})
|
||||||
|
const shareProduct = () => {
|
||||||
|
const item = product.value
|
||||||
|
if (!available.value) return
|
||||||
|
uni.setClipboardData({ data: item.bank + '|最高' + item.amount + '万元|年化利率' + item.rate.toFixed(2) + '%起|' + item.term + '。展示信息仅供参考,以机构审批为准。' })
|
||||||
|
}
|
||||||
|
const apply = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '准备申请资料',
|
||||||
|
content: '先完善融资档案,核对申请所需材料。当前尚未开通机构申请,资料不会提交至银行。',
|
||||||
|
confirmText: '完善档案',
|
||||||
|
success: (result) => { if (result.confirm) uni.navigateTo({ url: '/pages/archive/archive' }) }
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.share { color: #0868f7; font-size: 23rpx; }
|
.share { color: #0868f7; font-size: 25rpx; padding: 16rpx 0 16rpx 16rpx; }
|
||||||
.product-scroll { }
|
.product-content { padding: 24rpx 28rpx 60rpx; }
|
||||||
.product-content { padding: 26rpx 28rpx 130rpx; }
|
.bank-head { padding: 26rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; align-items: center; }
|
||||||
.bank-head { min-height: 150rpx; padding: 26rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; align-items: center; }
|
.bank-mark { width: 80rpx; height: 80rpx; border-radius: 24rpx; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||||
.bank-mark { width: 84rpx; height: 84rpx; border-radius: 50%; background-color: #e8192c; align-items: center; justify-content: center; margin-right: 22rpx; }
|
.bank-initial { color: #fff; font-size: 36rpx; font-weight: 700; }
|
||||||
/* #ifndef APP */
|
.bank-copy { flex: 1; }
|
||||||
.bank-mark .app-icon { filter: brightness(0) invert(1); }
|
.product-name { font-size: 30rpx; font-weight: 700; color: #20324d; }
|
||||||
/* #endif */
|
.product-tags { color: #75859b; font-size: 22rpx; margin-top: 10rpx; }
|
||||||
|
.product-metrics { margin-top: 20rpx; padding: 30rpx 12rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; }
|
||||||
.product-name { font-size: 30rpx; font-weight: 800; }
|
.metric { flex: 1; align-items: center; border-right: 1rpx solid #edf1f6; }
|
||||||
.product-tags { color: #8794a8; font-size: 20rpx; margin-top: 11rpx; }
|
.last-metric { border-right: 0; }
|
||||||
.product-metrics { min-height: 180rpx; margin-top: 20rpx; padding: 34rpx 15rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e6edf6; flex-direction: row; }
|
.metric-label { color: #75859b; font-size: 21rpx; }
|
||||||
.product-metrics-view { flex: 1; align-items: center; border-right: 1rpx solid #edf1f6; }
|
.amount, .rate, .term { margin-top: 14rpx; font-size: 36rpx; line-height: 46rpx; font-weight: 800; color: #20324d; }
|
||||||
.product-metrics-view-last-child { border-right: 0; }
|
|
||||||
.metric-label { color: #8491a5; font-size: 21rpx; }
|
|
||||||
.amount, .rate, .term { margin-top: 16rpx; font-size: 34rpx; font-weight: 800; }
|
|
||||||
.rate { color: #0868f7; }
|
.rate { color: #0868f7; }
|
||||||
.detail-section { margin-top: 22rpx; padding: 28rpx; background-color: #fff; border-radius: 18rpx; border: 1rpx solid #e6edf6; }
|
.term { font-size: 27rpx; }
|
||||||
.advantage-grid { margin-top: 25rpx; flex-direction: row; flex-wrap: wrap; }
|
.detail-section { margin-top: 20rpx; padding: 26rpx; background-color: #fff; border-radius: 18rpx; border: 1rpx solid #e6edf6; }
|
||||||
.advantage { width: 50%; flex-direction: row; align-items: center; margin-bottom: 22rpx; font-size: 24rpx; color: #435168; }
|
.advantage-grid { margin-top: 24rpx; flex-direction: row; flex-wrap: wrap; }
|
||||||
.advantage .app-icon { margin-right: 12rpx; }
|
.advantage { width: 50%; flex-direction: row; align-items: center; margin-bottom: 16rpx; }
|
||||||
.condition { height: 80rpx; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #edf1f6; color: #667489; font-size: 24rpx; }
|
.advantage-label { margin-left: 12rpx; font-size: 25rpx; color: #435168; }
|
||||||
.condition-text-last-child { color: #1e2c43; font-weight: 600; }
|
.condition { min-height: 78rpx; padding: 14rpx 0; flex-direction: row; justify-content: space-between; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.material { margin-top: 22rpx; font-size: 24rpx; color: #657389; }
|
.condition-label { color: #748399; font-size: 24rpx; flex-shrink: 0; margin-right: 20rpx; }
|
||||||
|
.condition-value { flex: 1; text-align: right; color: #273b54; font-size: 24rpx; line-height: 36rpx; }
|
||||||
|
.material { margin-top: 18rpx; font-size: 24rpx; line-height: 38rpx; color: #53667f; }
|
||||||
|
.product-note { margin: 22rpx 6rpx 0; color: #7a899e; font-size: 21rpx; line-height: 34rpx; }
|
||||||
.apply-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 18rpx 28rpx; background-color: #fff; border-top: 1rpx solid #e7edf6; }
|
.apply-bar { position: fixed; left: 0; right: 0; bottom: 0; padding: 18rpx 28rpx; background-color: #fff; border-top: 1rpx solid #e7edf6; }
|
||||||
.apply-button { margin: 0; }
|
.apply-button { margin: 0; }
|
||||||
|
.state { padding: 80rpx 24rpx; align-items: center; }
|
||||||
|
.state-title { font-size: 30rpx; color: #20324d; font-weight: 600; margin-bottom: 14rpx; }
|
||||||
|
.state-text { font-size: 25rpx; line-height: 40rpx; text-align: center; color: #75859b; }
|
||||||
|
.retry-button { margin-top: 24rpx; color: #0868f7; background-color: #e8f2ff; font-size: 25rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+59
-46
@@ -1,14 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page" :style="pageStyle()">
|
<view class="page" :style="pageStyle()">
|
||||||
<AppHeader title="个人中心" :back="false" />
|
<AppHeader title="个人中心" :back="false" />
|
||||||
<scroll-view scroll-y class="profile-scroll" :style="contentStyle(226, false)">
|
<scroll-view scroll-y :style="contentStyle(226, false)">
|
||||||
<view class="profile-content">
|
<view class="profile-content">
|
||||||
<view class="profile-card"><view class="avatar"><text>张</text></view><view class="profile-main"><text class="profile-name">张三</text><text class="profile-phone">138****8888</text></view><view class="verified"><text>已认证</text></view></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-row-view"><text class="stat-number">1</text><text class="stat-label">申请中</text></view><view class="stat-row-view"><text class="stat-number">3</text><text class="stat-label">已匹配</text></view><view class="stat-row-view stat-row-view-last-child"><text class="stat-number">2680</text><text class="stat-label">积分</text></view></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="menu-list">
|
<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-row" v-for="item in menus" :key="item.name" @click="open(item.url, item.name)"><view :class="'menu-icon ' + item.color"><AppIcon :name="item.icon" :size="40" /></view><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" muted /></view>
|
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
|
||||||
</view>
|
<text class="version-note">AI融资 · 让融资更简单</text>
|
||||||
<button class="logout" @click="logout">退出登录</button>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
<BottomNav active="profile" />
|
<BottomNav active="profile" />
|
||||||
@@ -17,49 +16,63 @@
|
|||||||
|
|
||||||
<script setup lang="uts">
|
<script setup lang="uts">
|
||||||
import { usePageLayout } from '@/services/layout.uts'
|
import { usePageLayout } from '@/services/layout.uts'
|
||||||
const { contentStyle, pageStyle } = usePageLayout()
|
|
||||||
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'
|
||||||
import { clearSession } from '@/services/api.uts'
|
import { clearSession, getSession } from '@/services/api.uts'
|
||||||
type ProfileMenu = { icon: string, name: string, note: string, color: string, url: string }
|
import { activeAssessmentType, loadAssessmentDraft, draftText, assessmentProgress, showAgreementNotice } from '@/services/assessment.uts'
|
||||||
const menus: ProfileMenu[] = [
|
const { contentStyle, pageStyle } = usePageLayout()
|
||||||
{ icon: 'archive', name: '我的融资档案', note: '75%', color: 'blue', url: '/pages/archive/archive' },
|
type ProfileMenu = { icon: string, name: string, note: string, key: string }
|
||||||
{ icon: 'points', name: '积分中心', note: '2680', color: 'purple', url: '/pages/points/points' },
|
const menus: ProfileMenu[] = [{ icon: 'archive', name: '我的融资档案', note: '', key: 'archive' }, { icon: 'shield', name: '账户安全', note: '', key: 'security' }, { icon: 'privacy', name: '隐私与授权', note: '', key: 'privacy' }, { icon: 'help', name: '帮助与反馈', note: '', key: 'help' }, { icon: 'info', name: '关于我们', note: 'V1.0.0', key: 'about' }]
|
||||||
{ icon: 'shield', name: '账户安全', note: '', color: 'green', url: '' },
|
const loggedIn = ref(false)
|
||||||
{ icon: 'privacy', name: '隐私设置', note: '', color: 'orange', url: '' },
|
const displayName = ref('登录 / 注册')
|
||||||
{ icon: 'help', name: '帮助与反馈', note: '', color: 'cyan', url: '/pages/assistant/assistant' },
|
const maskedPhone = ref('')
|
||||||
{ icon: 'info', name: '关于我们', note: 'V1.0.0', color: 'gray', url: '' }
|
const completion = ref(0)
|
||||||
]
|
const hasReport = ref(false)
|
||||||
const open = (url: string, name: string) => { if (url.length > 0) uni.navigateTo({ url: url }); else uni.showToast({ title: name + '功能已预留', icon: 'none' }) }
|
onShow(() => {
|
||||||
const logout = () => { clearSession(); uni.reLaunch({ url: '/pages/login/login' }) }
|
const session = getSession()
|
||||||
|
loggedIn.value = session != null
|
||||||
|
const phone = session != null ? draftText(session, 'phone') : ''
|
||||||
|
maskedPhone.value = phone.length >= 7 ? phone.substring(0, 3) + '****' + phone.substring(phone.length - 4) : phone
|
||||||
|
const type = activeAssessmentType(); const saved = loadAssessmentDraft(type)
|
||||||
|
displayName.value = loggedIn.value ? '融资用户' : '登录 / 注册'
|
||||||
|
if (loggedIn.value && saved != null && draftText(saved, 'phone') == phone && draftText(saved, 'name').length > 0) displayName.value = draftText(saved, 'name')
|
||||||
|
completion.value = assessmentProgress(type).percent
|
||||||
|
const latest = uni.getStorageSync('latestAssessment')
|
||||||
|
hasReport.value = latest != null && typeof latest == 'object'
|
||||||
|
})
|
||||||
|
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 open = (key: string) => {
|
||||||
|
if (key == 'archive') go('/pages/archive/archive')
|
||||||
|
else if (key == 'help') go('/pages/assistant/assistant')
|
||||||
|
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 == '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 logout = () => {
|
||||||
|
if (!loggedIn.value) { go('/pages/login/login'); return }
|
||||||
|
uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) { clearSession(); uni.reLaunch({ url: '/pages/login/login' }) } } })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.profile-scroll { }
|
.profile-content { padding: 26rpx 30rpx 90rpx; }
|
||||||
.profile-content { padding: 26rpx 28rpx 160rpx; }
|
.profile-card { min-height: 166rpx; padding: 28rpx 24rpx; border-radius: 18rpx 18rpx 0 0; background-color: #fff; flex-direction: row; align-items: center; }
|
||||||
.profile-card { min-height: 170rpx; padding: 30rpx; border-radius: 18rpx 18rpx 0 0; background-color: #fff; border: 1rpx solid #e5ecf5; flex-direction: row; align-items: center; }
|
.avatar { width: 84rpx; height: 84rpx; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
|
||||||
.avatar { width: 92rpx; height: 92rpx; border-radius: 50%; background: linear-gradient(145deg, #2d8cff, #0c4fc6); align-items: center; justify-content: center; color: white; font-size: 36rpx; font-weight: 800; margin-right: 22rpx; }
|
.profile-main { flex: 1; min-width: 0; }
|
||||||
.profile-main { flex: 1; }
|
.profile-name { color: #23344e; font-size: 32rpx; font-weight: 700; }
|
||||||
.profile-name { font-size: 32rpx; font-weight: 800; }
|
.profile-phone { color: #8190a4; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
|
||||||
.profile-phone { color: #8390a3; font-size: 22rpx; margin-top: 10rpx; }
|
.account-state { padding: 8rpx 12rpx; border-radius: 8rpx; background-color: #eef4fd; color: #6c84a5; font-size: 22rpx; flex-shrink: 0; margin-left: 10rpx; }
|
||||||
.verified { padding: 8rpx 14rpx; border-radius: 9rpx; background-color: #e7f8f0; color: #0ba66e; font-size: 19rpx; }
|
.stat-row { min-height: 136rpx; background-color: #fff; border-top: 1rpx solid #edf1f6; border-radius: 0 0 18rpx 18rpx; flex-direction: row; }
|
||||||
.stat-row { height: 140rpx; background-color: #fff; border: 1rpx solid #e5ecf5; border-top: 0; border-radius: 0 0 18rpx 18rpx; flex-direction: row; }
|
.stat-item { flex: 1; align-items: center; justify-content: center; }
|
||||||
.stat-row-view { flex: 1; align-items: center; justify-content: center; border-right: 1rpx solid #edf1f6; }
|
.stat-value { font-size: 31rpx; font-weight: 700; color: #173864; line-height: 44rpx; }
|
||||||
.stat-row-view-last-child { border-right: 0; }
|
.stat-label { color: #7d8ca1; font-size: 24rpx; margin-top: 10rpx; }
|
||||||
.stat-number { font-size: 31rpx; font-weight: 800; color: #102557; }
|
.menu-list { margin-top: 24rpx; padding: 0 24rpx; border-radius: 18rpx; background-color: #fff; }
|
||||||
.stat-label { color: #8491a4; font-size: 20rpx; margin-top: 8rpx; }
|
.menu-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
||||||
.menu-list { margin-top: 24rpx; padding: 0 24rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e5ecf5; }
|
.menu-name { flex: 1; font-size: 28rpx; color: #2a3a53; margin-left: 20rpx; }
|
||||||
.menu-row { height: 105rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
|
.menu-note { color: #8793a6; font-size: 23rpx; margin-right: 12rpx; }
|
||||||
.menu-icon { width: 52rpx; height: 52rpx; border-radius: 16rpx; align-items: center; justify-content: center; font-size: 20rpx; font-weight: 800; margin-right: 18rpx; }
|
.logout { margin-top: 26rpx; height: 92rpx; line-height: 92rpx; border-radius: 16rpx; color: #56667d; background-color: #fff; border: 1rpx solid #e5ecf5; font-size: 28rpx; }
|
||||||
.blue { color: #0868f7; background-color: #e8f2ff; }
|
.version-note { color: #94a0b0; font-size: 22rpx; text-align: center; margin-top: 28rpx; }
|
||||||
.purple { color: #7757ea; background-color: #efebff; }
|
|
||||||
.green { color: #0ba979; background-color: #e6f8f1; }
|
|
||||||
.orange { color: #f2751b; background-color: #fff0e5; }
|
|
||||||
.cyan { color: #0a9eb2; background-color: #e7f8fb; }
|
|
||||||
.gray { color: #667489; background-color: #eef2f6; }
|
|
||||||
.menu-name { flex: 1; font-size: 25rpx; }
|
|
||||||
.menu-note { color: #8793a6; font-size: 21rpx; margin-right: 14rpx; }
|
|
||||||
.chevron { color: #abb5c4; font-size: 36rpx; }
|
|
||||||
.logout { margin-top: 25rpx; height: 88rpx; line-height: 88rpx; border-radius: 16rpx; color: #ef4444; background-color: #fff; border: 1rpx solid #e5ecf5; font-size: 26rpx; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
<AppHeader title="融资需求报告" />
|
<AppHeader title="融资需求报告" />
|
||||||
<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">您的融资资质超过 82% 的用户</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><view class="radar-core"><text>85</text></view><view class="radar-label top"><text>还款能力</text></view><view class="radar-label left"><text>收入稳定</text></view><view class="radar-label right"><text>信用状况</text></view><view class="radar-label bottom-left"><text>资产条件</text></view><view class="radar-label bottom-right"><text>负债情况</text></view></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="conclusion"><text class="section-title">评估结论</text><text class="conclusion-text">您的资质整体良好,符合多家银行产品要求。</text><view class="suggest-row"><text>建议额度</text><text class="suggest-value">6-36个月</text></view></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>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
@@ -29,17 +29,19 @@
|
|||||||
.score-unit { font-size: 28rpx; margin: 0 16rpx 14rpx 5rpx; }
|
.score-unit { font-size: 28rpx; margin: 0 16rpx 14rpx 5rpx; }
|
||||||
.score-badge { padding: 8rpx 17rpx; border-radius: 10rpx; background-color: #e5f8ee; color: #0ba868; font-size: 22rpx; margin-bottom: 15rpx; }
|
.score-badge { padding: 8rpx 17rpx; border-radius: 10rpx; background-color: #e5f8ee; color: #0ba868; font-size: 22rpx; margin-bottom: 15rpx; }
|
||||||
.score-caption { color: #8793a6; font-size: 22rpx; margin-top: 6rpx; }
|
.score-caption { color: #8793a6; font-size: 22rpx; margin-top: 6rpx; }
|
||||||
.radar-panel { height: 500rpx; margin-top: 24rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
.radar-panel { height: 370rpx; margin-top: 20rpx; border-radius: 18rpx; background-color: #fff; border: 1rpx solid #e7edf6; align-items: center; justify-content: center; position: relative; overflow: hidden; }
|
||||||
.radar-image { width: 430rpx; height: 430rpx; }
|
.radar-image { width: 340rpx; height: 310rpx; }
|
||||||
.radar-core { position: absolute; width: 92rpx; height: 92rpx; left: 50%; top: 50%; margin-left: -46rpx; margin-top: -46rpx; border-radius: 50%; background-color: #0868f7; align-items: center; justify-content: center; color: white; font-size: 34rpx; font-weight: 800; box-shadow: 0 0 32rpx rgba(8,104,247,.28); }
|
.radar-core { position: absolute; width: 92rpx; height: 92rpx; left: 50%; top: 50%; margin-left: -46rpx; margin-top: -46rpx; border-radius: 50%; background-color: #0868f7; align-items: center; justify-content: center; color: white; font-size: 34rpx; font-weight: 800; box-shadow: 0 0 32rpx rgba(8,104,247,.28); }
|
||||||
.radar-label { position: absolute; color: #58667b; font-size: 21rpx; }
|
.radar-label { position: absolute; color: #58667b; font-size: 21rpx; }
|
||||||
.top { top: 28rpx; }
|
.top { top: 28rpx; }
|
||||||
.left { left: 27rpx; top: 224rpx; }
|
.left { left: 27rpx; top: 162rpx; }
|
||||||
.right { right: 27rpx; top: 224rpx; }
|
.right { right: 27rpx; top: 162rpx; }
|
||||||
.bottom-left { left: 95rpx; bottom: 32rpx; }
|
.bottom-left { left: 95rpx; bottom: 32rpx; }
|
||||||
.bottom-right { right: 86rpx; bottom: 32rpx; }
|
.bottom-right { right: 86rpx; bottom: 32rpx; }
|
||||||
.conclusion { margin: 24rpx 0 32rpx; background-color: #fff; border-radius: 18rpx; padding: 28rpx; border: 1rpx solid #e7edf6; }
|
.conclusion { margin: 24rpx 0 32rpx; background-color: #fff; border-radius: 18rpx; padding: 28rpx; border: 1rpx solid #e7edf6; }
|
||||||
.conclusion-text { margin-top: 18rpx; font-size: 24rpx; color: #59677b; line-height: 42rpx; }
|
.conclusion-text { margin-top: 18rpx; font-size: 24rpx; color: #59677b; line-height: 42rpx; }
|
||||||
.suggest-row { margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid #edf1f6; flex-direction: row; justify-content: space-between; font-size: 25rpx; }
|
.suggest-row { margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid #edf1f6; flex-direction: row; justify-content: space-between; font-size: 25rpx; }
|
||||||
.suggest-value { color: #0868f7; font-weight: 700; }
|
.suggest-value { color: #0868f7; font-size: 27rpx; font-weight: 700; }
|
||||||
|
.suggest-label { color: #53667f; font-size: 25rpx; }
|
||||||
|
.report-note { font-size: 21rpx; line-height: 34rpx; color: #78889e; margin-top: 20rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+14
-7
@@ -1,4 +1,5 @@
|
|||||||
/** Mobile API adapter with deterministic local mocks. */
|
/** Mobile API adapter with deterministic local mocks. */
|
||||||
|
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'
|
||||||
@@ -97,18 +98,24 @@ export async function submitAssessment(payload: UTSJSONObject): Promise<string>
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchMatches(): Promise<UTSJSONObject[]> {
|
export async function fetchMatches(): Promise<FinanceProduct[]> {
|
||||||
if (API_BASE_URL.length == 0) {
|
if (API_BASE_URL.length == 0) {
|
||||||
await wait(320)
|
await wait(320)
|
||||||
return [
|
return demoProducts.slice()
|
||||||
{ rank: 1, bank: '招商银行·闪电贷', amount: '50万', rate: '3.85%', term: '12-36期', score: 96 },
|
|
||||||
{ rank: 2, bank: '平安银行·新一贷', amount: '30万', rate: '4.20%', term: '12-60期', score: 91 },
|
|
||||||
{ rank: 3, bank: '建设银行·快e贷', amount: '20万', rate: '4.35%', term: '12-36期', score: 88 }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
|
const result = await requestJson('/matches', 'GET', new UTSJSONObject())
|
||||||
const items = result['items'] as UTSJSONObject[] | null
|
const items = result['items'] as UTSJSONObject[] | null
|
||||||
return items == null ? [] : items
|
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> {
|
export async function sendAssistantMessage(message: string): Promise<string> {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
export type AssessmentForm = {
|
||||||
|
name: string, idCard: string, phone: string, city: string, maritalStatus: string,
|
||||||
|
occupation: string, company: string, monthlyIncome: string, socialSecurity: string,
|
||||||
|
housingFund: string, house: string, car: string, savings: string, creditCard: string,
|
||||||
|
otherAssets: string, enterpriseName: string, businessCode: string, annualRevenue: string, operatingYears: string
|
||||||
|
}
|
||||||
|
export type SelectedDocument = { name: string, path: string }
|
||||||
|
export type DataAuthorization = { key: string, name: string, selected: boolean }
|
||||||
|
|
||||||
|
export function emptyAssessmentForm(): AssessmentForm {
|
||||||
|
return { name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '', enterpriseName: '', businessCode: '', annualRevenue: '', operatingYears: '' }
|
||||||
|
}
|
||||||
|
export function draftText(saved: UTSJSONObject, key: string): string {
|
||||||
|
const value = saved[key]
|
||||||
|
return typeof value == 'string' ? value as string : ''
|
||||||
|
}
|
||||||
|
export function activeAssessmentType(): string {
|
||||||
|
const type = uni.getStorageSync('assessmentActiveType')
|
||||||
|
if (type == 'enterprise' || type == 'personal') return type as string
|
||||||
|
const legacy = uni.getStorageSync('assessmentDraft')
|
||||||
|
if (legacy != null && typeof legacy == 'object' && draftText(legacy as UTSJSONObject, 'type') == 'enterprise') return 'enterprise'
|
||||||
|
return 'personal'
|
||||||
|
}
|
||||||
|
export function loadAssessmentDraft(type: string): UTSJSONObject | null {
|
||||||
|
const scoped = uni.getStorageSync('assessmentDraft:' + type)
|
||||||
|
if (scoped != null && typeof scoped == 'object') return scoped as UTSJSONObject
|
||||||
|
const legacy = uni.getStorageSync('assessmentDraft')
|
||||||
|
if (legacy != null && typeof legacy == 'object') {
|
||||||
|
const saved = legacy as UTSJSONObject
|
||||||
|
if (draftText(saved, 'type') == type) return saved
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
export function formFromDraft(saved: UTSJSONObject): AssessmentForm {
|
||||||
|
return {
|
||||||
|
name: draftText(saved, 'name'), idCard: draftText(saved, 'idCard'), phone: draftText(saved, 'phone'), city: draftText(saved, 'city'), maritalStatus: draftText(saved, 'maritalStatus'),
|
||||||
|
occupation: draftText(saved, 'occupation'), company: draftText(saved, 'company'), monthlyIncome: draftText(saved, 'monthlyIncome'), socialSecurity: draftText(saved, 'socialSecurity'), housingFund: draftText(saved, 'housingFund'),
|
||||||
|
house: draftText(saved, 'house'), car: draftText(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'),
|
||||||
|
enterpriseName: draftText(saved, 'enterpriseName'), businessCode: draftText(saved, 'businessCode'), annualRevenue: draftText(saved, 'annualRevenue'), operatingYears: draftText(saved, 'operatingYears')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] {
|
||||||
|
const results: SelectedDocument[] = []
|
||||||
|
const items = saved['documents'] as UTSJSONObject[] | null
|
||||||
|
if (items == null) return results
|
||||||
|
items.forEach((item) => {
|
||||||
|
const name = draftText(item, 'name'); const path = draftText(item, 'path')
|
||||||
|
if (name.length > 0 && path.length > 0) { const document: SelectedDocument = { name, path }; results.push(document) }
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
export function saveAssessmentDraft(type: string, step: number, form: AssessmentForm, documents: SelectedDocument[], authorizations: DataAuthorization[], documentConsent: boolean, dataConsent: boolean): void {
|
||||||
|
const files: UTSJSONObject[] = documents.map((item): UTSJSONObject => ({ name: item.name, path: item.path }))
|
||||||
|
const grants: string[] = authorizations.filter((item) => item.selected).map((item) => item.key)
|
||||||
|
const payload: UTSJSONObject = {
|
||||||
|
type, step, name: form.name, idCard: form.idCard, phone: form.phone, city: form.city, maritalStatus: form.maritalStatus,
|
||||||
|
occupation: form.occupation, company: form.company, monthlyIncome: form.monthlyIncome, socialSecurity: form.socialSecurity, housingFund: form.housingFund,
|
||||||
|
house: form.house, car: form.car, savings: form.savings, creditCard: form.creditCard, otherAssets: form.otherAssets,
|
||||||
|
enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears,
|
||||||
|
documents: files, authorizations: grants, documentConsent, dataConsent
|
||||||
|
}
|
||||||
|
uni.setStorageSync('assessmentActiveType', type)
|
||||||
|
uni.setStorageSync('assessmentDraft:' + type, payload)
|
||||||
|
// 保留现有提交与档案入口读取的活动草稿。
|
||||||
|
uni.setStorageSync('assessmentDraft', payload)
|
||||||
|
}
|
||||||
|
export function showAgreementNotice(title: string): void {
|
||||||
|
uni.showModal({ title, content: '正式协议尚未配置。当前为功能演示,勾选仅记录本地选择,不会发起实名认证、查询外部数据或上传资料。正式服务开放前,将展示完整协议供您阅读与确认。', showCancel: false, confirmText: '我知道了' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssessmentProgress = { percent: number, basicDone: boolean, incomeDone: boolean, assetsDone: boolean, authorizationSelected: boolean }
|
||||||
|
export function assessmentProgress(type: string): AssessmentProgress {
|
||||||
|
const saved = loadAssessmentDraft(type)
|
||||||
|
if (saved == null) return { percent: 0, basicDone: false, incomeDone: false, assetsDone: false, authorizationSelected: false }
|
||||||
|
const basic = type == 'enterprise' ? ['enterpriseName', 'businessCode', 'name', 'phone', 'city'] : ['name', 'idCard', 'phone', 'city', 'maritalStatus']
|
||||||
|
const income = type == 'enterprise' ? ['occupation', 'operatingYears', 'annualRevenue'] : ['occupation', 'company', 'monthlyIncome', 'socialSecurity', 'housingFund']
|
||||||
|
const assets = ['house', 'car', 'savings', 'creditCard']
|
||||||
|
const keys = basic.concat(income).concat(assets)
|
||||||
|
const filled = keys.filter((key) => draftText(saved, key).trim().length > 0).length
|
||||||
|
const grants = saved['authorizations'] as string[] | null
|
||||||
|
return { percent: Math.round(filled / keys.length * 100), basicDone: basic.every((key) => draftText(saved, key).length > 0), incomeDone: income.every((key) => draftText(saved, key).length > 0), assetsDone: assets.every((key) => draftText(saved, key).length > 0), authorizationSelected: documentsFromDraft(saved).length > 0 || (grants != null && grants.length > 0) }
|
||||||
|
}
|
||||||
@@ -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: '/static/mobile/images/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: '/static/mobile/images/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: '/static/mobile/images/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: '/static/mobile/images/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: '/static/mobile/images/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
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
export type FinanceProduct = {
|
||||||
|
id: string, bank: string, initials: string, color: string,
|
||||||
|
amount: number, rate: number, term: string, score: number,
|
||||||
|
advantages: string[], age: string, work: string, credit: string, materials: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表和详情共用产品标识;正式数据由 API 适配层提供。
|
||||||
|
export const demoProducts: FinanceProduct[] = [
|
||||||
|
{ id: '1', bank: '招商银行·闪电贷', initials: '招', color: '#c93547', amount: 50, rate: 3.85, term: '12–36个月', score: 96, advantages: ['线上申请', '无需抵押', '额度循环', '还款灵活'], age: '22–55周岁', work: '现单位工作满6个月', credit: '近两年无严重逾期', materials: '身份证 · 银行卡 · 收入证明' },
|
||||||
|
{ id: '2', bank: '平安银行·新一贷', initials: '平', color: '#ed7924', amount: 30, rate: 4.20, term: '12–60个月', score: 91, advantages: ['信用融资', '期限可选', '在线预审', '无需担保'], age: '23–55周岁', work: '有稳定工作或经营收入', credit: '征信记录良好', materials: '身份证 · 收入证明 · 居住证明' },
|
||||||
|
{ id: '3', bank: '建设银行·快e贷', initials: '建', color: '#2869b7', amount: 20, rate: 4.35, term: '12–36个月', score: 88, advantages: ['线上办理', '便捷用款', '信用授信', '支持提前还款'], age: '22–60周岁', work: '有稳定收入来源', credit: '符合机构信用要求', materials: '身份证 · 银行卡 · 资产或收入证明' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function parseProduct(item: UTSJSONObject): FinanceProduct {
|
||||||
|
return {
|
||||||
|
id: item['id'] as string ?? '', bank: item['bank'] as string ?? '融资产品',
|
||||||
|
initials: item['initials'] as string ?? '融', color: item['color'] as string ?? '#2869b7',
|
||||||
|
amount: item['amount'] as number ?? 0, rate: item['rate'] as number ?? 0,
|
||||||
|
term: item['term'] as string ?? '以审批为准', score: item['score'] as number ?? 0,
|
||||||
|
advantages: item['advantages'] as string[] ?? [], age: item['age'] as string ?? '以机构要求为准',
|
||||||
|
work: item['work'] as string ?? '以机构要求为准', credit: item['credit'] as string ?? '以机构要求为准',
|
||||||
|
materials: item['materials'] as string ?? '以机构要求为准'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" fill="#8fd4ff"/><path d="m10 7 5 5-5 5" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path d="m9 5 7 7-7 7" stroke="#324763" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 184 B |
Reference in New Issue
Block a user