文件
OmniCard/mpuniapp/pages/owner/workplace/index.uvue

822 行
19 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="workplace-page" :class="pageRtlClass" :style="pageTheme">
<!-- 顶部受控局部极光微光晕 (面积 < 8%) -->
<view class="workplace-aurora-glow"></view>
<!-- 顶栏:当前名片身份与雷达在线探测状态 -->
<view class="workplace-topbar">
<view class="workplace-identity" @click="handleSwitchCard">
<text class="identity-name">{{ activeCardName }}</text>
<text class="identity-badge">{{ activeCardBadge }}</text>
<text class="identity-arrow">v</text>
</view>
<view class="radar-status-pill" @click="handleOpenRadar">
<view class="radar-pulse-dot"></view>
<text class="radar-status-text">{{ t('workplace.radar_active') }}</text>
</view>
</view>
<scroll-view class="workplace-scroll" :scroll-y="true">
<!-- 问候语横幅 -->
<view class="greeting-banner">
<text class="greeting-title">{{ greetingTitle }}</text>
<text class="greeting-subtitle">{{ t('workplace.greeting') }}</text>
</view>
<!-- 1. 闪电触达微展台 (Quick Showcase) -->
<view class="quick-showcase-section">
<view class="showcase-card">
<view class="showcase-qr-wrap" @click="handleOpenQrcode">
<rich-text
v-if="state.activeQrSvg !== ''"
class="showcase-qr-code"
:nodes="state.activeQrSvg"
></rich-text>
<view v-else class="showcase-qr-placeholder">
<view class="qr-pulse-block"></view>
</view>
</view>
<view class="showcase-meta">
<view class="meta-header">
<text class="meta-title">{{ activeCardTitle }}</text>
<text class="meta-preview-btn" @click="handlePreviewPortal">
{{ t('workplace.view_portal') }} &gt;
</text>
</view>
<text class="meta-company">{{ activeCardCompany }}</text>
<view class="meta-nfc-tag">
<view class="nfc-dot"></view>
<text class="nfc-tag-text">{{ t('workplace.nfc_active') }}</text>
</view>
<view class="meta-actions">
<button class="copy-link-btn" @click="handleCopyLink">
<text class="copy-link-text">{{ t('workplace.copy_link') }}</text>
</button>
</view>
</view>
</view>
</view>
<!-- 2. 今日战情报表 (2x2 Bento KPIs) -->
<view class="kpi-grid-section">
<view class="section-header">
<text class="section-title">{{ t('workplace.kpi_title') }}</text>
</view>
<view class="kpi-grid">
<view class="kpi-card" @click="handleOpenRadar">
<text class="kpi-label">{{ t('workplace.kpi_taps') }}</text>
<view class="kpi-value-row">
<text class="kpi-value">{{ state.kpis.tapCount }}</text>
<text class="kpi-delta">+{{ state.kpis.tapDelta }}</text>
</view>
</view>
<view class="kpi-card" @click="handleOpenRadar">
<text class="kpi-label">{{ t('workplace.kpi_visitors') }}</text>
<view class="kpi-value-row">
<text class="kpi-value">{{ state.kpis.viewCount }}</text>
<text class="kpi-delta">+{{ state.kpis.viewDelta }}</text>
</view>
</view>
<view class="kpi-card" @click="handleOpenLeads">
<text class="kpi-label">{{ t('workplace.kpi_leads') }}</text>
<view class="kpi-value-row">
<text class="kpi-value">{{ state.kpis.newLeadCount }}</text>
<text class="kpi-delta">+{{ state.kpis.newLeadDelta }}</text>
</view>
</view>
<view class="kpi-card" @click="handleOpenLeads">
<text class="kpi-label">{{ t('workplace.kpi_conversion') }}</text>
<view class="kpi-value-row">
<text class="kpi-value">{{ state.kpis.conversionRate }}%</text>
<text class="kpi-tag-hot">HOT</text>
</view>
</view>
</view>
</view>
<!-- 3. 实时雷达高光足迹 (Live Radar Hot Pulse) -->
<view class="radar-pulse-section">
<view class="section-header">
<text class="section-title">{{ t('workplace.radar_pulse_title') }}</text>
<text class="section-action" @click="handleOpenRadar">{{ t('workplace.leads_view_all') }} &gt;</text>
</view>
<view class="pulse-card" v-if="state.hotRadarItem != null" @click="handleOpenRadar">
<view class="pulse-header">
<view class="pulse-geo">
<view class="geo-badge">
<text class="geo-code">{{ state.hotRadarItem!.country }}</text>
</view>
<text class="geo-city">{{ state.hotRadarItem!.city }}</text>
</view>
<view class="pulse-score-pill">
<text class="pulse-score-text">Score {{ state.hotRadarItem!.intentScore || 90 }}</text>
</view>
</view>
<text class="pulse-desc">{{ t('workplace.radar_pulse_desc') }}</text>
<view class="pulse-footer">
<text class="pulse-time">{{ formatTime(state.hotRadarItem!.createTime) }}</text>
<button class="pulse-action-btn" @click.stop="handleQuickContact">
<text class="pulse-action-text">{{ t('workplace.radar_action_whatsapp') }}</text>
</button>
</view>
</view>
</view>
<!-- 4. 今日待办商机 (Actionable Leads) -->
<view class="leads-todo-section">
<view class="section-header">
<text class="section-title">{{ t('workplace.leads_todo_title') }}</text>
<text class="section-action" @click="handleOpenLeads">{{ t('workplace.leads_view_all') }} &gt;</text>
</view>
<view class="leads-flow">
<view
class="lead-item-card"
v-for="lead in state.recentLeads"
:key="lead.id"
@click="handleOpenLead(lead.id)"
>
<view class="lead-top">
<text class="lead-name">{{ lead.name }}</text>
<text class="lead-status-chip" :class="'status-' + lead.status">{{ lead.status.toUpperCase() }}</text>
</view>
<text class="lead-company">{{ lead.company || 'Global Client' }}</text>
<text class="lead-notes">{{ lead.notes || '待跟进商机' }}</text>
</view>
</view>
</view>
<!-- 5. AI 销售副驾驶建议 (AI Co-pilot) -->
<view class="ai-copilot-section">
<view class="ai-card">
<view class="ai-header">
<view class="ai-spark-dot"></view>
<text class="ai-title">{{ t('workplace.ai_tip_title') }}</text>
</view>
<text class="ai-desc">{{ t('workplace.ai_tip_desc') }}</text>
</view>
</view>
<!-- 底部安全区垫高 -->
<view class="workplace-spacer"></view>
</scroll-view>
<!-- 底部常驻单悬浮主 CTA 栏 (一屏仅1个主按钮) -->
<view class="workplace-cta-bar">
<view class="cta-inner">
<button class="primary-cta-btn" @click="handleOpenQrcode">
<text class="primary-cta-btn-text">{{ t('workplace.primary_cta') }}</text>
</button>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { getCurrentLocale, isRtl } from '../../../common/locale.uts';
import { utsLightColors } from '../../../common/tokens.uts';
import {
createWorkplaceState,
loadWorkplaceData,
openCardPreview,
openCardQrcode,
openCardsPage,
openLeadDetail,
openLeadsPage,
openRadarPage,
resolveWorkplaceText,
WorkplaceState,
} from './useWorkplace.uts';
const state = reactive(createWorkplaceState());
const currentLang = computed((): string => getCurrentLocale());
const pageRtlClass = computed((): string => (isRtl(currentLang.value) ? 'rtl-layout' : 'ltr-layout'));
const pageTheme = computed((): Record<string, string> => {
return {
'--primary': utsLightColors.primary,
'--bg': utsLightColors.bg,
'--surface': utsLightColors.surface,
'--text': utsLightColors.text,
'--text2': utsLightColors.text2,
'--border': utsLightColors.border,
'--primary-soft': utsLightColors.primarySoft,
};
});
function t(key: string): string {
return resolveWorkplaceText(key, currentLang.value);
}
const activeCardName = computed((): string => {
return state.activeCard != null ? state.activeCard!.name : 'OmniCard Sales';
});
const activeCardTitle = computed((): string => {
return state.activeCard != null ? (state.activeCard!.title || 'Senior Consultant') : 'Senior Consultant';
});
const activeCardCompany = computed((): string => {
return state.activeCard != null ? (state.activeCard!.company || 'OmniCard Global') : 'OmniCard Global';
});
const activeCardBadge = computed((): string => {
return state.activeCard != null && state.activeCard!.status === 'published' ? 'PRO' : 'CARD';
});
const greetingTitle = computed((): string => {
return currentLang.value.startsWith('zh') ? '早安,销售精英' : 'Good day, Sales Partner';
});
function formatTime(time: string | Date | undefined): string {
if (time == null) return '刚刚';
return '5分钟前';
}
function handleOpenQrcode(): void {
if (state.activeCard != null) {
openCardQrcode(state.activeCard!);
}
}
function handlePreviewPortal(): void {
if (state.activeCard != null) {
openCardPreview(state.activeCard!);
}
}
function handleSwitchCard(): void {
openCardsPage();
}
function handleOpenRadar(): void {
openRadarPage();
}
function handleOpenLeads(): void {
openLeadsPage();
}
function handleOpenLead(id: string | number): void {
openLeadDetail(id);
}
function handleQuickContact(): void {
uni.showToast({
title: t('workplace.radar_action_whatsapp'),
icon: 'none',
});
}
function handleCopyLink(): void {
const url = state.activeCard != null && state.activeCard!.url != null
? state.activeCard!.url!
: 'https://omnicard.cc';
uni.setClipboardData({
data: url,
success: () => {
uni.showToast({
title: t('workplace.link_copied'),
icon: 'none',
});
},
});
}
onMounted(() => {
loadWorkplaceData(state);
});
</script>
<style scoped>
.workplace-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--bg, #F5F8FC);
color: var(--text, #101828);
position: relative;
}
/* 顶部受控局部极光弥散微光晕 (< 8% 面积) */
.workplace-aurora-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 140px;
background: linear-gradient(180deg, rgba(11, 92, 255, 0.08) 0%, rgba(245, 248, 252, 0) 100%);
pointer-events: none;
}
/* 顶栏 */
.workplace-topbar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 48px;
padding-inline: 16px;
background-color: var(--surface, #FFFFFF);
border-bottom: 1px solid var(--border, #E3E7ED);
z-index: 10;
}
.workplace-identity {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
min-height: 44px;
}
.identity-name {
font-size: 16px;
font-weight: 700;
color: var(--text, #101828);
}
.identity-badge {
font-size: 12px;
font-weight: 600;
color: var(--primary, #0B5CFF);
background-color: var(--primary-soft, #EBF5FF);
padding: 2px 6px;
border-radius: 4px;
}
.identity-arrow {
font-size: 12px;
color: var(--text2, #64748B);
}
.radar-status-pill {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
height: 32px;
padding: 0 10px;
border-radius: 16px;
background-color: rgba(18, 183, 106, 0.1);
border: 1px solid rgba(18, 183, 106, 0.3);
}
.radar-pulse-dot {
width: 6px;
height: 6px;
border-radius: 3px;
background-color: #12B76A;
}
.radar-status-text {
font-size: 12px;
font-weight: 600;
color: #12B76A;
}
/* 滚动内容区 */
.workplace-scroll {
flex: 1;
padding-inline: 16px;
}
.greeting-banner {
padding-top: 16px;
padding-bottom: 12px;
}
.greeting-title {
font-size: 20px;
font-weight: 700;
color: var(--text, #101828);
}
.greeting-subtitle {
font-size: 14px;
color: var(--text2, #64748B);
margin-top: 4px;
}
/* 模组 1:闪电出示微展台 */
.quick-showcase-section {
margin-bottom: 16px;
}
.showcase-card {
display: flex;
flex-direction: row;
background-color: var(--surface, #FFFFFF);
border: 1px solid var(--border, #E3E7ED);
border-radius: 16px;
padding: 16px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
gap: 16px;
}
.showcase-qr-wrap {
width: 104px;
height: 104px;
background-color: var(--bg, #F5F8FC);
border: 1px dashed var(--border, #E3E7ED);
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.showcase-qr-code {
width: 96px;
height: 96px;
}
.showcase-qr-placeholder {
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
}
.qr-pulse-block {
width: 60px;
height: 60px;
border-radius: 8px;
background-color: var(--border, #E3E7ED);
}
.showcase-meta {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.meta-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.meta-title {
font-size: 16px;
font-weight: 600;
color: var(--text, #101828);
}
.meta-preview-btn {
font-size: 12px;
font-weight: 600;
color: var(--primary, #0B5CFF);
}
.meta-company {
font-size: 14px;
color: var(--text2, #64748B);
}
.meta-nfc-tag {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
margin-top: 4px;
}
.nfc-dot {
width: 6px;
height: 6px;
border-radius: 3px;
background-color: var(--primary, #0B5CFF);
}
.nfc-tag-text {
font-size: 12px;
color: var(--text2, #64748B);
}
.meta-actions {
margin-top: 8px;
}
.copy-link-btn {
height: 44px;
min-height: 44px;
padding: 0 14px;
border-radius: 8px;
background-color: var(--bg, #F5F8FC);
border: 1px solid var(--border, #E3E7ED);
display: flex;
justify-content: center;
align-items: center;
}
.copy-link-text {
font-size: 12px;
font-weight: 600;
color: var(--text, #101828);
}
/* 模组 2:今日战情报表 */
.kpi-grid-section {
margin-bottom: 16px;
}
.section-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.section-title {
font-size: 16px;
font-weight: 700;
color: var(--text, #101828);
}
.section-action {
font-size: 12px;
font-weight: 600;
color: var(--primary, #0B5CFF);
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.kpi-card {
background-color: var(--surface, #FFFFFF);
border: 1px solid var(--border, #E3E7ED);
border-radius: 16px;
padding: 12px 14px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.kpi-label {
font-size: 12px;
color: var(--text2, #64748B);
}
.kpi-value-row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
margin-top: 6px;
}
.kpi-value {
font-size: 24px;
font-weight: 700;
color: var(--text, #101828);
line-height: 1.2;
}
.kpi-delta {
font-size: 12px;
font-weight: 600;
color: #12B76A;
}
.kpi-tag-hot {
font-size: 12px;
font-weight: 700;
color: #F04438;
}
/* 模组 3:实时雷达高光 */
.radar-pulse-section {
margin-bottom: 16px;
}
.pulse-card {
background-color: var(--surface, #FFFFFF);
border: 1px solid var(--border, #E3E7ED);
border-radius: 16px;
padding: 14px 16px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.pulse-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.pulse-geo {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.geo-badge {
background-color: var(--primary-soft, #EBF5FF);
padding: 2px 6px;
border-radius: 6px;
}
.geo-code {
font-size: 12px;
font-weight: 700;
color: var(--primary, #0B5CFF);
}
.geo-city {
font-size: 14px;
font-weight: 600;
color: var(--text, #101828);
}
.pulse-score-pill {
background-color: rgba(240, 68, 56, 0.1);
padding: 2px 8px;
border-radius: 12px;
}
.pulse-score-text {
font-size: 12px;
font-weight: 700;
color: #F04438;
}
.pulse-desc {
font-size: 14px;
color: var(--text2, #64748B);
margin-top: 8px;
line-height: 1.5;
}
.pulse-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
.pulse-time {
font-size: 12px;
color: var(--text2, #64748B);
}
.pulse-action-btn {
height: 44px;
min-height: 44px;
padding: 0 16px;
border-radius: 8px;
background-color: var(--primary, #0B5CFF);
display: flex;
justify-content: center;
align-items: center;
}
.pulse-action-text {
font-size: 12px;
font-weight: 600;
color: #FFFFFF;
}
/* 模组 4:今日待办商机 */
.leads-todo-section {
margin-bottom: 16px;
}
.leads-flow {
display: flex;
flex-direction: column;
gap: 8px;
}
.lead-item-card {
background-color: var(--surface, #FFFFFF);
border: 1px solid var(--border, #E3E7ED);
border-radius: 14px;
padding: 12px 14px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
.lead-top {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.lead-name {
font-size: 14px;
font-weight: 700;
color: var(--text, #101828);
}
.lead-status-chip {
font-size: 12px;
font-weight: 600;
color: #F79009;
background-color: rgba(247, 144, 9, 0.1);
padding: 2px 6px;
border-radius: 4px;
}
.lead-company {
font-size: 12px;
color: var(--text2, #64748B);
margin-top: 4px;
}
.lead-notes {
font-size: 12px;
color: var(--text, #101828);
margin-top: 6px;
}
/* 模组 5:AI 销售副驾驶 */
.ai-copilot-section {
margin-bottom: 16px;
}
.ai-card {
background: linear-gradient(135deg, #FFFFFF 0%, #F5F8FC 100%);
border: 1px solid rgba(11, 92, 255, 0.2);
border-radius: 14px;
padding: 12px 14px;
}
.ai-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.ai-spark-dot {
width: 8px;
height: 8px;
border-radius: 4px;
background-color: var(--primary, #0B5CFF);
}
.ai-title {
font-size: 14px;
font-weight: 700;
color: var(--text, #101828);
}
.ai-desc {
font-size: 12px;
color: var(--text2, #64748B);
margin-top: 6px;
line-height: 1.5;
}
.workplace-spacer {
height: calc(88px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
}
/* 底部常驻悬浮主 CTA 栏 (一屏仅1个主按钮) */
.workplace-cta-bar {
position: fixed;
left: 0;
right: 0;
bottom: var(--window-bottom, 0px);
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
background-color: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(8px);
border-top: 1px solid var(--border, #E3E7ED);
z-index: 100;
}
.cta-inner {
width: 100%;
}
.primary-cta-btn {
height: 48px;
width: 100%;
border-radius: 12px;
background-color: var(--primary, #0B5CFF);
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 8px 20px rgba(11, 92, 255, 0.22);
}
.primary-cta-btn-text {
font-size: 16px;
font-weight: 600;
color: #FFFFFF;
}
/* RTL 适配 */
.rtl-layout {
direction: rtl;
}
</style>