refactor(mpuniapp): 删掉繁冗堆叠,按现代极简分组风格彻底重构名片主个人中心

这个提交包含在:
wikiboss
2026-09-21 13:41:13 +08:00
父节点 78023e8665
当前提交 d250a151e2

查看文件

@@ -1,160 +1,80 @@
<template> <template>
<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-content">
<!-- 1. Header Hero Bento: Profile identity & Quick site preview --> <!-- 1. Minimal Profile Identity Header -->
<view class="hero-card"> <view class="profile-header">
<view class="hero-main"> <view class="avatar-wrap">
<view class="avatar-box"> <image
<image v-if="state.account.avatar !== ''"
v-if="state.account.avatar !== ''" class="avatar-img"
class="account-avatar" :src="state.account.avatar"
:src="state.account.avatar" mode="aspectFill"
mode="aspectFill" @click="onAvatarClick"
@click="onAvatarClick" />
/> <view
<view v-else
v-else class="avatar-empty"
class="account-avatar-placeholder" @click="onAvatarChoose"
@click="onAvatarChoose" >
> <text class="avatar-add-icon">+</text>
<text class="avatar-placeholder-plus">+</text>
</view>
<view
v-if="state.account.avatar !== ''"
class="avatar-del-badge"
@click="onAvatarDelete"
>
<text class="avatar-del-icon">×</text>
</view>
</view> </view>
<view
<view class="hero-info"> v-if="state.account.avatar !== ''"
<view class="name-row"> class="avatar-remove-badge"
<text class="account-name">{{ state.account.name }}</text> @click="onAvatarDelete"
<view class="tier-pill"> >
<text class="tier-pill-text">{{ t('tierBadge') }}</text> <text class="avatar-remove-icon">×</text>
</view>
</view>
<text class="account-company">{{ state.account.company }}</text>
<text class="account-dept">{{ state.account.department }}</text>
</view> </view>
</view> </view>
<view class="hero-actions"> <view class="identity-info">
<view class="preview-btn" @click="openPage('/pages/portal/index')"> <text class="identity-name">{{ state.account.name }}</text>
<text class="preview-btn-text">{{ t('previewPortal') }}</text> <text class="identity-company">{{ state.account.company }}</text>
</view> <text class="identity-dept">{{ state.account.department }}</text>
</view> </view>
</view> </view>
<!-- 2. Bento Metrics Grid: Core Business Assets --> <!-- 2. Operational Hardware & Team Group -->
<view class="metrics-grid"> <view class="menu-group">
<view class="metric-cell" @click="openPage('/pages/owner/card/index')"> <view class="menu-row" @click="openPage('/pages/owner/nfc/scan')">
<text class="metric-val">{{ cardCount }}</text> <text class="menu-title">{{ t('nfcScan') }}</text>
<text class="metric-lbl">{{ t('metricsCards') }}</text> <text class="menu-arrow">›</text>
</view> </view>
<view class="metric-cell" @click="openPage('/pages/owner/radar')"> <view class="menu-divider"></view>
<text class="metric-val">{{ visitorCount }}</text> <view class="menu-row" @click="openPage('/pages/owner/nfc/mode')">
<text class="metric-lbl">{{ t('metricsVisitors') }}</text> <text class="menu-title">{{ t('nfcMode') }}</text>
<text class="menu-arrow">›</text>
</view> </view>
<view class="metric-cell" @click="openPage('/pages/owner/leads')"> <view class="menu-divider"></view>
<text class="metric-val">{{ leadCount }}</text> <view class="menu-row" @click="openPage('/pages/owner/nfc/security')">
<text class="metric-lbl">{{ t('metricsLeads') }}</text> <text class="menu-title">{{ t('nfcSecurity') }}</text>
<text class="menu-arrow">›</text>
</view>
<view class="menu-divider"></view>
<view class="menu-row" @click="openPage('/pages/owner/team/overview')">
<text class="menu-title">{{ t('teamBriefing') }}</text>
<text class="menu-arrow">›</text>
</view> </view>
</view> </view>
<!-- 3. Plan & Quota Bento Card --> <!-- 3. Plan & Quota Group -->
<view class="section-card quota-card"> <view class="menu-group">
<view class="card-header-row"> <view class="menu-row">
<text class="section-title">{{ t('quotaSection') }}</text> <text class="menu-title">{{ t('quotaCards') }}</text>
<view class="plan-status-badge"> <text class="menu-value">{{ quotaLine }}</text>
<view class="status-dot"></view>
<text class="plan-status-text">{{ t('planActive') }}</text>
</view>
</view> </view>
<view v-if="quotaExceeded" class="quota-tip-row">
<view class="quota-body"> <text class="quota-tip-text">{{ t('quotaUpgrade') }}</text>
<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>
</view> </view>
<!-- 4. Operational Workspace: Hardware & Team Briefing --> <!-- 4. Notifications & Switches Group -->
<view class="section-card"> <view class="menu-group">
<text class="section-title">{{ t('workspaceSection') }}</text> <view class="menu-row menu-row-switch">
<view class="switch-label-wrap">
<view class="setting-cell" @click="openPage('/pages/owner/nfc/scan')"> <text class="menu-title">{{ t('radarPushTitle') }}</text>
<view class="cell-left"> <text class="menu-subtitle">{{ t('radarPushDesc') }}</text>
<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">
<text class="section-title">{{ t('notificationsSection') }}</text>
<!-- Radar push notification -->
<view class="setting-cell setting-cell-switch">
<view class="cell-text-wrap">
<text class="cell-label">{{ t('radarPushTitle') }}</text>
<text class="cell-desc">{{ t('radarPushDesc') }}</text>
</view> </view>
<view class="switch-touch-area"> <view class="switch-touch-area">
<switch <switch
@@ -164,14 +84,11 @@
/> />
</view> </view>
</view> </view>
<view class="menu-divider"></view>
<view class="row-divider"></view> <view class="menu-row menu-row-switch">
<view class="switch-label-wrap">
<!-- Lead notice notification --> <text class="menu-title">{{ t('leadNoticeTitle') }}</text>
<view class="setting-cell setting-cell-switch"> <text class="menu-subtitle">{{ t('leadNoticeDesc') }}</text>
<view class="cell-text-wrap">
<text class="cell-label">{{ t('leadNoticeTitle') }}</text>
<text class="cell-desc">{{ t('leadNoticeDesc') }}</text>
</view> </view>
<view class="switch-touch-area"> <view class="switch-touch-area">
<switch <switch
@@ -183,22 +100,22 @@
</view> </view>
</view> </view>
<!-- 6. Preferred Display Language --> <!-- 5. Display Language Preferences Group -->
<view class="section-card"> <view class="menu-group">
<text class="section-title">{{ t('languageSection') }}</text> <view class="language-header">
<text class="section-desc">{{ t('languageLabel') }}</text> <text class="menu-title">{{ t('languageSection') }}</text>
</view>
<view class="language-options"> <view class="language-chip-list">
<view <view
v-for="item in languageList" v-for="item in languageList"
:key="item.code" :key="item.code"
class="language-chip" class="lang-pill"
:class="{ 'language-chip-active': item.code === state.locale }" :class="{ 'lang-pill-active': item.code === state.locale }"
@click="onSelectLocale(item.code)" @click="onSelectLocale(item.code)"
> >
<text <text
class="language-chip-text" class="lang-pill-text"
:class="{ 'language-chip-text-active': item.code === state.locale }" :class="{ 'lang-pill-text-active': item.code === state.locale }"
> >
{{ item.label }} {{ item.label }}
</text> </text>
@@ -206,19 +123,19 @@
</view> </view>
</view> </view>
<!-- 7. Logout Action (Minimal, safe text button) --> <!-- 6. Minimal Sign Out -->
<view class="logout-wrap"> <view class="signout-section">
<view class="logout-btn" @click="onLogout"> <view class="signout-btn" @click="onLogout">
<text class="logout-text">{{ t('logout') }}</text> <text class="signout-text">{{ t('logout') }}</text>
</view> </view>
</view> </view>
<!-- Error feedback banner --> <!-- System error notification if any -->
<view v-if="state.errorMessage !== ''" class="error-banner"> <view v-if="state.errorMessage !== ''" class="error-notice">
<text class="error-banner-text">{{ state.errorMessage }}</text> <text class="error-notice-text">{{ state.errorMessage }}</text>
</view> </view>
<view class="bottom-spacer"></view> <view class="bottom-safe-space"></view>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -235,7 +152,6 @@ 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';
@@ -249,10 +165,6 @@ 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 !== '') {
@@ -313,25 +225,15 @@ 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 = '1 / 5'; quotaLine.value = '1 / 5';
quotaExceeded.value = false; quotaExceeded.value = false;
quotaPercent.value = 20;
} }
} }
@@ -381,373 +283,159 @@ onShow(() => {
flex: 1; flex: 1;
} }
.profile-container { .profile-content {
padding: 16px 16px 32px 16px; padding: 24px 16px 32px 16px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
/* 1. Hero Identity Bento */ /* 1. Minimal Identity Header */
.hero-card { .profile-header {
background-color: #FFFFFF;
border: 1px solid #E3E7ED;
border-radius: 16px;
padding: 16px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
}
.hero-main {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 20px;
padding: 4px 4px;
} }
.avatar-box { .avatar-wrap {
position: relative; position: relative;
width: 56px; width: 54px;
height: 56px; height: 54px;
margin-right: 14px; margin-right: 16px;
} }
.account-avatar { .avatar-img {
width: 56px; width: 54px;
height: 56px; height: 54px;
border-radius: 28px; border-radius: 27px;
background-color: #E3E7ED; background-color: #E3E7ED;
} }
.account-avatar-placeholder { .avatar-empty {
width: 56px; width: 54px;
height: 56px; height: 54px;
border-radius: 28px; border-radius: 27px;
border: 1px dashed #C9CDD4; border: 1px dashed #C9CDD4;
background-color: #F7F8FA; background-color: #FFFFFF;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.avatar-placeholder-plus { .avatar-add-icon {
font-size: 24px; font-size: 22px;
color: #667085; color: #667085;
line-height: 1; line-height: 1;
} }
.avatar-del-badge { .avatar-remove-badge {
position: absolute; position: absolute;
top: 0px; top: -2px;
right: 0px; right: -2px;
width: 18px; width: 16px;
height: 18px; height: 16px;
border-radius: 9px; border-radius: 8px;
background-color: rgba(240, 68, 56, 0.9); background-color: rgba(240, 68, 56, 0.9);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.avatar-del-icon { .avatar-remove-icon {
color: #FFFFFF; color: #FFFFFF;
font-size: 13px; font-size: 11px;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
} }
.hero-info { .identity-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
} }
.name-row { .identity-name {
display: flex; font-size: 18px;
flex-direction: row;
align-items: center;
}
.account-name {
font-size: 17px;
font-weight: 600; font-weight: 600;
color: #101828; color: #101828;
line-height: 1.4; line-height: 1.3;
margin-right: 8px;
} }
.tier-pill { .identity-company {
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; font-size: 13px;
color: #667085; color: #667085;
margin-top: 3px; margin-top: 3px;
} }
.account-dept { .identity-dept {
font-size: 12px; font-size: 12px;
color: #667085; color: #667085;
margin-top: 1px; margin-top: 1px;
} }
.hero-actions { /* 2. Modern Inset Grouped Card */
margin-top: 12px; .menu-group {
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; background-color: #FFFFFF;
border: 1px solid #E3E7ED; border: 1px solid #E3E7ED;
border-radius: 16px; border-radius: 16px;
padding: 16px; padding: 0 16px;
margin-bottom: 12px; margin-bottom: 14px;
} }
.card-header-row { .menu-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: 6px; padding: 10px 0;
padding-bottom: 6px;
} }
.setting-cell:active { .menu-row:active {
opacity: 0.75; opacity: 0.7;
} }
.cell-left { .menu-row-switch {
display: flex; min-height: 52px;
flex-direction: column;
flex: 1;
} }
.cell-text-wrap { .menu-title {
font-size: 14px;
font-weight: 500;
color: #101828;
}
.menu-value {
font-size: 13px;
color: #667085;
}
.menu-arrow {
font-size: 16px;
color: #667085;
line-height: 1;
}
.menu-divider {
height: 1px;
background-color: #E3E7ED;
}
.switch-label-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
padding-right: 12px; padding-right: 12px;
} }
.cell-label { .menu-subtitle {
font-size: 14px;
font-weight: 500;
color: #101828;
}
.cell-desc {
font-size: 12px; font-size: 12px;
color: #667085; color: #667085;
line-height: 1.4; line-height: 1.4;
margin-top: 2px; margin-top: 2px;
} }
.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 { .switch-touch-area {
min-width: 44px; min-width: 44px;
min-height: 44px; min-height: 44px;
@@ -756,26 +444,33 @@ onShow(() => {
justify-content: center; justify-content: center;
} }
.row-divider { .quota-tip-row {
height: 1px; padding-bottom: 12px;
background-color: #E3E7ED;
margin-top: 4px;
margin-bottom: 4px;
} }
/* Language Chips */ .quota-tip-text {
.language-options { font-size: 12px;
color: #F04438;
}
/* Language selector */
.language-header {
padding-top: 14px;
margin-bottom: 10px;
}
.language-chip-list {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 4px; padding-bottom: 14px;
} }
.language-chip { .lang-pill {
min-height: 44px; min-height: 36px;
padding: 0 16px; padding: 0 14px;
border-radius: 12px; border-radius: 10px;
border: 1px solid #E3E7ED; border: 1px solid #E3E7ED;
background-color: #F7F8FA; background-color: #F7F8FA;
display: flex; display: flex;
@@ -783,58 +478,62 @@ onShow(() => {
justify-content: center; justify-content: center;
} }
.language-chip-active { .lang-pill-active {
border-color: #0B5CFF; border-color: #0B5CFF;
background-color: #0B5CFF; background-color: #0B5CFF;
} }
.language-chip-text { .lang-pill-text {
font-size: 13px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: #101828; color: #101828;
} }
.language-chip-text-active { .lang-pill-text-active {
color: #FFFFFF; color: #FFFFFF;
} }
/* Logout wrap */ /* Sign out button */
.logout-wrap { .signout-section {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-top: 8px; margin-top: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
.logout-btn { .signout-btn {
min-height: 44px; min-height: 44px;
padding: 0 24px; padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.logout-text { .signout-text {
font-size: 14px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: #667085; color: #667085;
} }
.error-banner { .signout-btn:active {
padding: 12px 16px; opacity: 0.6;
background-color: #F7F8FA;
border: 1px solid #E3E7ED;
border-radius: 12px;
margin-bottom: 16px;
} }
.error-banner-text { .error-notice {
font-size: 13px; padding: 10px 14px;
background-color: #F7F8FA;
border: 1px solid #E3E7ED;
border-radius: 10px;
margin-bottom: 14px;
}
.error-notice-text {
font-size: 12px;
color: #667085; color: #667085;
} }
.bottom-spacer { .bottom-safe-space {
height: calc(32px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px)); height: calc(24px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
} }
</style> </style>