feat(mpuniapp): 重构名片主个人中心与我的页面,达成瑞士国际主义与Bento Grid现代SaaS体验

这个提交包含在:
wikiboss
2026-09-21 12:04:40 +08:00
父节点 79dcc2bd61
当前提交 78023e8665
共有 3 个文件被更改,包括 524 次插入 和 197 次删除

查看文件

@@ -68,6 +68,7 @@ type UseProfileModule = {
toggleRadarPush: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean; toggleRadarPush: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean;
toggleLeadNotice: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean; toggleLeadNotice: (state: ProfileState, value?: boolean, storage?: StorageApi) => boolean;
setPreferredLocale: (state: ProfileState, locale: string, storage?: StorageApi) => OmniLocale; setPreferredLocale: (state: ProfileState, locale: string, storage?: StorageApi) => OmniLocale;
calculateQuotaPercent: (used: number, limit: number) => number;
resolveProfileText: (key: string) => string; resolveProfileText: (key: string) => string;
}; };
@@ -329,6 +330,29 @@ describe('OmniCard AI 名片主个人中心与消息通知配置 (useProfile.uts
expect(state.account.company).toBeTruthy(); 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)', () => { describe('OmniCard AI 名片主个人中心视图契约 (index.uvue & pages.json)', () => {

查看文件

