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

1.  调整页面路由,将业务页面迁移到对应分包,主包仅保留首页
2.  拆分公共服务模块,将账户、融资相关逻辑移入对应分包
3.  统一资源路径,将专属静态资源放入各分包static目录,更新导航栏图标资源
4.  修复启动引导逻辑,实现仅首次冷启动显示引导页
5.  更新README文档,补充分包配置与开发规范说明
This commit is contained in:
hujun
2026-09-20 10:52:10 +08:00
parent 94a4558290
commit 69130d11e7
43 changed files with 278 additions and 180 deletions
+44
View File
@@ -0,0 +1,44 @@
<template>
<view class="splash page-white" @click="enter" :style="pageStyle()">
<view class="splash-status" :style="statusStyle(0)"></view>
<view class="brand-area">
<BrandLogo />
<text class="tagline">智能评估 · 精准匹配</text>
<text class="slogan">让融资更简单</text>
</view>
<image class="splash-art" src="/packages/account/static/shield.png" mode="aspectFit"></image>
<view class="launch-line"><view class="launch-dot"></view><text class="launch-text">开启智能融资新体验</text></view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
const { statusStyle, pageStyle } = usePageLayout()
import BrandLogo from '@/components/BrandLogo.uvue'
import { isLoggedIn } from '@/services/api.uts'
import { consumeInitialLaunch } from '@/services/launch.uts'
onLoad(() => { consumeInitialLaunch() })
let enterTimer = 0
let entered = false
const enter = () => {
if (entered) return
entered = true
clearTimeout(enterTimer)
uni.redirectTo({ url: isLoggedIn() ? '/pages/home/home' : '/packages/account/login/login' })
}
onShow(() => { if (!entered) enterTimer = setTimeout(() => enter(), 1800) })
onHide(() => { clearTimeout(enterTimer) })
onUnmounted(() => { clearTimeout(enterTimer) })
</script>
<style>
.splash { align-items: center; position: relative; overflow: hidden; }
.splash-status { }
.brand-area { align-items: center; margin-top: 220rpx; }
.tagline { margin-top: 18rpx; color: #32405a; font-size: 28rpx; font-weight: 600; }
.slogan { margin-top: 42rpx; color: #102557; font-size: 33rpx; font-weight: 800; }
.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-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>
+126
View File
@@ -0,0 +1,126 @@
<template>
<view class="login-page page-white" :style="pageStyle()">
<view :style="statusStyle(44)"></view>
<scroll-view scroll-y :style="contentStyle(44, false)">
<view class="login-content">
<BrandLogo />
<text class="welcome">欢迎登录 AI融资</text>
<text class="login-subtitle">智能评估,让融资更简单</text>
<text class="form-heading">{{ mode == 'phone' ? '手机号登录 / 注册' : '密码登录' }}</text>
<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 v-if="mode == 'password'" class="input-box"><input v-model="password" :disabled="loading" password placeholder="请输入登录密码" class="field-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>
<text v-if="errorMessage.length > 0" class="error-message">{{ errorMessage }}</text>
<button class="login-button" :disabled="loading" @click="login">{{ loading ? '处理中…' : (mode == 'phone' ? (smsSent ? '登录' : '获取验证码') : '登录') }}</button>
<view class="mode-row"><text class="mode-link" @click="switchMode">{{ mode == 'phone' ? '密码登录' : '使用验证码登录' }}</text><text v-if="isDemo" class="demo-label">演示模式</text></view>
<text v-if="isDemo && smsSent" class="demo-hint">未发送真实短信,可输入任意 6 位数字体验。</text>
<view class="other-login"><view class="line"></view><text class="other-text">其他登录方式</text><view class="line"></view></view>
<view class="social-row"><view class="social-button" @click="socialLogin('微信')"><image class="social-icon" src="/packages/account/static/wechat.svg" mode="aspectFit" /><text class="social-label">微信</text></view><view class="social-button" @click="socialLogin('一键登录')"><image class="social-icon" src="/packages/account/static/quick-login.svg" mode="aspectFit" /><text class="social-label">一键登录</text></view></view>
<view class="agreement"><view class="check-hit" @click="agreed = !agreed"><view :class="agreed ? 'check checked' : 'check'"><text v-if="agreed" class="check-mark">✓</text></view></view><text class="agreement-copy">我已阅读并同意<text class="agreement-link" @click="showAgreement('用户协议')">《用户协议》</text>和<text class="agreement-link" @click="showAgreement('隐私政策')">《隐私政策》</text></text></view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import BrandLogo from '@/components/BrandLogo.uvue'
import { API_BASE_URL, requestSmsCode, loginWithSms, loginWithPassword, saveSession } from '@/packages/account/services/auth.uts'
import { showAgreementNotice } from '@/services/assessment.uts'
import { consumeInitialLaunch } from '@/services/launch.uts'
onLoad(() => { consumeInitialLaunch() })
const { statusStyle, contentStyle, pageStyle } = usePageLayout()
const mode = ref('phone')
const phone = ref('')
const password = ref('')
const smsCode = ref('')
const smsSent = ref(false)
const countdown = ref(0)
const loading = ref(false)
const errorMessage = ref('')
const agreed = ref(false)
const isDemo = API_BASE_URL.length == 0
const showAgreement = (title: string) => { showAgreementNotice(title) }
let countdownTimer = 0
watch(phone, () => {
smsSent.value = false; smsCode.value = ''; countdown.value = 0; errorMessage.value = ''
clearInterval(countdownTimer)
})
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 => {
if (!/^1[3-9][0-9]{9}$/.test(phone.value)) { errorMessage.value = '请输入正确的 11 位手机号'; return false }
return true
}
const startCountdown = () => {
countdown.value = 60
clearInterval(countdownTimer)
countdownTimer = setInterval(() => {
if (countdown.value <= 1) { countdown.value = 0; clearInterval(countdownTimer) } else countdown.value -= 1
}, 1000)
}
const finishLogin = (token: string) => { saveSession(token, phone.value); uni.redirectTo({ url: '/pages/home/home' }) }
const resendSms = async () => {
if (loading.value || countdown.value > 0 || !validPhone()) return
if (!agreed.value) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return }
loading.value = true; errorMessage.value = ''
try {
if (!(await requestSmsCode(phone.value))) throw new Error('验证码发送失败,请稍后重试')
startCountdown()
uni.showToast({ title: isDemo ? '演示验证码已就绪' : '验证码已重新发送', icon: 'none' })
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '验证码发送失败,请稍后重试' } finally { loading.value = false }
}
const login = async () => {
errorMessage.value = ''
if (!agreed.value) { errorMessage.value = '请先阅读并勾选用户协议与隐私政策'; return }
if (!validPhone() || loading.value) return
loading.value = true
try {
if (mode.value == 'phone') {
if (!smsSent.value) {
if (!(await requestSmsCode(phone.value))) throw new Error('验证码发送失败,请稍后重试')
smsSent.value = true; startCountdown()
uni.showToast({ title: isDemo ? '演示验证码已就绪' : '验证码已发送', icon: 'none' })
} else {
if (!/^[0-9]{6}$/.test(smsCode.value)) throw new Error('请输入 6 位数字验证码')
finishLogin(await loginWithSms(phone.value, smsCode.value))
}
} else {
if (password.value.length < 6) throw new Error('密码至少需要 6 位')
finishLogin(await loginWithPassword(phone.value, password.value))
}
} catch (error) { errorMessage.value = error instanceof Error ? error.message : '登录失败,请稍后重试' } finally { loading.value = false }
}
onUnmounted(() => { clearInterval(countdownTimer) })
</script>
<style>
.login-content { padding: 20rpx 40rpx 48rpx; }
.welcome { font-size: 42rpx; line-height: 60rpx; font-weight: 800; margin-top: 38rpx; color: #152443; }
.login-subtitle { font-size: 26rpx; color: #748198; line-height: 40rpx; margin-top: 8rpx; }
.form-heading { margin-top: 54rpx; font-size: 28rpx; font-weight: 600; color: #22324a; }
.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; }
.input-prefix { font-size: 28rpx; color: #31415a; margin-right: 22rpx; flex-shrink: 0; }
.field-input { flex: 1; min-width: 0; font-size: 28rpx; color: #17243c; }
.countdown { text-align: right; color: #0868f7; font-size: 24rpx; flex-shrink: 0; margin-left: 12rpx; }
.error-message { margin-top: 16rpx; color: #d64040; font-size: 24rpx; line-height: 36rpx; }
.login-button { height: 92rpx; line-height: 92rpx; margin-top: 28rpx; border-radius: 16rpx; color: #fff; font-size: 30rpx; font-weight: 600; background-color: #0868f7; }
.mode-row { flex-direction: row; justify-content: space-between; align-items: center; margin-top: 12rpx; }
.mode-link { padding: 16rpx 0; color: #31415a; font-size: 26rpx; }
.demo-label { color: #8591a2; font-size: 22rpx; }
.demo-hint { color: #65748a; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
.other-login { margin-top: 66rpx; flex-direction: row; align-items: center; justify-content: center; }
.other-text { font-size: 24rpx; color: #8490a3; flex-shrink: 0; }
.line { height: 1rpx; background-color: #e5eaf1; flex: 1; margin: 0 20rpx; }
.social-row { flex-direction: row; justify-content: center; margin-top: 28rpx; }
.social-button { width: 150rpx; min-height: 112rpx; align-items: center; justify-content: center; margin: 0 26rpx; }
.social-icon { width: 62rpx; height: 62rpx; flex-shrink: 0; }
.social-label { color: #58677e; font-size: 24rpx; margin-top: 12rpx; }
.agreement { margin-top: 52rpx; flex-direction: row; align-items: flex-start; }
.check-hit { width: 44rpx; min-height: 44rpx; flex-shrink: 0; justify-content: center; }
.check { width: 30rpx; height: 30rpx; border: 2rpx solid #9aa8ba; border-radius: 6rpx; align-items: center; justify-content: center; }
.checked { background-color: #0868f7; border-color: #0868f7; }
.check-mark { font-size: 23rpx; color: #fff; }
.agreement-copy { flex: 1; min-width: 0; color: #748198; font-size: 24rpx; line-height: 42rpx; }
.agreement-link { color: #0868f7; font-size: 24rpx; line-height: 42rpx; }
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="消息中心" :back="false" />
<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 :style="contentStyle(314, false)">
<view class="messages-content">
<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>
</scroll-view>
<BottomNav active="message" />
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue'
import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout()
const tabs: string[] = ['全部', '系统通知', '服务通知', '活动消息']
const tab = ref('全部')
type Notice = { id: string, icon: string, category: string, title: string, desc: string, body: string, time: string, unread: boolean }
const notices = ref<Notice[]>([
{ id: 'assessment-guide', icon: 'approval', category: '服务通知', title: '融资评估使用指南', desc: '完成资料填写,了解自己的融资需求。', body: '从首页选择个人或企业融资入口,按步骤填写基本信息、经营或收入、资产与负债情况。提交前请核对资料,评估结果仅供参考,具体申请条件以机构审核为准。', time: '10:30', unread: true },
{ id: 'repayment-guide', icon: 'payout', category: '服务通知', title: '还款安排提醒', desc: '申请前,记得核对每期应还金额与日期。', body: '比较融资方案时,除利率外还应核对还款方式、每期应还金额与到期本金。正式签约后,请以机构提供的合同和还款计划为准,通过官方渠道查询账户状态。', time: '09:15', unread: false },
{ id: 'invite-guide', icon: 'gift', category: '活动消息', title: '邀请积分说明', desc: '邀请前先了解积分规则与适用范围。', body: '可在积分中心查看邀请入口与积分说明。积分发放条件、有效期及可用权益以活动页面公布的规则为准,请勿向他人承诺固定收益或贷款结果。', time: '08:42', 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>
<style>
.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; border-bottom: 4rpx solid transparent; }
.active-tab { border-bottom-color: #0868f7; }
.tab-text { color: #66758a; font-size: 25rpx; }
.active-text { color: #0868f7; font-weight: 700; }
.messages-content { padding: 14rpx 28rpx 90rpx; }
.message-summary { flex-direction: row; align-items: center; justify-content: space-between; min-height: 66rpx; }
.summary-text { color: #66758a; font-size: 23rpx; }
.read-all { padding: 14rpx 0 14rpx 24rpx; }
.read-all-text { color: #0868f7; font-size: 23rpx; }
.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-head { flex-direction: row; justify-content: space-between; align-items: flex-start; }
.notice-title { color: #172640; font-size: 28rpx; line-height: 40rpx; font-weight: 600; flex: 1; }
.notice-time { color: #66758a; font-size: 22rpx; line-height: 40rpx; margin-left: 12rpx; flex-shrink: 0; }
.notice-desc { color: #66758a; font-size: 25rpx; line-height: 38rpx; margin-top: 10rpx; }
.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>
+132
View File
@@ -0,0 +1,132 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="积分中心" />
<scroll-view scroll-y :style="contentStyle(96, false)">
<view class="points-content">
<view class="points-hero">
<view class="points-hero-top"><text class="points-label">我的体验积分</text><button @click="showRules" class="points-hero-button">积分规则</button></view>
<text class="points-number">{{ totalPoints }}</text><text class="points-note">本地体验 · 仅在本机记录</text>
</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>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue'
import AppIcon from '@/components/AppIcon.uvue'
const { contentStyle, pageStyle } = usePageLayout()
type PointsTask = { id: string, icon: string, name: string, caption: string }
type PointsRecord = { date: string, score: number }
const tasks: PointsTask[] = [
{ id: 'checkin', icon: 'calendar-check', name: '签到', caption: '每日 +10' },
{ id: 'share', icon: 'share', name: '分享', caption: '复制文案' },
{ id: 'profile', icon: 'edit-file', name: '完善资料', caption: '查看档案' },
{ 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: '/packages/finance/archive/archive' }); return }
if (id == 'invite') { invite(); return }
copyText('分享一个融资准备方法:先梳理信用、收入与负债,再比较适合自己的产品。可以在 AI融资的融资课堂中学习相关知识。', '分享文案已复制,可自行分享')
}
</script>
<style>
.points-content { padding: 24rpx 28rpx 48rpx; }
.points-hero { padding: 28rpx; border-radius: 20rpx; background-color: #5148d8; }
.points-hero-top { flex-direction: row; align-items: center; justify-content: space-between; }
.points-label { font-size: 26rpx; color: #f0efff; }
.points-number { font-size: 74rpx; line-height: 94rpx; font-weight: 700; color: #ffffff; margin-top: 12rpx; }
.points-note { color: #e5e1ff; font-size: 22rpx; line-height: 34rpx; margin-top: 4rpx; }
.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 { min-height: 186rpx; flex-direction: row; background-color: #ffffff; border-radius: 18rpx; margin-top: 20rpx; }
.task { flex: 1; align-items: center; justify-content: center; padding: 24rpx 0; }
.task-name { margin-top: 14rpx; font-size: 24rpx; color: #172640; font-weight: 600; }
.task-caption { margin-top: 7rpx; font-size: 22rpx; color: #66758a; }
.completed-caption { color: #168454; }
.invite-panel, .records { margin-top: 22rpx; padding: 24rpx; border-radius: 18rpx; background-color: #ffffff; }
.panel-title { color: #172640; font-size: 30rpx; line-height: 42rpx; font-weight: 700; }
.invite-copy { margin-top: 12rpx; color: #66758a; font-size: 25rpx; line-height: 38rpx; }
.invite-code { min-height: 92rpx; padding: 12rpx 18rpx; margin-top: 22rpx; background-color: #f4f7fb; border-radius: 12rpx; flex-direction: row; align-items: center; }
.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-note { color: #66758a; font-size: 22rpx; line-height: 34rpx; margin-top: 14rpx; }
.record-head { flex-direction: row; justify-content: space-between; align-items: center; min-height: 52rpx; }
.record-toggle { flex-direction: row; align-items: center; padding-left: 20rpx; }
.more { color: #0868f7; font-size: 23rpx; margin-right: 6rpx; }
.record-arrow { transform: rotate(90deg); }
.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>
+78
View File
@@ -0,0 +1,78 @@
<template>
<view class="page" :style="pageStyle()">
<AppHeader title="个人中心" :back="false" />
<scroll-view scroll-y :style="contentStyle(226, false)">
<view class="profile-content">
<view class="profile-card" @click="openAccount"><view class="avatar"><AppIcon name="user" :size="80" /></view><view class="profile-main"><text class="profile-name">{{ displayName }}</text><text class="profile-phone">{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }}</text></view><text class="account-state">{{ loggedIn ? '演示账户' : '未登录' }}</text></view>
<view class="stat-row"><view class="stat-item" @click="go('/packages/finance/archive/archive')"><text class="stat-value">{{ completion }}%</text><text class="stat-label">档案完整度</text></view><view class="stat-item" @click="openReport"><text class="stat-value">{{ hasReport ? '已生成' : '暂无' }}</text><text class="stat-label">评估报告</text></view><view class="stat-item" @click="go('/packages/account/points/points')"><AppIcon name="points" :size="42" /><text class="stat-label">积分中心</text></view></view>
<view class="menu-list"><view class="menu-row" v-for="item in menus" :key="item.name" @click="open(item.key)"><AppIcon :name="item.icon" :size="46" /><text class="menu-name">{{ item.name }}</text><text v-if="item.note.length > 0" class="menu-note">{{ item.note }}</text><AppIcon name="chevron-right" :size="28" /></view></view>
<button class="logout" @click="logout">{{ loggedIn ? '退出登录' : '登录 / 注册' }}</button>
<text class="version-note">AI融资 · 让融资更简单</text>
</view>
</scroll-view>
<BottomNav active="profile" />
</view>
</template>
<script setup lang="uts">
import { usePageLayout } from '@/services/layout.uts'
import AppHeader from '@/components/AppHeader.uvue'
import BottomNav from '@/components/BottomNav.uvue'
import AppIcon from '@/components/AppIcon.uvue'
import { clearSession, getSession } from '@/services/api.uts'
import { activeAssessmentType, loadAssessmentDraft, draftText, assessmentProgress, showAgreementNotice } from '@/services/assessment.uts'
const { contentStyle, pageStyle } = usePageLayout()
type ProfileMenu = { icon: string, name: string, note: string, key: string }
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' }]
const loggedIn = ref(false)
const displayName = ref('登录 / 注册')
const maskedPhone = ref('')
const completion = ref(0)
const hasReport = ref(false)
onShow(() => {
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('/packages/finance/report/report'); else uni.showModal({ title: '暂无评估报告', content: '完善融资信息后,即可体验融资评估与方案匹配。', confirmText: '去完善', success: (result) => { if (result.confirm) go('/packages/finance/archive/archive') } }) }
const open = (key: string) => {
if (key == 'archive') go('/packages/finance/archive/archive')
else if (key == 'help') go('/packages/finance/assistant/assistant')
else if (key == 'security') uni.showModal({ title: '账户安全', content: loggedIn.value ? '当前账户:' + maskedPhone.value + '\n当前为本地演示登录,尚未进行真实身份认证。请勿使用真实密码体验。' : '当前尚未登录,可使用手机号进入演示账户。', showCancel: false, confirmText: '我知道了' })
else if (key == 'privacy') uni.showActionSheet({ itemList: ['查看隐私说明', '管理资料与授权'], success: (result) => { if (result.tapIndex == 0) showAgreementNotice('隐私说明'); else if (result.tapIndex == 1) { uni.setStorageSync('assessmentEditStep', 3); go('/packages/finance/assessment/assessment?type=' + activeAssessmentType()) } } })
else if (key == 'about') uni.showModal({ title: '关于 AI融资', content: '版本 V1.0.0\n帮助您整理融资信息、了解融资知识并体验智能方案匹配。当前为功能演示,评估结果不构成授信承诺。', showCancel: false, confirmText: '我知道了' })
}
const openAccount = () => { if (!loggedIn.value) go('/packages/account/login/login'); else open('security') }
const logout = () => {
if (!loggedIn.value) { go('/packages/account/login/login'); return }
uni.showModal({ title: '退出登录', content: '退出后,本机保存的评估草稿仍会保留。', confirmText: '退出登录', success: (result) => { if (result.confirm) { clearSession(); uni.reLaunch({ url: '/packages/account/login/login' }) } } })
}
</script>
<style>
.profile-content { padding: 26rpx 30rpx 90rpx; }
.profile-card { min-height: 166rpx; padding: 28rpx 24rpx; border-radius: 18rpx 18rpx 0 0; background-color: #fff; flex-direction: row; align-items: center; }
.avatar { width: 84rpx; height: 84rpx; align-items: center; justify-content: center; margin-right: 20rpx; flex-shrink: 0; }
.profile-main { flex: 1; min-width: 0; }
.profile-name { color: #23344e; font-size: 32rpx; font-weight: 700; }
.profile-phone { color: #8190a4; font-size: 24rpx; line-height: 36rpx; margin-top: 8rpx; }
.account-state { padding: 8rpx 12rpx; border-radius: 8rpx; background-color: #eef4fd; color: #6c84a5; font-size: 22rpx; flex-shrink: 0; margin-left: 10rpx; }
.stat-row { min-height: 136rpx; background-color: #fff; border-top: 1rpx solid #edf1f6; border-radius: 0 0 18rpx 18rpx; flex-direction: row; }
.stat-item { flex: 1; align-items: center; justify-content: center; }
.stat-value { font-size: 31rpx; font-weight: 700; color: #173864; line-height: 44rpx; }
.stat-label { color: #7d8ca1; font-size: 24rpx; margin-top: 10rpx; }
.menu-list { margin-top: 24rpx; padding: 0 24rpx; border-radius: 18rpx; background-color: #fff; }
.menu-row { min-height: 106rpx; flex-direction: row; align-items: center; border-bottom: 1rpx solid #edf1f6; }
.menu-name { flex: 1; font-size: 28rpx; color: #2a3a53; margin-left: 20rpx; }
.menu-note { color: #8793a6; font-size: 23rpx; margin-right: 12rpx; }
.logout { margin-top: 26rpx; height: 92rpx; line-height: 92rpx; border-radius: 16rpx; color: #56667d; background-color: #fff; border: 1rpx solid #e5ecf5; font-size: 28rpx; }
.version-note { color: #94a0b0; font-size: 22rpx; text-align: center; margin-top: 28rpx; }
</style>
+35
View File
@@ -0,0 +1,35 @@
import { API_BASE_URL, wait, requestJson } from '@/services/api.uts'
export { API_BASE_URL, saveSession } from '@/services/api.uts'
export async function requestSmsCode(phone: string): Promise<boolean> {
if (API_BASE_URL.length == 0) {
await wait(350)
return phone.length == 11
}
await requestJson('/auth/sms-code', 'POST', { phone })
return true
}
export async function loginWithSms(phone: string, code: string): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(300)
if (code.length != 6) throw new Error('验证码应为 6 位数字')
return 'demo-sms-token'
}
const result = await requestJson('/auth/sms-login', 'POST', { phone, code })
const token = result['token'] as string | null
if (token == null || token.length == 0) throw new Error('登录响应缺少 token')
return token
}
export async function loginWithPassword(phone: string, password: string): Promise<string> {
if (API_BASE_URL.length == 0) {
await wait(300)
return 'demo-password-token'
}
const result = await requestJson('/auth/password-login', 'POST', { phone, password })
const token = result['token'] as string | null
if (token == null || token.length == 0) throw new Error('登录响应缺少 token')
return token
}
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#0868F7">
<circle cx="16" cy="10" r="6"/>
<path d="M16 18C8.8 18 3 22.1 3 27.2 3 28.2 3.8 29 4.8 29h22.4c1 0 1.8-.8 1.8-1.8C29 22.1 23.2 18 16 18Z"/>
</svg>

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

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

After

Width:  |  Height:  |  Size: 622 B