From 4a693576068ebb684899b2bbaf89723105aa9b75 Mon Sep 17 00:00:00 2001 From: hujun Date: Mon, 21 Sep 2026 15:04:59 +0800 Subject: [PATCH] feat: add account profile, search, help and security features 1. add default avatar and multiple notification icons 2. update finance archive note and add form styles 3. add new pages: edit-profile, security, password, change-phone, search, help 4. refactor navigation action layout to support multiple actions 5. add search button to home page 6. add security code component and account service APIs 7. add help and search content services 8. update message notice to use svg icons 9. update asset section in finance assessment 10. update manifest appid and pack options --- README.md | 23 ++- manifest.json | 152 +++++++++--------- .../account/change-phone/change-phone.uvue | 38 +++++ packages/account/components/SecurityCode.uvue | 31 ++++ .../account/edit-profile/edit-profile.uvue | 83 ++++++++++ packages/account/messages/messages.uvue | 16 +- packages/account/password/password.uvue | 53 ++++++ packages/account/profile/profile.uvue | 22 +-- packages/account/security/security.uvue | 43 +++++ packages/account/services/account.uts | 103 ++++++++++++ packages/account/static/default-avatar.png | Bin 0 -> 43632 bytes .../account/static/notifications/approval.svg | 1 + .../account/static/notifications/gift.svg | 1 + .../account/static/notifications/payout.svg | 1 + .../static/notifications/system-bell.svg | 1 + packages/account/styles/forms.css | 10 ++ packages/content/help/help.uvue | 80 +++++++++ packages/content/search/search.uvue | 114 +++++++++++++ packages/content/services/help.uts | 35 ++++ packages/content/services/search.uts | 48 ++++++ packages/finance/archive/archive.uvue | 2 +- packages/finance/assessment/assessment.uvue | 23 ++- packages/finance/confirm/confirm.uvue | 16 +- pages.json | 6 + pages/home/home.uvue | 16 +- services/assessment.uts | 34 +++- services/layout.uts | 9 +- static/icons/search.svg | 1 + 28 files changed, 846 insertions(+), 116 deletions(-) create mode 100644 packages/account/change-phone/change-phone.uvue create mode 100644 packages/account/components/SecurityCode.uvue create mode 100644 packages/account/edit-profile/edit-profile.uvue create mode 100644 packages/account/password/password.uvue create mode 100644 packages/account/security/security.uvue create mode 100644 packages/account/services/account.uts create mode 100644 packages/account/static/default-avatar.png create mode 100644 packages/account/static/notifications/approval.svg create mode 100644 packages/account/static/notifications/gift.svg create mode 100644 packages/account/static/notifications/payout.svg create mode 100644 packages/account/static/notifications/system-bell.svg create mode 100644 packages/account/styles/forms.css create mode 100644 packages/content/help/help.uvue create mode 100644 packages/content/search/search.uvue create mode 100644 packages/content/services/help.uts create mode 100644 packages/content/services/search.uts create mode 100644 static/icons/search.svg diff --git a/README.md b/README.md index f33dd34..91aeeb6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # AI 融资移动端 -基于 uni-app x、Vue 3、UTS 的移动端原型,包含 19 个页面。使用 HBuilderX(VDOM 模式)开发,已验证 5.24 和 5.26 的本地编译;本项目没有独立的 npm 构建入口。 +基于 uni-app x、Vue 3、UTS 的移动端原型,包含 25 个页面。使用 HBuilderX(VDOM 模式)开发,已验证 5.24 和 5.26 的本地编译;本项目没有独立的 npm 构建入口。 ## 本地运行 @@ -33,7 +33,7 @@ $projectPath = (Get-Location).Path - 页面尺寸和安全区计算集中在 `services/layout.uts`。新增页面应复用这里的逻辑,并检查小屏、横屏、键盘弹出及底部安全区。 - 微信小程序首页在右侧预留胶囊区域与 8px 间距;公共页面标题采用左右对称的安全留白,始终按屏幕中心定位。获取胶囊失败时按 104px 避让。产品分享入口在小程序端放在内容区,App 和 Web 仍保留在标题栏右侧。导航不新增顶部空行,顶部按状态栏和安全区计算。 -- 首页右上角只保留消息通知;微信端通知按钮与胶囊等高、顶部对齐,左侧 Logo 对齐胶囊的垂直中心;其他端仍在导航栏内居中。AI 助手保留首页横幅及快捷入口。 +- 首页右上角依次为搜索、消息通知;微信端两个按钮与胶囊等高、顶部对齐,按钮组向左展开,左侧 Logo 对齐胶囊的垂直中心;其他端仍在导航栏内居中。AI 助手保留首页横幅及快捷入口。 - 原生样式遵循 uni-app x 支持范围。仅 Web/小程序支持的样式用条件编译隔离;修改 UTS 类型后应重新验证 Android 编译。 - 首页及跨包共用的轻量位图位于 `static/mobile/`,专属大图位于各分包的 `static/`,原始图片保留在 `static/images/` 与 `static/icons-generated/`。箭头继续使用 `static/icons/` 下的 SVG。 - 底栏使用 `static/mobile/navigation/` 的专用位图,由现有高清图标按可见边界裁切、等高居中得到。更换素材时保持一致的图案高度和画布留白,避免仅统一图片元素尺寸却出现视觉错位。 @@ -44,18 +44,33 @@ $projectPath = (Get-Location).Path 个人与企业评估分别保存本机草稿;选图仅供本机预览,资料上传和数据查询尚未接入。课程章节进度、消息已读与体验签到均保存在当前设备,不代表服务器账户数据。 +个人中心顶部的头像、昵称、手机号区域统一进入修改资料;账户安全独立提供修改密码、验证码重设密码及换绑手机号。昵称和头像按当前手机号隔离保存在本机,头像使用 500KB 以内的 JPG / PNG / WebP 数据,不保存会失效的临时路径,不修改融资档案的实名信息。真实账户认证尚未接入,修改密码和换绑手机号不会在本机模拟成功,也不保存密码、验证码或其散列。安全接口成功后清除当前会话并要求重新登录;验证码计时仅在服务端发送成功后启动。 + +账户接口集中在 `packages/account/services/account.uts`,请求携带当前会话的 `Authorization: Bearer `;后端需根据 token 确定账户,校验验证码用途与有效期、限制发送频率并完成旧会话撤销。预留: + +- `PUT /account/profile`:`nickname`、`avatar`(图片 data URL 或空字符串);昵称 2–20 字符。当前本地缓存用于个人中心回显,后续应接入服务端资料读取和持久头像存储。 +- `POST /account/security/sms-code`:`purpose`(`password` / `phone-current` / `phone-new`)与 `newPhone`(仅新号码验证传入);当前号码从认证账户读取。 +- `PUT /account/password`:`verification`(`password` / `sms`)、`oldPassword` 或 `code`、`newPassword`。新密码为 8–32 位可打印 ASCII,需含字母和数字,不允许空格。 +- `PUT /account/phone`:`newPhone`、`currentCode`、`newCode`;后端验证两组验证码及新号码占用情况。 + +资产与负债包含 `creditLoanCount`(信用贷款笔数)、`totalDebtBalance`(欠款总余额)、`monthlyLoanPayment`(贷款月还款金额)、`creditCardCount`(银行信用卡张数)、`creditCardTotalLimit`(总额度)、`creditCardUsedLimit`(已使用总额度)、`house` / `car`(有 / 无)。金额以元计,最多两位小数,数量为非负整数;保持原来的可跳过规则,填满八项后计为档案已完善。旧草稿的房车选项自动映射,新数字字段留空,不从旧存款区间或信用卡状态猜测金额;旧字段保留在草稿中以免丢失历史输入。 + 新闻与课程的数据定义位于 `packages/content/services/content.uts`,产品定义位于 `packages/finance/services/finance.uts`。产品列表与详情共用 ID;预留 `GET /matches`(`items` 数组)及 `GET /products/:id`(单个产品),字段见 `FinanceProduct`,其中 `amount` 为万元数值、`rate` 为年化百分比数值。申请入口目前仅引导准备资料,不向金融机构提交申请。 +首页搜索进入内容分包的 `search/search`,支持全部、新闻、融资课堂筛选及对应详情跳转。搜索服务位于 `packages/content/services/search.uts`,当前匹配本地标题、简介、分类和正文;空格分隔的多个关键词需同时命中。预留 `GET /content/search`,参数为 `keyword` 与 `type`(`all` / `news` / `lesson`),返回 `{ items: ContentSearchResult[] }`,每项包含 `id`、`type`、`title`、`summary`、`category`、`meta`、`image`。后端接入时需同步接入新闻和课程详情的数据源,确保返回的 ID 可查询详情。 + ## 页面与分包 +“帮助与反馈”从个人中心进入内容分包的 `help/help`,按账户使用、融资评估、资料与隐私分组展示可展开的问答,不跳转 AI 助手。数据与客服接入点位于 `packages/content/services/help.uts`;预留 `GET /help/faqs` 返回 `{ items: HelpQuestion[] }`。联系客服当前只展示暂未开放提示,不发起客服会话或提交反馈。 + `pages/` 只保留 `home/home.uvue`,也是唯一主包页面。页面注册统一维护在 `pages.json` 的 `pages` 和 `subPackages`,分包内的 `path` 相对于 `root`。 | 目录 | 内容 | 页面数 | | --- | --- | --- | | `pages/home` | 首页 | 1 | | `packages/finance` | 融资入口、评估、确认、分析、报告、匹配、产品、助手、档案 | 9 | -| `packages/account` | 启动引导、登录、个人中心、积分、消息 | 5 | -| `packages/content` | 新闻、文章、融资课堂、课程详情 | 4 | +| `packages/account` | 启动引导、登录、个人中心、修改资料、账户安全、修改密码、更换手机号、积分、消息 | 9 | +| `packages/content` | 搜索、帮助与反馈、新闻、文章、融资课堂、课程详情 | 6 | 当前各模块体积较小,优先保持完整业务链;普通内容统一一个包。新增功能先放入所属分包,再根据微信编译产物大小决定是否细分。单包以 2 MiB 为保守预算,需预留增长空间,最终体积以微信开发者工具为准。 diff --git a/manifest.json b/manifest.json index 6c79cd1..dc59dff 100644 --- a/manifest.json +++ b/manifest.json @@ -1,73 +1,81 @@ { - "name" : "ai融资", - "appid" : "__UNI__F40DD82", - "description" : "", - "versionName" : "1.0.0", - "versionCode" : "100", - "uni-app-x" : { - "styleIsolationVersion" : "2" - }, - /* 快应用特有相关 */ - "quickapp" : {}, - /* 小程序特有相关 */ - "mp-weixin" : { - "appid" : "", - "packOptions" : { - "ignore" : [ - { "type": "folder", "value": "static/images" }, - { "type": "folder", "value": "static/icons-generated" }, - { "type": "file", "value": "static/logo.png" } - ] - }, - "setting" : { - "urlCheck" : false - }, - "usingComponents" : true - }, - "mp-alipay" : { - "usingComponents" : true - }, - "mp-baidu" : { - "usingComponents" : true - }, - "mp-toutiao" : { - "usingComponents" : true - }, - "uniStatistics" : { - "enable" : false - }, - "vueVersion" : "3", - "app" : { - "distribute" : { - "icons" : { - "android" : { - "hdpi" : "", - "xhdpi" : "", - "xxhdpi" : "", - "xxxhdpi" : "" - } - } - } - }, - "app-android" : { - "distribute" : { - "modules" : {}, - "icons" : { - "hdpi" : "", - "xhdpi" : "", - "xxhdpi" : "", - "xxxhdpi" : "" - }, - "splashScreens" : { - "default" : {} - } - } - }, - "app-ios" : { - "distribute" : { - "modules" : {}, - "icons" : {}, - "splashScreens" : {} - } - } -} + "name": "ai融资", + "appid": "__UNI__204876E", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "uni-app-x": { + "styleIsolationVersion": "2" + }, + /* 快应用特有相关 */ + "quickapp": {}, + /* 小程序特有相关 */ + "mp-weixin": { + "appid": "wx8e92cbd69e97d2b8", + "packOptions": { + "ignore": [{ + "type": "folder", + "value": "static/images" + }, + { + "type": "folder", + "value": "static/icons-generated" + }, + { + "type": "file", + "value": "static/logo.png" + } + ] + }, + "setting": { + "urlCheck": false + }, + "usingComponents": true + }, + "mp-alipay": { + "usingComponents": true + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + }, + "vueVersion": "3", + "app": { + "distribute": { + "icons": { + "android": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "", + "xxxhdpi": "" + } + } + } + }, + "app-android": { + "distribute": { + "modules": {}, + "icons": { + "hdpi": "", + "xhdpi": "", + "xxhdpi": "", + "xxxhdpi": "" + }, + "splashScreens": { + "default": {} + } + } + }, + "app-ios": { + "distribute": { + "modules": {}, + "icons": {}, + "splashScreens": {} + } + } +} \ No newline at end of file diff --git a/packages/account/change-phone/change-phone.uvue b/packages/account/change-phone/change-phone.uvue new file mode 100644 index 0000000..7926bc2 --- /dev/null +++ b/packages/account/change-phone/change-phone.uvue @@ -0,0 +1,38 @@ + + + diff --git a/packages/account/components/SecurityCode.uvue b/packages/account/components/SecurityCode.uvue new file mode 100644 index 0000000..1572c28 --- /dev/null +++ b/packages/account/components/SecurityCode.uvue @@ -0,0 +1,31 @@ + + + diff --git a/packages/account/edit-profile/edit-profile.uvue b/packages/account/edit-profile/edit-profile.uvue new file mode 100644 index 0000000..e5500ba --- /dev/null +++ b/packages/account/edit-profile/edit-profile.uvue @@ -0,0 +1,83 @@ + + + diff --git a/packages/account/messages/messages.uvue b/packages/account/messages/messages.uvue index 5b5a229..420256b 100644 --- a/packages/account/messages/messages.uvue +++ b/packages/account/messages/messages.uvue @@ -7,7 +7,7 @@ {{ unreadCount > 0 ? unreadCount + ' 条未读消息' : '消息已全部阅读' }}全部已读 - + {{ item.title }}{{ item.time }}{{ item.desc }}{{ item.category }} @@ -22,7 +22,6 @@ 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('全部') @@ -67,15 +66,20 @@ .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; } + .notice { padding: 26rpx 0; border-bottom: 1rpx solid #e8eef6; flex-direction: row; align-items: center; } .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-icon { width: 76rpx; height: 76rpx; margin-right: 20rpx; position: relative; flex-shrink: 0; } + .notice-icon-surface { width: 100%; height: 100%; border-radius: 20rpx; background-color: #edf5ff; align-items: center; justify-content: center; } + .notice-icon-payout .notice-icon-surface { background-color: #eaf7f3; } + .notice-icon-gift .notice-icon-surface { background-color: #fff3e8; } + .notice-symbol { width: 46rpx; height: 46rpx; flex-shrink: 0; } + .notice-copy { flex: 1; min-width: 0; } .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; } + /* 红点独立于圆角背景,避免跨端圆角裁切。 */ + .unread { position: absolute; width: 15rpx; height: 15rpx; border-radius: 8rpx; background-color: #f04452; border: 2rpx solid #ffffff; right: -3rpx; top: -3rpx; z-index: 1; } .message-footer { color: #66758a; font-size: 22rpx; text-align: center; margin-top: 28rpx; } diff --git a/packages/account/password/password.uvue b/packages/account/password/password.uvue new file mode 100644 index 0000000..988825d --- /dev/null +++ b/packages/account/password/password.uvue @@ -0,0 +1,53 @@ + + + diff --git a/packages/account/profile/profile.uvue b/packages/account/profile/profile.uvue index 233d982..650d9a5 100644 --- a/packages/account/profile/profile.uvue +++ b/packages/account/profile/profile.uvue @@ -3,7 +3,7 @@ - {{ displayName }}{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }} + {{ displayName }}{{ loggedIn ? maskedPhone : '登录后管理您的融资信息' }} {{ completion }}%档案完整度{{ hasReport ? '已生成' : '暂无' }}评估报告积分中心 {{ item.name }}{{ item.note }} @@ -20,23 +20,24 @@ 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' + import { activeAssessmentType, assessmentProgress, showAgreementNotice } from '@/services/assessment.uts' + import { DEFAULT_AVATAR, getAccountProfile, maskPhone } from '@/packages/account/services/account.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 avatar = 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') + const profile = getAccountProfile() + maskedPhone.value = maskPhone(profile.phone); avatar.value = loggedIn.value ? profile.avatar : '' + const type = activeAssessmentType() + displayName.value = loggedIn.value ? profile.nickname : '登录 / 注册' completion.value = assessmentProgress(type).percent const latest = uni.getStorageSync('latestAssessment') hasReport.value = latest != null && typeof latest == 'object' @@ -45,12 +46,12 @@ 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 == 'help') go('/packages/content/help/help') + else if (key == 'security') go(loggedIn.value ? '/packages/account/security/security' : '/packages/account/login/login') 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 openAccount = () => { go(loggedIn.value ? '/packages/account/edit-profile/edit-profile' : '/packages/account/login/login') } 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' }) } } }) @@ -61,6 +62,7 @@ .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-avatar { width: 84rpx; height: 84rpx; border-radius: 42rpx; background-color: #eaf2ff; } .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; } diff --git a/packages/account/security/security.uvue b/packages/account/security/security.uvue new file mode 100644 index 0000000..029a341 --- /dev/null +++ b/packages/account/security/security.uvue @@ -0,0 +1,43 @@ + + + diff --git a/packages/account/services/account.uts b/packages/account/services/account.uts new file mode 100644 index 0000000..40f8801 --- /dev/null +++ b/packages/account/services/account.uts @@ -0,0 +1,103 @@ +import { API_BASE_URL, getSession } from '@/services/api.uts' +import { activeAssessmentType, loadAssessmentDraft, draftText } from '@/services/assessment.uts' + +export type AccountProfile = { nickname: string, avatar: string, phone: string } +export const DEFAULT_AVATAR = '/packages/account/static/default-avatar.png' +const profileKey = (phone: string): string => 'finance.account.profile:' + phone +export const maskPhone = (phone: string): string => phone.length == 11 ? phone.substring(0, 3) + '****' + phone.substring(7) : phone +export const validPhone = (phone: string): boolean => /^1[3-9][0-9]{9}$/.test(phone) +export const validCode = (code: string): boolean => /^[0-9]{6}$/.test(code) +export const validNewPassword = (password: string): boolean => /^[!-~]{8,32}$/.test(password) && /[A-Za-z]/.test(password) && /[0-9]/.test(password) + +export function requireAccount(): UTSJSONObject { + const session = getSession() + if (session == null || draftText(session, 'token').length == 0 || !validPhone(draftText(session, 'phone'))) throw new Error('请先登录后再操作') + return session +} +export function sameAccount(session: UTSJSONObject): boolean { + const current = getSession() + return current != null && draftText(current, 'token') == draftText(session, 'token') && draftText(current, 'phone') == draftText(session, 'phone') +} +export function getAccountProfile(): AccountProfile { + const session = getSession() + const phone = session == null ? '' : draftText(session, 'phone') + let nickname = '融资用户'; let avatar = '' + if (phone.length > 0) { + const saved = uni.getStorageSync(profileKey(phone)) + if (saved != null && typeof saved == 'object') { + nickname = draftText(saved as UTSJSONObject, 'nickname'); avatar = draftText(saved as UTSJSONObject, 'avatar') + } else { + const draft = loadAssessmentDraft(activeAssessmentType()) + if (draft != null && draftText(draft, 'phone') == phone && draftText(draft, 'name').length > 0) nickname = draftText(draft, 'name') + } + } + return { phone, nickname: nickname.length > 0 ? nickname : '融资用户', avatar } +} + +// 安全接口必须经过服务端认证。未配置服务时不模拟验证或修改成功。 +function accountRequest(path: string, method: RequestMethod, payload: UTSJSONObject): Promise { + return new Promise((resolve, reject) => { + const session = requireAccount() + if (API_BASE_URL.length == 0) { reject(new Error('账户服务暂未开放,请稍后再试')); return } + uni.request({ url: API_BASE_URL + path, method, data: payload, header: { Authorization: 'Bearer ' + draftText(session, 'token') }, timeout: 10000, + success: (response) => { + if (!sameAccount(session)) { reject(new Error('登录状态已变化,请重新进入页面')); return } + if (response.statusCode == 401) { reject(new Error('登录已失效,请重新登录')); return } + if (response.statusCode < 200 || response.statusCode >= 300) { reject(new Error('操作失败,请核对信息后重试')); return } + resolve(response.data == null ? new UTSJSONObject() : response.data as UTSJSONObject) + }, fail: () => reject(new Error('网络连接失败,请稍后重试')) + }) + }) +} +export async function saveAccountProfile(nickname: string, avatar: string): Promise { + const session = requireAccount(); const phone = draftText(session, 'phone') + const name = nickname.trim() + if (name.length < 2 || name.length > 20) throw new Error('昵称请输入 2–20 个字符') + if (API_BASE_URL.length > 0) await accountRequest('/account/profile', 'PUT', { nickname: name, avatar }) + if (!sameAccount(session)) throw new Error('登录状态已变化,请重新进入页面') + uni.setStorageSync(profileKey(phone), { nickname: name, avatar }) +} +export async function requestSecurityCode(purpose: string, newPhone: string): Promise { + if (!['password', 'phone-current', 'phone-new'].includes(purpose)) throw new Error('验证码用途无效') + if (purpose == 'phone-new' && !validPhone(newPhone)) throw new Error('请输入正确的新手机号') + await accountRequest('/account/security/sms-code', 'POST', { purpose, newPhone: purpose == 'phone-new' ? newPhone : '' }) +} +export async function changeAccountPassword(mode: string, oldPassword: string, code: string, newPassword: string): Promise { + if (!validNewPassword(newPassword)) throw new Error('新密码需为 8–32 位,包含字母和数字,可使用符号') + if (mode == 'password') { + if (oldPassword.length == 0) throw new Error('请输入当前密码') + if (oldPassword == newPassword) throw new Error('新密码不能与当前密码相同') + } else if (mode != 'sms' || !validCode(code)) throw new Error('请输入 6 位数字验证码') + await accountRequest('/account/password', 'PUT', { verification: mode, oldPassword: mode == 'password' ? oldPassword : '', code: mode == 'sms' ? code : '', newPassword }) +} +export async function changeAccountPhone(newPhone: string, currentCode: string, newCode: string): Promise { + const session = requireAccount() + if (!validPhone(newPhone)) throw new Error('请输入正确的新手机号') + if (newPhone == draftText(session, 'phone')) throw new Error('新手机号不能与当前手机号相同') + if (!validCode(currentCode) || !validCode(newCode)) throw new Error('请输入两组 6 位数字验证码') + await accountRequest('/account/phone', 'PUT', { newPhone, currentCode, newCode }) +} + +// 限制头像体积,避免把临时路径或过大的图片写入账户缓存。 +export function readAvatar(path: string): Promise { + return new Promise((resolve, reject) => { + // #ifdef WEB + fetch(path).then((response) => response.blob()).then((blob) => { + if (blob.size > 512000 || !['image/jpeg', 'image/png', 'image/webp'].includes(blob.type)) { reject(new Error('请选择 500KB 以内的 JPG、PNG 或 WebP 图片')); return } + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = () => reject(new Error('图片读取失败,请重新选择')) + reader.readAsDataURL(blob) + }).catch(() => reject(new Error('图片读取失败,请重新选择'))) + // #endif + // #ifndef WEB + uni.getFileSystemManager().readFile({ filePath: path, encoding: 'base64', success: (result) => { + const data = result.data as string + if (data.length > 683000) { reject(new Error('请选择 500KB 以内的图片')); return } + const mime = data.startsWith('/9j/') ? 'image/jpeg' : (data.startsWith('iVBOR') ? 'image/png' : (data.startsWith('UklGR') ? 'image/webp' : '')) + if (mime.length == 0) { reject(new Error('请选择 JPG、PNG 或 WebP 图片')); return } + resolve('data:' + mime + ';base64,' + data) + }, fail: () => reject(new Error('图片读取失败,请重新选择')) }) + // #endif + }) +} diff --git a/packages/account/static/default-avatar.png b/packages/account/static/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..09b9aede63aa0739e3cdcbfb0eca21ed2622424d GIT binary patch literal 43632 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 diff --git a/packages/account/static/notifications/gift.svg b/packages/account/static/notifications/gift.svg new file mode 100644 index 0000000..79f3934 --- /dev/null +++ b/packages/account/static/notifications/gift.svg @@ -0,0 +1 @@ + diff --git a/packages/account/static/notifications/payout.svg b/packages/account/static/notifications/payout.svg new file mode 100644 index 0000000..fac1b71 --- /dev/null +++ b/packages/account/static/notifications/payout.svg @@ -0,0 +1 @@ + diff --git a/packages/account/static/notifications/system-bell.svg b/packages/account/static/notifications/system-bell.svg new file mode 100644 index 0000000..6e2cfe0 --- /dev/null +++ b/packages/account/static/notifications/system-bell.svg @@ -0,0 +1 @@ + diff --git a/packages/account/styles/forms.css b/packages/account/styles/forms.css new file mode 100644 index 0000000..9ae8b2b --- /dev/null +++ b/packages/account/styles/forms.css @@ -0,0 +1,10 @@ +.account-form-content { padding: 30rpx; } +.form-title { font-size: 36rpx; line-height: 52rpx; font-weight: 700; color: #23344e; } +.form-description { font-size: 25rpx; line-height: 40rpx; color: #738198; margin-top: 14rpx; } +.form-card { padding: 8rpx 26rpx 22rpx; margin-top: 28rpx; border-radius: 18rpx; background-color: #ffffff; } +.form-label { font-size: 26rpx; font-weight: 600; color: #405675; margin-top: 22rpx; } +.account-input { height: 96rpx; border-bottom: 1rpx solid #edf1f6; font-size: 28rpx; color: #23344e; } +.form-hint { font-size: 24rpx; line-height: 38rpx; color: #8793a6; margin-top: 22rpx; } +.form-error { font-size: 25rpx; line-height: 38rpx; color: #d64040; margin-top: 22rpx; } +.form-submit { margin: 30rpx 0 0; } +.form-link { font-size: 26rpx; line-height: 40rpx; padding: 20rpx 0; color: #0868f7; text-align: center; } diff --git a/packages/content/help/help.uvue b/packages/content/help/help.uvue new file mode 100644 index 0000000..d8710b5 --- /dev/null +++ b/packages/content/help/help.uvue @@ -0,0 +1,80 @@ + + + diff --git a/packages/content/search/search.uvue b/packages/content/search/search.uvue new file mode 100644 index 0000000..c3f262a --- /dev/null +++ b/packages/content/search/search.uvue @@ -0,0 +1,114 @@ + + + + + diff --git a/packages/content/services/help.uts b/packages/content/services/help.uts new file mode 100644 index 0000000..fb25615 --- /dev/null +++ b/packages/content/services/help.uts @@ -0,0 +1,35 @@ +import { API_BASE_URL, requestJson } from '@/services/api.uts' + +export type HelpQuestion = { id: string, category: string, question: string, answer: string } +export const helpCategories: string[] = ['账户使用', '融资评估', '资料与隐私'] +const questions: HelpQuestion[] = [ + { id: 'profile', category: '账户使用', question: '如何修改头像和昵称?', answer: '在“我的”页面,点击顶部头像、昵称和手机号所在的区域进入修改资料。更换头像或编辑昵称后,点击“保存资料”即可。昵称不会替代融资档案中的实名信息。' }, + { id: 'password', category: '账户使用', question: '忘记登录密码怎么办?', answer: '可先使用手机号验证码登录,再进入“我的 → 账户安全 → 验证码重设密码”。如果记得当前密码,也可以选择“修改登录密码”。修改成功后需要重新登录。' }, + { id: 'phone', category: '账户使用', question: '如何更换绑定手机号?', answer: '进入“我的 → 账户安全 → 绑定手机号”,填写新手机号,并分别验证当前号码与新号码。换绑成功后使用新号码登录。如果原号码无法接收短信,需要通过官方客服核验身份。' }, + { id: 'assessment', category: '融资评估', question: '如何开始融资评估?', answer: '从首页选择个人或企业融资入口,依次填写基本信息、收入或经营信息、资产与负债情况,再核对并确认。已有资料可在“我的融资档案”中继续完善。' }, + { id: 'result', category: '融资评估', question: '评估结果是否代表贷款已经获批?', answer: '不代表。评估结果用于帮助了解融资需求和方案,不是金融机构的授信或放款承诺。实际额度、利率、审批与放款结果,以相关机构的审核和合同为准。' }, + { id: 'amount', category: '融资评估', question: '资产与负债信息应该怎么填写?', answer: '信用贷款笔数、信用卡张数填写非负整数,没有则填 0。欠款余额、月还款金额、信用卡额度与已使用额度以元填写,最多保留两位小数;房产、车辆按实际情况选择“有”或“无”。' }, + { id: 'files', category: '资料与隐私', question: '选择资料或勾选授权会立即上传、查询吗?', answer: '当前选择的资料仅用于本机预览,勾选授权仅保存您的选择,不会自动上传资料或查询外部数据。使用相关服务前,请确认授权对象、用途和范围。' }, + { id: 'draft', category: '资料与隐私', question: '退出登录后,填写的资料还在吗?', answer: '退出登录会结束当前会话,本机保存的评估草稿仍会保留。个人与企业评估分别保存。清理应用数据、更换设备或卸载应用可能使本地资料丢失,请勿将本机草稿视为云端备份。' } +] + +export async function fetchHelpQuestions(): Promise { + if (API_BASE_URL.length == 0) return questions.slice() + const response = await requestJson('/help/faqs', 'GET', {}) + const items = response['items'] as UTSJSONObject[] | null + if (items == null) throw new Error('帮助内容加载失败') + const results: HelpQuestion[] = [] + items.forEach((item) => { + const id = item['id'] as string ?? ''; const category = item['category'] as string ?? '' + const question = item['question'] as string ?? ''; const answer = item['answer'] as string ?? '' + if (id.length > 0 && helpCategories.includes(category) && question.length > 0 && answer.length > 0 && !results.some((entry: HelpQuestion): boolean => entry.id == id)) { + const result: HelpQuestion = { id, category, question, answer }; results.push(result) + } + }) + return results +} + +export function contactCustomerService(): void { + // 后续在此接入各端客服渠道;当前不发起会话、拨号或发送反馈。 + uni.showModal({ title: '联系客服', content: '客服服务暂未开放,请稍后再试。', showCancel: false, confirmText: '我知道了' }) +} diff --git a/packages/content/services/search.uts b/packages/content/services/search.uts new file mode 100644 index 0000000..17d3c3c --- /dev/null +++ b/packages/content/services/search.uts @@ -0,0 +1,48 @@ +import { API_BASE_URL, requestJson } from '@/services/api.uts' +import { newsArticles, financeLessons, ContentSection } from './content.uts' + +export type ContentSearchResult = { + id: string, type: string, title: string, summary: string, category: string, meta: string, image: string +} + +const sectionText = (sections: ContentSection[]): string => sections.map((item: ContentSection): string => item.title + ' ' + item.body).join(' ') +const matches = (text: string, words: string[]): boolean => words.every((word: string): boolean => text.toLowerCase().includes(word)) +const readText = (item: UTSJSONObject, key: string): string => typeof item[key] == 'string' ? item[key] as string : '' + +// type: all / news / lesson。接入后端时返回相同类型的 items,详情沿用内容 ID。 +export async function searchContent(keyword: string, type: string): Promise { + const query = keyword.trim().replace(/\s+/g, ' ') + const results: ContentSearchResult[] = [] + if (query.length == 0) return results + if (API_BASE_URL.length > 0) { + const response = await requestJson('/content/search', 'GET', { keyword: query, type }) + const items = response['items'] as UTSJSONObject[] | null + if (items == null) throw new Error('搜索结果加载失败,请重试') + items.forEach((item: UTSJSONObject) => { + const kind = readText(item, 'type'); const id = readText(item, 'id'); const title = readText(item, 'title') + if ((kind == 'news' || kind == 'lesson') && (type == 'all' || type == kind) && id.length > 0 && title.length > 0) { + const result: ContentSearchResult = { id, type: kind, title, summary: readText(item, 'summary'), category: readText(item, 'category'), meta: readText(item, 'meta'), image: readText(item, 'image') } + results.push(result) + } + }) + return results + } + const words = query.toLowerCase().split(' ') + if (type == 'all' || type == 'news') { + newsArticles.forEach((item) => { + if (matches(item.title + ' ' + item.summary + ' ' + item.category + ' ' + sectionText(item.sections), words)) { + const result: ContentSearchResult = { id: item.id, type: 'news', title: item.title, summary: item.summary, category: item.category, meta: item.date, image: item.image } + results.push(result) + } + }) + } + if (type == 'all' || type == 'lesson') { + financeLessons.forEach((item) => { + if (matches(item.title + ' ' + item.desc + ' ' + item.tag + ' ' + item.intro + ' ' + sectionText(item.chapters), words)) { + const result: ContentSearchResult = { id: item.id, type: 'lesson', title: item.title, summary: item.desc, category: item.tag, meta: item.duration + ' · ' + item.chapters.length + ' 节', image: item.image } + results.push(result) + } + }) + } + return results +} diff --git a/packages/finance/archive/archive.uvue b/packages/finance/archive/archive.uvue index 041bd2b..ae2a042 100644 --- a/packages/finance/archive/archive.uvue +++ b/packages/finance/archive/archive.uvue @@ -23,7 +23,7 @@ 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: '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) } diff --git a/packages/finance/assessment/assessment.uvue b/packages/finance/assessment/assessment.uvue index eb512bd..19508ca 100644 --- a/packages/finance/assessment/assessment.uvue +++ b/packages/finance/assessment/assessment.uvue @@ -31,11 +31,17 @@ - - - - - 其他资产 + 贷款信息 + 信用贷款笔数笔 + 欠款总余额元 + 贷款月还款金额元 + 信用卡信息 + 银行信用卡张数张 + 总额度元 + 已使用总额度元 + 房车信息 + + 资料选择 (选填) @@ -56,7 +62,7 @@ import AppHeader from '@/components/AppHeader.uvue' import AppIcon from '@/components/AppIcon.uvue' import AssessmentSelect from '@/packages/finance/components/AssessmentSelect.uvue' - import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice } from '@/services/assessment.uts' + import { AssessmentForm, SelectedDocument, DataAuthorization, emptyAssessmentForm, activeAssessmentType, loadAssessmentDraft, formFromDraft, documentsFromDraft, saveAssessmentDraft, showAgreementNotice, assetValidationMessage } from '@/services/assessment.uts' const { bottomStyle, contentStyle, pageStyle } = usePageLayout() const showAgreement = (title: string) => { showAgreementNotice(title) } const step = ref(0) @@ -74,7 +80,7 @@ const saveDraft = () => { if (!initialized) return const source = form.value - 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 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, creditLoanCount: source.creditLoanCount, totalDebtBalance: source.totalDebtBalance, monthlyLoanPayment: source.monthlyLoanPayment, creditCardCount: source.creditCardCount, creditCardTotalLimit: source.creditCardTotalLimit, creditCardUsedLimit: source.creditCardUsedLimit, enterpriseName: source.enterpriseName, businessCode: source.businessCode, annualRevenue: source.annualRevenue, operatingYears: source.operatingYears } if (draftForm.city == '其他城市' && customCity.value.trim().length > 0) draftForm.city = customCity.value.trim() saveAssessmentDraft(assessmentType.value, step.value, draftForm, documents.value, authorizations.value, documentConsent.value, dataConsent.value) } @@ -145,6 +151,7 @@ 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' ? '请输入有效的年营业收入' : '请输入有效的月收入' } + if (step.value == 2) message = assetValidationMessage(form.value) 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 @@ -167,6 +174,8 @@ .type-active { color: #0868f7; font-weight: 600; background-color: #fff; } .section-hint { color: #738198; font-size: 24rpx; line-height: 36rpx; margin-top: 30rpx; } .form-list { margin-top: 14rpx; } + .asset-section-title { margin-top: 28rpx; font-size: 26rpx; line-height: 38rpx; color: #738198; font-weight: 600; } + .field-unit { margin-left: 12rpx; font-size: 25rpx; color: #738198; flex-shrink: 0; } .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; } diff --git a/packages/finance/confirm/confirm.uvue b/packages/finance/confirm/confirm.uvue index d8bfd5e..b64bc38 100644 --- a/packages/finance/confirm/confirm.uvue +++ b/packages/finance/confirm/confirm.uvue @@ -19,7 +19,7 @@ import { usePageLayout } from '@/services/layout.uts' import AppHeader from '@/components/AppHeader.uvue' import { submitAssessment } from '@/packages/finance/services/api.uts' - import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft } from '@/services/assessment.uts' + import { activeAssessmentType, loadAssessmentDraft, draftText, documentsFromDraft, ownershipFromDraft, formFromDraft, assetValidationMessage } from '@/services/assessment.uts' const { pageStyle, contentStyle, bottomStyle } = usePageLayout() type InfoRow = { label: string, value: string } type InfoSection = { title: string, step: number, rows: InfoRow[], expanded: boolean } @@ -31,6 +31,7 @@ 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 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 valueWithUnit = (saved: UTSJSONObject, key: string, unit: string): string => { const value = draftText(saved, key); return value.length > 0 ? value + ' ' + unit : '未填写' } const loadDraft = () => { assessmentType.value = activeAssessmentType() const saved = loadAssessmentDraft(assessmentType.value) @@ -62,7 +63,16 @@ 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: 2, rows: [ + { label: '信用贷款笔数', value: valueWithUnit(saved, 'creditLoanCount', '笔') }, + { label: '欠款总余额', value: valueWithUnit(saved, 'totalDebtBalance', '元') }, + { label: '贷款月还款金额', value: valueWithUnit(saved, 'monthlyLoanPayment', '元') }, + { label: '银行信用卡张数', value: valueWithUnit(saved, 'creditCardCount', '张') }, + { label: '总额度', value: valueWithUnit(saved, 'creditCardTotalLimit', '元') }, + { label: '已使用总额度', value: valueWithUnit(saved, 'creditCardUsedLimit', '元') }, + { label: '是否有房', value: ownershipFromDraft(saved, 'house').length > 0 ? ownershipFromDraft(saved, 'house') : '未填写' }, + { label: '是否有车', value: ownershipFromDraft(saved, 'car').length > 0 ? ownershipFromDraft(saved, 'car') : '未填写' } + ], 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 } @@ -85,6 +95,8 @@ 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 } + const assetError = assetValidationMessage(formFromDraft(saved)) + if (assetError.length > 0) { uni.showToast({ title: assetError, icon: 'none' }); edit(2); return } submitting.value = true try { await submitAssessment(payload.value); uni.redirectTo({ url: '/packages/finance/analyzing/analyzing' }) } catch (error) { uni.showToast({ title: error instanceof Error ? error.message : '提交失败,请重试', icon: 'none' }) } diff --git a/pages.json b/pages.json index 227febe..655cc4c 100644 --- a/pages.json +++ b/pages.json @@ -23,6 +23,10 @@ { "path": "index/index", "style": { "navigationStyle": "custom" } }, { "path": "login/login", "style": { "navigationStyle": "custom" } }, { "path": "profile/profile", "style": { "navigationStyle": "custom" } }, + { "path": "edit-profile/edit-profile", "style": { "navigationStyle": "custom" } }, + { "path": "security/security", "style": { "navigationStyle": "custom" } }, + { "path": "password/password", "style": { "navigationStyle": "custom" } }, + { "path": "change-phone/change-phone", "style": { "navigationStyle": "custom" } }, { "path": "points/points", "style": { "navigationStyle": "custom" } }, { "path": "messages/messages", "style": { "navigationStyle": "custom" } } ] @@ -30,6 +34,8 @@ { "root": "packages/content", "pages": [ + { "path": "search/search", "style": { "navigationStyle": "custom" } }, + { "path": "help/help", "style": { "navigationStyle": "custom" } }, { "path": "news/news", "style": { "navigationStyle": "custom" } }, { "path": "article/article", "style": { "navigationStyle": "custom" } }, { "path": "classroom/classroom", "style": { "navigationStyle": "custom" } }, diff --git a/pages/home/home.uvue b/pages/home/home.uvue index b436e5a..a4f8756 100644 --- a/pages/home/home.uvue +++ b/pages/home/home.uvue @@ -4,8 +4,11 @@ - - + + + + + @@ -60,11 +63,12 @@ .safe { } .home-bar { height: 104rpx; padding: 0 28rpx; flex-direction: row; align-items: center; justify-content: space-between; position: relative; } .home-brand { flex-shrink: 0; justify-content: center; } - .header-actions { width: 62rpx; height: 62rpx; margin-left: 16rpx; flex-shrink: 0; } - .round-action { width: 100%; height: 100%; position: relative; flex-shrink: 0; } - .action-surface { width: 100%; height: 100%; border-radius: 50%; background-color: #eef5ff; align-items: center; justify-content: center; } + .header-actions { width: 140rpx; height: 62rpx; margin-left: 16rpx; flex-shrink: 0; flex-direction: row; } + .round-action { flex: 1; height: 100%; position: relative; } + .action-following { margin-left: 16rpx; } + .action-surface { width: 100%; height: 100%; align-items: center; justify-content: center; } .header-icon { width: 36rpx; height: 36rpx; flex-shrink: 0; } - /* 红点与圆形背景同级,避免被跨端 view 的圆角裁切。 */ + /* 红点独立定位,不影响图标居中及点击区域。 */ .red-dot { position: absolute; width: 16rpx; height: 16rpx; right: 2rpx; top: 2rpx; z-index: 1; background-color: #ff4d4f; border-radius: 50%; border: 2rpx solid #fff; } .home-scroll { } .home-content { padding: 18rpx 28rpx 180rpx; } diff --git a/services/assessment.uts b/services/assessment.uts index 0191ad2..2d71e14 100644 --- a/services/assessment.uts +++ b/services/assessment.uts @@ -2,13 +2,15 @@ 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, + creditLoanCount: string, totalDebtBalance: string, monthlyLoanPayment: string, + creditCardCount: string, creditCardTotalLimit: string, creditCardUsedLimit: 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: '' } + return { name: '', idCard: '', phone: '', city: '', maritalStatus: '', occupation: '', company: '', monthlyIncome: '', socialSecurity: '', housingFund: '', house: '', car: '', savings: '', creditCard: '', otherAssets: '', creditLoanCount: '', totalDebtBalance: '', monthlyLoanPayment: '', creditCardCount: '', creditCardTotalLimit: '', creditCardUsedLimit: '', enterpriseName: '', businessCode: '', annualRevenue: '', operatingYears: '' } } export function draftText(saved: UTSJSONObject, key: string): string { const value = saved[key] @@ -35,10 +37,32 @@ 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'), + house: ownershipFromDraft(saved, 'house'), car: ownershipFromDraft(saved, 'car'), savings: draftText(saved, 'savings'), creditCard: draftText(saved, 'creditCard'), otherAssets: draftText(saved, 'otherAssets'), + creditLoanCount: draftText(saved, 'creditLoanCount'), totalDebtBalance: draftText(saved, 'totalDebtBalance'), monthlyLoanPayment: draftText(saved, 'monthlyLoanPayment'), + creditCardCount: draftText(saved, 'creditCardCount'), creditCardTotalLimit: draftText(saved, 'creditCardTotalLimit'), creditCardUsedLimit: draftText(saved, 'creditCardUsedLimit'), enterpriseName: draftText(saved, 'enterpriseName'), businessCode: draftText(saved, 'businessCode'), annualRevenue: draftText(saved, 'annualRevenue'), operatingYears: draftText(saved, 'operatingYears') } } +export function ownershipFromDraft(saved: UTSJSONObject, key: string): string { + const value = draftText(saved, key) + if (value == '有' || value == '无') return value + if (value == '无房产' || value == '无车辆') return '无' + if (['全款房产', '按揭房产', '全款车辆', '按揭车辆'].includes(value)) return '有' + return '' +} +export function assetValidationMessage(form: AssessmentForm): string { + const counts = [form.creditLoanCount, form.creditCardCount] + const countLabels = ['信用贷款笔数', '银行信用卡张数'] + for (let i = 0; i < counts.length; i++) { + if (counts[i].length > 0 && !/^[0-9]{1,4}$/.test(counts[i])) return countLabels[i] + '请输入非负整数' + } + const amounts = [form.totalDebtBalance, form.monthlyLoanPayment, form.creditCardTotalLimit, form.creditCardUsedLimit] + const amountLabels = ['欠款总余额', '贷款月还款金额', '总额度', '已使用总额度'] + for (let i = 0; i < amounts.length; i++) { + if (amounts[i].length > 0 && !/^[0-9]{1,12}([.][0-9]{1,2})?$/.test(amounts[i])) return amountLabels[i] + '请输入有效金额,最多两位小数' + } + return '' +} export function documentsFromDraft(saved: UTSJSONObject): SelectedDocument[] { const results: SelectedDocument[] = [] const items = saved['documents'] as UTSJSONObject[] | null @@ -56,6 +80,8 @@ export function saveAssessmentDraft(type: string, step: number, form: Assessment 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, + creditLoanCount: form.creditLoanCount, totalDebtBalance: form.totalDebtBalance, monthlyLoanPayment: form.monthlyLoanPayment, + creditCardCount: form.creditCardCount, creditCardTotalLimit: form.creditCardTotalLimit, creditCardUsedLimit: form.creditCardUsedLimit, enterpriseName: form.enterpriseName, businessCode: form.businessCode, annualRevenue: form.annualRevenue, operatingYears: form.operatingYears, documents: files, authorizations: grants, documentConsent, dataConsent } @@ -74,9 +100,9 @@ export function assessmentProgress(type: string): AssessmentProgress { 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 assets = ['house', 'car', 'creditLoanCount', 'totalDebtBalance', 'monthlyLoanPayment', 'creditCardCount', 'creditCardTotalLimit', 'creditCardUsedLimit'] 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) } + 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).trim().length > 0) && assetValidationMessage(formFromDraft(saved)).length == 0, authorizationSelected: documentsFromDraft(saved).length > 0 || (grants != null && grants.length > 0) } } diff --git a/services/layout.uts b/services/layout.uts index 7777a4d..56f3627 100644 --- a/services/layout.uts +++ b/services/layout.uts @@ -7,7 +7,7 @@ export type PageLayout = { statusStyle: (offset: number) => string, navigationStyle: () => string, navigationTitleStyle: () => string, - navigationActionStyle: (headerHeight: number) => string, + navigationActionStyle: (headerHeight: number, actionCount: number) => string, navigationBrandStyle: (headerHeight: number) => string, bottomStyle: (padding: number) => string } @@ -63,13 +63,14 @@ export function usePageLayout(): PageLayout { return '' // #endif } - const navigationActionStyle = (headerHeight: number): string => { - // 微信通知按钮与系统胶囊等高、顶部对齐;App 保持导航行内居中。 + const navigationActionStyle = (headerHeight: number, actionCount: number): string => { + // 微信操作按钮与胶囊等高、顶部对齐,整组向左展开;App 保持导航行内居中。 // #ifdef MP-WEIXIN const size = capsuleHeight.value > 0 ? capsuleHeight.value : 32 const top = capsuleHeight.value > 0 ? Math.max(0, capsuleTop.value - status()) : Math.max(0, (rpx(headerHeight) - size) / 2) const right = capsuleLeft.value > 0 ? windowInfo.value.windowWidth - capsuleLeft.value + 8 : 104 - return 'position:absolute;top:' + top + 'px;right:' + right + 'px;width:' + size + 'px;height:' + size + 'px;' + const width = size * actionCount + rpx(16) * Math.max(0, actionCount - 1) + return 'position:absolute;top:' + top + 'px;right:' + right + 'px;width:' + width + 'px;height:' + size + 'px;' // #endif // #ifndef MP-WEIXIN return '' diff --git a/static/icons/search.svg b/static/icons/search.svg new file mode 100644 index 0000000..ba88515 --- /dev/null +++ b/static/icons/search.svg @@ -0,0 +1 @@ +