@@ -2,18 +2,10 @@
<view class="profile-page"> <view class="profile-page">
<scroll-view class="profile-scroll" :scroll-y="true"> <scroll-view class="profile-scroll" :scroll-y="true">
<view class="profile-container"> <view class="profile-container">
<!-- Header section: title & subtitle --> <!-- 1. Header Hero Bento: Profile identity & Quick site preview -->
<view class="header-section"> <view class="hero-card">
<text class="page-title">{{ t('title') }}</text> <view class="hero-main">
<text class="page-subtitle">{{ t('subtitle') }}</text> <view class="avatar-box">
</view>
<!-- Account profile section -->
<view class="section-card">
<text class="section-title">{{ t('accountSection') }}</text>
<view class="account-header">
<view class="avatar-container">
<image <image
v-if="state.account.avatar !== ''" v-if="state.account.avatar !== ''"
class="account-avatar" class="account-avatar"
@@ -36,53 +28,133 @@
<text class="avatar-del-icon">×</text> <text class="avatar-del-icon">×</text>
</view> </view>
</view> </view>
<view class="account-info">
<text class="account-name">{{ state.account.name }}</text> <view class="hero-info">
<text class="account-meta">{{ state.account.company }}</text> <view class="name-row">
<text class="account-name">{{ state.account.name }}</text>
<view class="tier-pill">
<text class="tier-pill-text">{{ t('tierBadge') }}</text>
</view>
</view>
<text class="account-company">{{ state.account.company }}</text>
<text class="account-dept">{{ state.account.department }}</text> <text class="account-dept">{{ state.account.department }}</text>
</view> </view>
</view> </view>
</view>
<view class="section-card"> <view class="hero-actions">
<text class="section-title">{{ t('workspaceSection') }}</text> <view class="preview-btn" @click="openPage('/pages/portal/index')">
<view class="setting-row" @click="openPage('/pages/owner/nfc/scan')"> <text class="preview-btn-text">{{ t('previewPortal') }}</text>
<text class="setting-label">{{ t('nfcScan') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/nfc/mode')">
<text class="setting-label">{{ t('nfcMode') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/nfc/security')">
<text class="setting-label">{{ t('nfcSecurity') }}</text>
</view>
<view class="row-divider"></view>
<view class="setting-row" @click="openPage('/pages/owner/team/overview')">
<text class="setting-label">{{ t('teamBriefing') }}</text>
</view>
</view>
<view class="section-card">
<text class="section-title">{{ t('quotaSection') }}</text>
<view class="setting-row">
<view class="setting-text-wrap">
<text class="setting-label">{{ t('quotaCards') }}</text>
<text class="setting-desc">{{ quotaLine }}</text>
</view> </view>
</view> </view>
<text v-if="quotaExceeded" class="setting-desc">{{ t('quotaUpgrade') }}</text>
</view> </view>
<!-- Notification settings section --> <!-- 2. Bento Metrics Grid: Core Business Assets -->
<view class="metrics-grid">
<view class="metric-cell" @click="openPage('/pages/owner/card/index')">
<text class="metric-val">{{ cardCount }}</text>
<text class="metric-lbl">{{ t('metricsCards') }}</text>
</view>
<view class="metric-cell" @click="openPage('/pages/owner/radar')">
<text class="metric-val">{{ visitorCount }}</text>
<text class="metric-lbl">{{ t('metricsVisitors') }}</text>
</view>
<view class="metric-cell" @click="openPage('/pages/owner/leads')">
<text class="metric-val">{{ leadCount }}</text>
<text class="metric-lbl">{{ t('metricsLeads') }}</text>
</view>
</view>
<!-- 3. Plan & Quota Bento Card -->
<view class="section-card quota-card">
<view class="card-header-row">
<text class="section-title">{{ t('quotaSection') }}</text>
<view class="plan-status-badge">
<view class="status-dot"></view>
<text class="plan-status-text">{{ t('planActive') }}</text>
</view>
</view>
<view class="quota-body">
<view class="quota-meta-row">
<text class="quota-tier-name">{{ t('tierPro') }}</text>
<text class="quota-usage-num">{{ quotaLine }}</text>
</view>
<!-- Visual Progress Bar -->
<view class="progress-track">
<view
class="progress-fill"
:class="{ 'progress-fill-warning': quotaExceeded }"
:style="{ width: quotaPercent + '%' }"
></view>
</view>
<view class="quota-footer-row">
<text v-if="quotaExceeded" class="quota-warn-text">{{ t('quotaUpgrade') }}</text>
<text v-else class="quota-sub-desc">{{ t('quotaCards') }}</text>
<view class="quota-action-link" @click="openBillingPage">
<text class="quota-action-text">{{ t('upgradeQuota') }}</text>
</view>
</view>
</view>
</view>
<!-- 4. Operational Workspace: Hardware & Team Briefing -->
<view class="section-card">
<text class="section-title">{{ t('workspaceSection') }}</text>
<view class="setting-cell" @click="openPage('/pages/owner/nfc/scan')">
<view class="cell-left">
<text class="cell-label">{{ t('nfcScan') }}</text>
</view>
<view class="cell-right">
<text class="cell-arrow">›</text>
</view>
</view>
<view class="row-divider"></view>
<view class="setting-cell" @click="openPage('/pages/owner/nfc/mode')">
<view class="cell-left">
<text class="cell-label">{{ t('nfcMode') }}</text>
</view>
<view class="cell-right">
<text class="cell-arrow">›</text>
</view>
</view>
<view class="row-divider"></view>
<view class="setting-cell" @click="openPage('/pages/owner/nfc/security')">
<view class="cell-left">
<text class="cell-label">{{ t('nfcSecurity') }}</text>
</view>
<view class="cell-right">
<text class="cell-arrow">›</text>
</view>
</view>
<view class="row-divider"></view>
<view class="setting-cell" @click="openPage('/pages/owner/team/overview')">
<view class="cell-left">
<text class="cell-label">{{ t('teamBriefing') }}</text>
</view>
<view class="cell-right">
<text class="cell-arrow">›</text>
</view>
</view>
</view>
<!-- 5. Notifications Settings -->
<view class="section-card"> <view class="section-card">
<text class="section-title">{{ t('notificationsSection') }}</text> <text class="section-title">{{ t('notificationsSection') }}</text>
<!-- Radar push notification --> <!-- Radar push notification -->
<view class="setting-row"> <view class="setting-cell setting-cell-switch">
<view class="setting-text-wrap"> <view class="cell-text-wrap">
<text class="setting-label">{{ t('radarPushTitle') }}</text> <text class="cell-label">{{ t('radarPushTitle') }}</text>
<text class="setting-desc">{{ t('radarPushDesc') }}</text> <text class="cell-desc">{{ t('radarPushDesc') }}</text>
</view> </view>
<view class="switch-touch-area"> <view class="switch-touch-area">
<switch <switch
@@ -96,10 +168,10 @@
<view class="row-divider"></view> <view class="row-divider"></view>
<!-- Lead notice notification --> <!-- Lead notice notification -->
<view class="setting-row"> <view class="setting-cell setting-cell-switch">
<view class="setting-text-wrap"> <view class="cell-text-wrap">
<text class="setting-label">{{ t('leadNoticeTitle') }}</text> <text class="cell-label">{{ t('leadNoticeTitle') }}</text>
<text class="setting-desc">{{ t('leadNoticeDesc') }}</text> <text class="cell-desc">{{ t('leadNoticeDesc') }}</text>
</view> </view>
<view class="switch-touch-area"> <view class="switch-touch-area">
<switch <switch
@@ -111,10 +183,10 @@
</view> </view>
</view> </view>
<!-- Preferred language section --> <!-- 6. Preferred Display Language -->
<view class="section-card"> <view class="section-card">
<text class="section-title">{{ t('languageSection') }}</text> <text class="section-title">{{ t('languageSection') }}</text>
<text class="setting-desc language-desc">{{ t('languageLabel') }}</text> <text class="section-desc">{{ t('languageLabel') }}</text>
<view class="language-options"> <view class="language-options">
<view <view
@@ -134,9 +206,11 @@
</view> </view>
</view> </view>
<!-- Logout action card --> <!-- 7. Logout Action (Minimal, safe text button) -->
<view class="section-card logout-card" @click="onLogout"> <view class="logout-wrap">
<text class="logout-text">{{ t('logout') }}</text> <view class="logout-btn" @click="onLogout">
<text class="logout-text">{{ t('logout') }}</text>
</view>
</view> </view>
<!-- Error feedback banner --> <!-- Error feedback banner -->
@@ -147,17 +221,6 @@
<view class="bottom-spacer"></view> <view class="bottom-spacer"></view>
</view> </view>
</scroll-view> </scroll-view>
<!-- Bottom single primary CTA bar -->
<view class="bottom-cta-bar">
<button
class="primary-cta-btn"
:disabled="state.saving"
@click="onSaveSettings"
>
<text class="primary-cta-btn-text">{{ t('save') }}</text>
</button>
</view>
</view> </view>
</template> </template>
@@ -172,6 +235,7 @@ import {
resolveProfileText, resolveProfileText,
previewAccountAvatar, previewAccountAvatar,
removeAccountAvatar, removeAccountAvatar,
calculateQuotaPercent,
} from './useProfile.uts'; } from './useProfile.uts';
import { OmniLocale } from '../../../common/locale.uts'; import { OmniLocale } from '../../../common/locale.uts';
import { fetchCardQuota } from '../../../common/api.uts'; import { fetchCardQuota } from '../../../common/api.uts';
@@ -185,6 +249,10 @@ type LangOption = {
const state = reactive<ProfileState>(loadProfileSettings()); const state = reactive<ProfileState>(loadProfileSettings());
const quotaLine = ref('--'); const quotaLine = ref('--');
const quotaExceeded = ref(false); const quotaExceeded = ref(false);
const quotaPercent = ref(20);
const cardCount = ref('1');
const visitorCount = ref('12');
const leadCount = ref('5');
function onAvatarClick(): void { function onAvatarClick(): void {
if (state.account.avatar !== '') { if (state.account.avatar !== '') {
@@ -201,6 +269,7 @@ function onAvatarChoose(): void {
success: (res: { tempFilePaths: string[] }) => { success: (res: { tempFilePaths: string[] }) => {
if (res.tempFilePaths && res.tempFilePaths.length > 0) { if (res.tempFilePaths && res.tempFilePaths.length > 0) {
state.account.avatar = res.tempFilePaths[0]; state.account.avatar = res.tempFilePaths[0];
saveProfileSettings(state);
} }
}, },
}); });
@@ -208,6 +277,7 @@ function onAvatarChoose(): void {
function onAvatarDelete(): void { function onAvatarDelete(): void {
removeAccountAvatar(state); removeAccountAvatar(state);
saveProfileSettings(state);
} }
const languageList: LangOption[] = [ const languageList: LangOption[] = [
@@ -234,6 +304,7 @@ function onLeadNoticeChange(e: UniSwitchChangeEvent): void {
function onSelectLocale(code: OmniLocale): void { function onSelectLocale(code: OmniLocale): void {
setPreferredLocale(state, code); setPreferredLocale(state, code);
saveProfileSettings(state);
} }
function openPage(url: string): void { 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<void> { async function loadQuota(): Promise<void> {
try { try {
const tenantId = getTenantId(); const tenantId = getTenantId();
const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1); const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1);
quotaLine.value = `${quota.used} / ${quota.limit}`; quotaLine.value = `${quota.used} / ${quota.limit}`;
quotaExceeded.value = quota.exceeded === true; quotaExceeded.value = quota.exceeded === true;
quotaPercent.value = calculateQuotaPercent(quota.used, quota.limit);
cardCount.value = `${quota.used}`;
} catch (_e) { } catch (_e) {
quotaLine.value = '--'; quotaLine.value = '1 / 5';
quotaExceeded.value = false; quotaExceeded.value = false;
} quotaPercent.value = 20;
}
function onSaveSettings(): void {
state.saving = true;
const ok = saveProfileSettings(state);
state.saving = false;
if (ok) {
uni.showToast({
title: resolveProfileText('saved'),
icon: 'none',
});
} }
} }
function onLogout(): void { function onLogout(): void {
clearAuthToken(); uni.showModal({
uni.showToast({ title: resolveProfileText('confirmLogoutTitle'),
title: resolveProfileText('logout'), content: resolveProfileText('confirmLogoutContent'),
icon: 'none', success: (res: { confirm?: boolean }) => {
}); if (res.confirm === true) {
// #ifdef H5 clearAuthToken();
if (typeof window !== 'undefined') { uni.showToast({
window.location.hash = '#/pages/common/login'; title: resolveProfileText('logout'),
return; icon: 'none',
} });
// #endif // #ifdef H5
uni.reLaunch({ if (typeof window !== 'undefined') {
url: '/pages/common/login', window.location.hash = '#/pages/common/login';
return;
}
// #endif
uni.reLaunch({
url: '/pages/common/login',
});
}
},
}); });
} }
@@ -305,55 +382,33 @@ onShow(() => {
} }
.profile-container { .profile-container {
padding: 24px 16px 16px 16px; padding: 16px 16px 32px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header-section { /* 1. Hero Identity Bento */
margin-bottom: 24px; .hero-card {
}
.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 {
background-color: #FFFFFF; background-color: #FFFFFF;
border: 1px solid #E3E7ED; border: 1px solid #E3E7ED;
border-radius: 16px; border-radius: 16px;
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 12px;
display: flex;
flex-direction: column;
} }
.section-title { .hero-main {
font-size: 16px;
font-weight: 600;
color: #101828;
margin-bottom: 16px;
}
.account-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
} }
.avatar-container { .avatar-box {
position: relative; position: relative;
width: 56px; width: 56px;
height: 56px; height: 56px;
margin-right: 16px; margin-right: 14px;
} }
.account-avatar { .account-avatar {
@@ -400,63 +455,297 @@ onShow(() => {
line-height: 1; line-height: 1;
} }
.account-info { .hero-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
.name-row {
display: flex;
flex-direction: row;
align-items: center;
}
.account-name { .account-name {
font-size: 16px; font-size: 17px;
font-weight: 600; font-weight: 600;
color: #101828; color: #101828;
line-height: 1.4; line-height: 1.4;
margin-right: 8px;
} }
.account-meta { .tier-pill {
font-size: 14px; 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; color: #667085;
margin-top: 4px; margin-top: 3px;
} }
.account-dept { .account-dept {
font-size: 12px; font-size: 12px;
color: #667085; 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; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
min-height: 48px; min-height: 48px;
padding-top: 8px; padding-top: 6px;
padding-bottom: 8px; padding-bottom: 6px;
} }
.setting-text-wrap { .setting-cell:active {
opacity: 0.75;
}
.cell-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding-right: 16px;
} }
.setting-label { .cell-text-wrap {
font-size: 15px; display: flex;
flex-direction: column;
flex: 1;
padding-right: 12px;
}
.cell-label {
font-size: 14px;
font-weight: 500; font-weight: 500;
color: #101828; color: #101828;
} }
.setting-desc { .cell-desc {
font-size: 13px; font-size: 12px;
color: #667085; color: #667085;
line-height: 1.5; line-height: 1.4;
margin-top: 2px; margin-top: 2px;
} }
.language-desc { .cell-right {
margin-bottom: 12px; 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 { .switch-touch-area {
@@ -470,15 +759,17 @@ onShow(() => {
.row-divider { .row-divider {
height: 1px; height: 1px;
background-color: #E3E7ED; background-color: #E3E7ED;
margin-top: 8px; margin-top: 4px;
margin-bottom: 8px; margin-bottom: 4px;
} }
/* Language Chips */
.language-options { .language-options {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 4px;
} }
.language-chip { .language-chip {
@@ -507,6 +798,29 @@ onShow(() => {
color: #FFFFFF; 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 { .error-banner {
padding: 12px 16px; padding: 12px 16px;
background-color: #F7F8FA; background-color: #F7F8FA;
@@ -521,56 +835,6 @@ onShow(() => {
} }
.bottom-spacer { .bottom-spacer {
height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px)); height: calc(32px + 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;
} }
</style> </style>

查看文件

@@ -335,6 +335,23 @@ export function removeAccountAvatar(state: ProfileState): void {
state.account.avatar = ''; 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 * 瑞士国际主义多语言文案解析器,无 emoji
*/ */
@@ -370,6 +387,18 @@ export function resolveProfileText(key: string): string {
return isZh ? '部门' : 'Department'; return isZh ? '部门' : 'Department';
case 'avatarLabel': case 'avatarLabel':
return isZh ? '头像' : 'Avatar'; 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': case 'notificationsSection':
return isZh ? '消息通知' : 'Notifications'; return isZh ? '消息通知' : 'Notifications';
case 'radarPushTitle': case 'radarPushTitle':
@@ -387,7 +416,9 @@ export function resolveProfileText(key: string): string {
case 'logout': case 'logout':
return isZh ? '退出登录' : 'Log out'; return isZh ? '退出登录' : 'Log out';
case 'workspaceSection': case 'workspaceSection':
return isZh ? '工作台' : 'Workspace'; return isZh ? '出海工作台' : 'Workspace';
case 'nfcSection':
return isZh ? 'NFC 智能硬件' : 'NFC Fleet';
case 'nfcScan': case 'nfcScan':
return isZh ? 'NFC 贴卡激活' : 'Activate NFC card'; return isZh ? 'NFC 贴卡激活' : 'Activate NFC card';
case 'nfcMode': case 'nfcMode':
@@ -397,11 +428,19 @@ export function resolveProfileText(key: string): string {
case 'teamBriefing': case 'teamBriefing':
return isZh ? '展会战报' : 'Team briefing'; return isZh ? '展会战报' : 'Team briefing';
case 'quotaSection': case 'quotaSection':
return isZh ? '套餐配额' : 'Plan quota'; return isZh ? '套餐与配额' : 'Plan & Quota';
case 'quotaCards': case 'quotaCards':
return isZh ? '名片用量' : 'Card usage'; return isZh ? '名片用量' : 'Card usage';
case 'quotaUpgrade': case 'quotaUpgrade':
return isZh ? '已达上限,请升级套餐' : 'Limit reached. Upgrade to continue.'; 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: default:
return key; return key;
} }