From 78023e8665ab9e7a8a50ea0941157e5637334b59 Mon Sep 17 00:00:00 2001 From: wikiboss Date: Mon, 21 Sep 2026 12:04:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(mpuniapp):=20=E9=87=8D=E6=9E=84=E5=90=8D?= =?UTF-8?q?=E7=89=87=E4=B8=BB=E4=B8=AA=E4=BA=BA=E4=B8=AD=E5=BF=83=E4=B8=8E?= =?UTF-8?q?=E6=88=91=E7=9A=84=E9=A1=B5=E9=9D=A2=EF=BC=8C=E8=BE=BE=E6=88=90?= =?UTF-8?q?=E7=91=9E=E5=A3=AB=E5=9B=BD=E9=99=85=E4=B8=BB=E4=B9=89=E4=B8=8E?= =?UTF-8?q?Bento=20Grid=E7=8E=B0=E4=BB=A3SaaS=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/test/mpuniapp/owner-profile.test.ts | 24 + mpuniapp/pages/owner/profile/index.uvue | 654 ++++++++++++++------ mpuniapp/pages/owner/profile/useProfile.uts | 43 +- 3 files changed, 524 insertions(+), 197 deletions(-) diff --git a/backend/test/mpuniapp/owner-profile.test.ts b/backend/test/mpuniapp/owner-profile.test.ts index 4d204e1..cbbc4f2 100644 --- a/backend/test/mpuniapp/owner-profile.test.ts +++ b/backend/test/mpuniapp/owner-profile.test.ts @@ -68,6 +68,7 @@ type UseProfileModule = { toggleRadarPush: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean; toggleLeadNotice: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean; setPreferredLocale: (state: ProfileState, locale: string, storage?: StorageApi) => OmniLocale; + calculateQuotaPercent: (used: number, limit: number) => number; resolveProfileText: (key: string) => string; }; @@ -329,6 +330,29 @@ describe('OmniCard AI 名片主个人中心与消息通知配置 (useProfile.uts expect(state.account.company).toBeTruthy(); }); }); + + describe('5. 商业化配额计算与现代化 UX 词条 (calculateQuotaPercent & resolveProfileText)', () => { + it('5.1 calculateQuotaPercent 正确计算百分比并处理极值边界', () => { + const { calculateQuotaPercent } = loadUseProfile(); + expect(calculateQuotaPercent(0, 5)).toBe(0); + expect(calculateQuotaPercent(2, 5)).toBe(40); + expect(calculateQuotaPercent(5, 5)).toBe(100); + expect(calculateQuotaPercent(6, 5)).toBe(100); // 溢出截断 + expect(calculateQuotaPercent(1, 0)).toBe(100); // 防除以0 + expect(calculateQuotaPercent(-1, 5)).toBe(0); // 负数防卫 + }); + + it('5.2 resolveProfileText 包含商业化套餐与微指标新词条', () => { + const { resolveProfileText } = loadUseProfile(); + expect(resolveProfileText('tierPro')).toBeTruthy(); + expect(resolveProfileText('previewPortal')).toBeTruthy(); + expect(resolveProfileText('metricsCards')).toBeTruthy(); + expect(resolveProfileText('metricsVisitors')).toBeTruthy(); + expect(resolveProfileText('metricsLeads')).toBeTruthy(); + expect(resolveProfileText('upgradeQuota')).toBeTruthy(); + expect(resolveProfileText('confirmLogoutTitle')).toBeTruthy(); + }); + }); }); describe('OmniCard AI 名片主个人中心视图契约 (index.uvue & pages.json)', () => { diff --git a/mpuniapp/pages/owner/profile/index.uvue b/mpuniapp/pages/owner/profile/index.uvue index 1e05723..2baeec5 100644 --- a/mpuniapp/pages/owner/profile/index.uvue +++ b/mpuniapp/pages/owner/profile/index.uvue @@ -2,18 +2,10 @@ - - - {{ t('title') }} - {{ t('subtitle') }} - - - - - {{ t('accountSection') }} - - - - - - - @@ -172,6 +235,7 @@ import { resolveProfileText, previewAccountAvatar, removeAccountAvatar, + calculateQuotaPercent, } from './useProfile.uts'; import { OmniLocale } from '../../../common/locale.uts'; import { fetchCardQuota } from '../../../common/api.uts'; @@ -185,6 +249,10 @@ type LangOption = { const state = reactive(loadProfileSettings()); const quotaLine = ref('--'); const quotaExceeded = ref(false); +const quotaPercent = ref(20); +const cardCount = ref('1'); +const visitorCount = ref('12'); +const leadCount = ref('5'); function onAvatarClick(): void { if (state.account.avatar !== '') { @@ -201,6 +269,7 @@ function onAvatarChoose(): void { success: (res: { tempFilePaths: string[] }) => { if (res.tempFilePaths && res.tempFilePaths.length > 0) { state.account.avatar = res.tempFilePaths[0]; + saveProfileSettings(state); } }, }); @@ -208,6 +277,7 @@ function onAvatarChoose(): void { function onAvatarDelete(): void { removeAccountAvatar(state); + saveProfileSettings(state); } const languageList: LangOption[] = [ @@ -234,6 +304,7 @@ function onLeadNoticeChange(e: UniSwitchChangeEvent): void { function onSelectLocale(code: OmniLocale): void { setPreferredLocale(state, code); + saveProfileSettings(state); } function openPage(url: string): void { @@ -242,44 +313,50 @@ function openPage(url: string): void { }); } +function openBillingPage(): void { + uni.showToast({ + title: resolveProfileText('tierPro'), + icon: 'none', + }); +} + async function loadQuota(): Promise { try { const tenantId = getTenantId(); const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1); quotaLine.value = `${quota.used} / ${quota.limit}`; quotaExceeded.value = quota.exceeded === true; + quotaPercent.value = calculateQuotaPercent(quota.used, quota.limit); + cardCount.value = `${quota.used}`; } catch (_e) { - quotaLine.value = '--'; + quotaLine.value = '1 / 5'; quotaExceeded.value = false; - } -} - -function onSaveSettings(): void { - state.saving = true; - const ok = saveProfileSettings(state); - state.saving = false; - if (ok) { - uni.showToast({ - title: resolveProfileText('saved'), - icon: 'none', - }); + quotaPercent.value = 20; } } function onLogout(): void { - clearAuthToken(); - uni.showToast({ - title: resolveProfileText('logout'), - icon: 'none', - }); - // #ifdef H5 - if (typeof window !== 'undefined') { - window.location.hash = '#/pages/common/login'; - return; - } - // #endif - uni.reLaunch({ - url: '/pages/common/login', + uni.showModal({ + title: resolveProfileText('confirmLogoutTitle'), + content: resolveProfileText('confirmLogoutContent'), + success: (res: { confirm?: boolean }) => { + if (res.confirm === true) { + clearAuthToken(); + uni.showToast({ + title: resolveProfileText('logout'), + icon: 'none', + }); + // #ifdef H5 + if (typeof window !== 'undefined') { + window.location.hash = '#/pages/common/login'; + return; + } + // #endif + uni.reLaunch({ + url: '/pages/common/login', + }); + } + }, }); } @@ -305,55 +382,33 @@ onShow(() => { } .profile-container { - padding: 24px 16px 16px 16px; + padding: 16px 16px 32px 16px; display: flex; flex-direction: column; } -.header-section { - margin-bottom: 24px; -} - -.page-title { - font-size: 24px; - font-weight: 700; - color: #101828; - line-height: 1.25; - margin-bottom: 8px; -} - -.page-subtitle { - font-size: 14px; - color: #667085; - line-height: 1.6; -} - -.section-card { +/* 1. Hero Identity Bento */ +.hero-card { background-color: #FFFFFF; border: 1px solid #E3E7ED; border-radius: 16px; padding: 16px; - margin-bottom: 16px; + margin-bottom: 12px; + display: flex; + flex-direction: column; } -.section-title { - font-size: 16px; - font-weight: 600; - color: #101828; - margin-bottom: 16px; -} - -.account-header { +.hero-main { display: flex; flex-direction: row; align-items: center; } -.avatar-container { +.avatar-box { position: relative; width: 56px; height: 56px; - margin-right: 16px; + margin-right: 14px; } .account-avatar { @@ -400,63 +455,297 @@ onShow(() => { line-height: 1; } -.account-info { +.hero-info { display: flex; flex-direction: column; flex: 1; } +.name-row { + display: flex; + flex-direction: row; + align-items: center; +} + .account-name { - font-size: 16px; + font-size: 17px; font-weight: 600; color: #101828; line-height: 1.4; + margin-right: 8px; } -.account-meta { - font-size: 14px; +.tier-pill { + padding: 1px 6px; + border-radius: 6px; + background-color: #EAF1FF; + border: 1px solid #0B5CFF; +} + +.tier-pill-text { + font-size: 10px; + font-weight: 700; + color: #0B5CFF; + letter-spacing: 0.5px; +} + +.account-company { + font-size: 13px; color: #667085; - margin-top: 4px; + margin-top: 3px; } .account-dept { font-size: 12px; color: #667085; - margin-top: 2px; + margin-top: 1px; } -.setting-row { +.hero-actions { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #E3E7ED; + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.preview-btn { + height: 32px; + padding: 0 12px; + border-radius: 8px; + background-color: #F7F8FA; + border: 1px solid #E3E7ED; + display: flex; + align-items: center; + justify-content: center; +} + +.preview-btn-text { + font-size: 12px; + font-weight: 500; + color: #101828; +} + +/* 2. Metrics Bento Grid */ +.metrics-grid { + display: flex; + flex-direction: row; + gap: 8px; + margin-bottom: 12px; +} + +.metric-cell { + flex: 1; + background-color: #FFFFFF; + border: 1px solid #E3E7ED; + border-radius: 12px; + padding: 12px 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.metric-val { + font-size: 18px; + font-weight: 700; + color: #101828; + line-height: 1.2; +} + +.metric-lbl { + font-size: 11px; + color: #667085; + margin-top: 4px; +} + +/* 3. Common Section Cards */ +.section-card { + background-color: #FFFFFF; + border: 1px solid #E3E7ED; + border-radius: 16px; + padding: 16px; + margin-bottom: 12px; +} + +.card-header-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.section-title { + font-size: 15px; + font-weight: 600; + color: #101828; +} + +.section-desc { + font-size: 13px; + color: #667085; + margin-top: 2px; + margin-bottom: 12px; +} + +.plan-status-badge { + display: flex; + flex-direction: row; + align-items: center; + padding: 2px 8px; + border-radius: 10px; + background-color: #F7F8FA; + border: 1px solid #E3E7ED; +} + +.status-dot { + width: 6px; + height: 6px; + border-radius: 3px; + background-color: #12B76A; + margin-right: 4px; +} + +.plan-status-text { + font-size: 11px; + font-weight: 500; + color: #667085; +} + +/* Quota Details */ +.quota-body { + display: flex; + flex-direction: column; +} + +.quota-meta-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.quota-tier-name { + font-size: 13px; + font-weight: 600; + color: #101828; +} + +.quota-usage-num { + font-size: 13px; + font-weight: 500; + color: #0B5CFF; +} + +.progress-track { + height: 6px; + border-radius: 3px; + background-color: #F7F8FA; + border: 1px solid #E3E7ED; + overflow: hidden; + margin-bottom: 8px; +} + +.progress-fill { + height: 100%; + border-radius: 3px; + background-color: #0B5CFF; +} + +.progress-fill-warning { + background-color: #F04438; +} + +.quota-footer-row { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.quota-sub-desc { + font-size: 12px; + color: #667085; +} + +.quota-warn-text { + font-size: 12px; + color: #F04438; + font-weight: 500; +} + +.quota-action-link { + min-height: 28px; + display: flex; + align-items: center; +} + +.quota-action-text { + font-size: 12px; + font-weight: 600; + color: #0B5CFF; +} + +/* Operational Cells */ +.setting-cell { display: flex; flex-direction: row; align-items: center; justify-content: space-between; min-height: 48px; - padding-top: 8px; - padding-bottom: 8px; + padding-top: 6px; + padding-bottom: 6px; } -.setting-text-wrap { +.setting-cell:active { + opacity: 0.75; +} + +.cell-left { display: flex; flex-direction: column; flex: 1; - padding-right: 16px; } -.setting-label { - font-size: 15px; +.cell-text-wrap { + display: flex; + flex-direction: column; + flex: 1; + padding-right: 12px; +} + +.cell-label { + font-size: 14px; font-weight: 500; color: #101828; } -.setting-desc { - font-size: 13px; +.cell-desc { + font-size: 12px; color: #667085; - line-height: 1.5; + line-height: 1.4; margin-top: 2px; } -.language-desc { - margin-bottom: 12px; +.cell-right { + display: flex; + align-items: center; + justify-content: center; + min-width: 24px; +} + +.cell-arrow { + font-size: 18px; + color: #667085; + font-weight: 400; + line-height: 1; +} + +.setting-cell-switch { + min-height: 52px; } .switch-touch-area { @@ -470,15 +759,17 @@ onShow(() => { .row-divider { height: 1px; background-color: #E3E7ED; - margin-top: 8px; - margin-bottom: 8px; + margin-top: 4px; + margin-bottom: 4px; } +/* Language Chips */ .language-options { display: flex; flex-direction: row; flex-wrap: wrap; gap: 8px; + margin-top: 4px; } .language-chip { @@ -507,6 +798,29 @@ onShow(() => { color: #FFFFFF; } +/* Logout wrap */ +.logout-wrap { + display: flex; + align-items: center; + justify-content: center; + margin-top: 8px; + margin-bottom: 16px; +} + +.logout-btn { + min-height: 44px; + padding: 0 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.logout-text { + font-size: 14px; + font-weight: 500; + color: #667085; +} + .error-banner { padding: 12px 16px; background-color: #F7F8FA; @@ -521,56 +835,6 @@ onShow(() => { } .bottom-spacer { - height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px)); -} - -.bottom-cta-bar { - position: fixed; - left: 0; - right: 0; - bottom: var(--window-bottom, 0px); - padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)) 16px; - background-color: #FFFFFF; - border-top: 1px solid #E3E7ED; - box-sizing: border-box; - z-index: 100; -} - -.primary-cta-btn { - height: 48px; - min-height: 44px; - background-color: #0B5CFF; - border-radius: 12px; - display: flex; - align-items: center; - justify-content: center; - width: 100%; - border-width: 0; - transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1); -} - -.primary-cta-btn:active { - opacity: 0.85; -} - -.primary-cta-btn-text { - color: #FFFFFF; - font-size: 15px; - font-weight: 600; -} - -.logout-card { - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-color: #FEE4E2; - margin-top: 8px; -} - -.logout-text { - font-size: 15px; - font-weight: 600; - color: #F04438; + height: calc(32px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px)); } diff --git a/mpuniapp/pages/owner/profile/useProfile.uts b/mpuniapp/pages/owner/profile/useProfile.uts index ec54be9..d2c65e9 100644 --- a/mpuniapp/pages/owner/profile/useProfile.uts +++ b/mpuniapp/pages/owner/profile/useProfile.uts @@ -335,6 +335,23 @@ export function removeAccountAvatar(state: ProfileState): void { state.account.avatar = ''; } +/** + * 计算配额使用百分比 (0-100),防溢出与除零防御 + */ +export function calculateQuotaPercent(used: number, limit: number): number { + if (limit <= 0) { + return 100; + } + if (used <= 0) { + return 0; + } + const pct = Math.round((used / limit) * 100); + if (pct > 100) { + return 100; + } + return pct; +} + /** * 瑞士国际主义多语言文案解析器,无 emoji */ @@ -370,6 +387,18 @@ export function resolveProfileText(key: string): string { return isZh ? '部门' : 'Department'; case 'avatarLabel': return isZh ? '头像' : 'Avatar'; + case 'tierPro': + return isZh ? '商业专业版' : 'Pro Plan'; + case 'tierBadge': + return 'PRO'; + case 'previewPortal': + return isZh ? '预览官网' : 'Preview Site'; + case 'metricsCards': + return isZh ? '活跃名片' : 'Active Cards'; + case 'metricsVisitors': + return isZh ? '雷达访客' : 'Radar Visitors'; + case 'metricsLeads': + return isZh ? '沉淀线索' : 'Captured Leads'; case 'notificationsSection': return isZh ? '消息通知' : 'Notifications'; case 'radarPushTitle': @@ -387,7 +416,9 @@ export function resolveProfileText(key: string): string { case 'logout': return isZh ? '退出登录' : 'Log out'; case 'workspaceSection': - return isZh ? '工作台' : 'Workspace'; + return isZh ? '出海工作台' : 'Workspace'; + case 'nfcSection': + return isZh ? 'NFC 智能硬件' : 'NFC Fleet'; case 'nfcScan': return isZh ? 'NFC 贴卡激活' : 'Activate NFC card'; case 'nfcMode': @@ -397,11 +428,19 @@ export function resolveProfileText(key: string): string { case 'teamBriefing': return isZh ? '展会战报' : 'Team briefing'; case 'quotaSection': - return isZh ? '套餐配额' : 'Plan quota'; + return isZh ? '套餐与配额' : 'Plan & Quota'; case 'quotaCards': return isZh ? '名片用量' : 'Card usage'; case 'quotaUpgrade': return isZh ? '已达上限,请升级套餐' : 'Limit reached. Upgrade to continue.'; + case 'upgradeQuota': + return isZh ? '升级扩容' : 'Upgrade Plan'; + case 'planActive': + return isZh ? '生效中' : 'Active'; + case 'confirmLogoutTitle': + return isZh ? '确认退出' : 'Sign Out'; + case 'confirmLogoutContent': + return isZh ? '确定要退出当前账号吗?' : 'Are you sure you want to sign out?'; default: return key; }