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('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;
}