fix(portal): 微官网预览页新增极简返回按钮与预览态徽标,修复自定义导航栏无法回退的问题

这个提交包含在:
wikiboss
2026-09-21 15:37:06 +08:00
父节点 2d0b655200
当前提交 38506bbd40
共有 8 个文件被更改,包括 397 次插入 和 127 次删除

查看文件

@@ -389,7 +389,7 @@ describe('OmniCard AI 名片主潜客详情业务逻辑 (useLeadDetail.uts)', ()
});
});
describe('5. 国际销售触达与 AI 赋能函数', () => {
describe('5. 国际销售触达与智能话术函数', () => {
it('5.1 formatWhatsAppUrl 清洗国际号码并生成 wa.me 链接', () => {
const { formatWhatsAppUrl } = loadUseLeadDetail();

查看文件

@@ -332,4 +332,72 @@ describe('OmniCard AI 用户端微官网 Bento 首页与 Hero 业务逻辑测试
expect(visible.map((b: any) => b.type)).toEqual(['case', 'article', 'social']);
});
});
describe('7. 顶栏返回入口与智能导航 (executePortalGoBack)', () => {
it('7.1 当页面栈深度大于 1 时调用 uni.navigateBack', () => {
// @ts-ignore
const { executePortalGoBack } = require('../../../mpuniapp/pages/portal/usePortal.uts');
let navigatedBack = false;
let switchedTabUrl = '';
(global as any).getCurrentPages = () => [{}, {}];
(global as any).uni = {
...(global as any).uni,
navigateBack: () => {
navigatedBack = true;
},
switchTab: (p: { url: string }) => {
switchedTabUrl = p.url;
},
};
const result = executePortalGoBack(true);
expect(navigatedBack).toBe(true);
expect(result.action).toBe('navigateBack');
expect(switchedTabUrl).toBe('');
});
it('7.2 当页面栈为 1 且为名片主 (isOwner) 时安全跳转至名片管理首页', () => {
// @ts-ignore
const { executePortalGoBack } = require('../../../mpuniapp/pages/portal/usePortal.uts');
let navigatedBack = false;
let switchedTabUrl = '';
(global as any).getCurrentPages = () => [{}];
(global as any).uni = {
...(global as any).uni,
navigateBack: () => {
navigatedBack = true;
},
switchTab: (p: { url: string }) => {
switchedTabUrl = p.url;
},
};
const result = executePortalGoBack(true);
expect(navigatedBack).toBe(false);
expect(result.action).toBe('switchTab');
expect(result.targetUrl).toBe('/pages/owner/card/index');
expect(switchedTabUrl).toBe('/pages/owner/card/index');
});
it('7.3 当页面栈为 1 且为访客时安全跳转至大厅', () => {
// @ts-ignore
const { executePortalGoBack } = require('../../../mpuniapp/pages/portal/usePortal.uts');
let switchedTabUrl = '';
(global as any).getCurrentPages = () => [{}];
(global as any).uni = {
...(global as any).uni,
switchTab: (p: { url: string }) => {
switchedTabUrl = p.url;
},
};
const result = executePortalGoBack(false);
expect(result.targetUrl).toBe('/pages/owner/hall/index');
expect(switchedTabUrl).toBe('/pages/owner/hall/index');
});
});
});

查看文件

@@ -22,6 +22,7 @@ export interface LocaleMeta {
export type OmniMessageKey =
| 'common.confirm'
| 'common.cancel'
| 'common.back'
| 'common.save'
| 'common.delete'
| 'common.edit'
@@ -31,6 +32,7 @@ export type OmniMessageKey =
| 'common.error'
| 'common.empty'
| 'common.app_name'
| 'portal.preview_mode'
| 'card.save_contact'
| 'card.exchange'
| 'card.ai_chat'
@@ -89,6 +91,8 @@ export type OmniMessageKey =
| 'portal.appointment_time_call'
| 'portal.appointment_notes'
| 'portal.appointment_notes_placeholder'
| 'portal.appointment_photos'
| 'portal.appointment_photos_tip'
| 'portal.appointment_submit'
| 'portal.appointment_submitting'
| 'portal.appointment_success_tip'
@@ -109,6 +113,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': '确认',
'common.cancel': '取消',
'common.back': '返回',
'common.save': '保存',
'common.delete': '删除',
'common.edit': '编辑',
@@ -118,6 +123,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': '操作失败',
'common.empty': '暂无数据',
'common.app_name': 'OmniCard',
'portal.preview_mode': '预览中',
'card.save_contact': '保存到通讯录',
'card.exchange': '交换名片',
'card.ai_chat': 'AI 咨询',
@@ -141,41 +147,43 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.twitter': 'X',
'portal.wechat': '微信',
'portal.email': '邮件',
'portal.phone': '电话呼出',
'portal.case': '精选案例',
'portal.article': '深度洞见',
'portal.social': '社媒矩阵',
'portal.video': '展会视频',
'portal.block': '微官网内容',
'portal.contact_unset': '未设置该联系方式',
'portal.phone': '电话呼叫',
'portal.case': '案例详情',
'portal.article': '深度文章',
'portal.social': '社交矩阵',
'portal.video': '视频演示',
'portal.block': '便当盒内容',
'portal.contact_unset': '该联系方式未设置',
'portal.copied': '已复制',
'portal.copied_link': '已复制链接',
'portal.copied_whatsapp': '已复制 WhatsApp 链接',
'portal.copied_linkedin': '已复制 LinkedIn 链接',
'portal.copied_email': '已复制邮箱地址',
'portal.copied_telegram': '已复制 Telegram 链接',
'portal.copied_twitter': '已复制 X 链接',
'portal.copied_wechat': '已复制微信号',
'portal.collect': '收入大厅',
'portal.collected': '已收入大厅',
'portal.appointment': '商务预约',
'portal.appointment_desc': '预约 30 分钟跨国视频会谈或展会现场洽谈',
'portal.copied_link': '直链已复制',
'portal.copied_whatsapp': 'WhatsApp 号码已复制',
'portal.copied_linkedin': 'LinkedIn 链接已复制',
'portal.copied_email': '邮箱地址已复制',
'portal.copied_telegram': 'Telegram 账号已复制',
'portal.copied_twitter': 'X (Twitter) 链接已复制',
'portal.copied_wechat': '微信号已复制',
'portal.collect': '收入名片夹',
'portal.collected': '已收入',
'portal.appointment': '预约商谈',
'portal.appointment_desc': '快速预约 30 分钟全球跨时区视频会议或线下拜访',
'portal.book_now': '立即预约',
'portal.google_cal': 'Google 日历',
'portal.outlook_cal': 'Outlook 日历',
'portal.ics_download': '下载日历文件 (ICS)',
'portal.copied_meeting_link': '已复制预约日历链接',
'portal.appointment_form_title': '预约商务洽谈',
'portal.appointment_form_desc': '提交您的会谈意向与需求,将即时通知名片主',
'portal.ics_download': '下载 ICS 文件',
'portal.copied_meeting_link': '日程邀请链接已复制',
'portal.appointment_form_title': '预约商务会谈',
'portal.appointment_form_desc': '填写您的合作意向与时间偏好,需求将实时同步至名片主',
'portal.appointment_name': '您的姓名',
'portal.appointment_contact': '手机 / WhatsApp / 邮箱',
'portal.appointment_contact': '联系电话 / WhatsApp / 微信',
'portal.appointment_company': '所属公司 (选填)',
'portal.appointment_time': '期望会谈形式 / 时间',
'portal.appointment_time_video': '线上视频 (30分钟)',
'portal.appointment_time_offline': '展会现场洽谈',
'portal.appointment_time_call': '电话快速沟通',
'portal.appointment_notes': '合作需求 / 洽谈议题',
'portal.appointment_time': '期望沟通时段 / 形式',
'portal.appointment_time_video': '线上视频沟通 (30分钟)',
'portal.appointment_time_offline': '展会现场洽谈 / 线下拜访',
'portal.appointment_time_call': '快速电话沟通',
'portal.appointment_notes': '合作需求与议题简述',
'portal.appointment_notes_placeholder': '请简要说明您的业务需求或采购意向...',
'portal.appointment_photos': '名片或需求照片 (选填)',
'portal.appointment_photos_tip': '支持上传名片、采购清单或展位样品照片,最多3张',
'portal.appointment_submit': '提交预约需求',
'portal.appointment_submitting': '提交中...',
'portal.appointment_success_tip': '预约已提交!名片主已收到您的需求。',
@@ -189,6 +197,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': 'Confirm',
'common.cancel': 'Cancel',
'common.back': 'Back',
'common.save': 'Save',
'common.delete': 'Delete',
'common.edit': 'Edit',
@@ -198,6 +207,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'Error occurred',
'common.empty': 'No data available',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'Preview',
'card.save_contact': 'Save Contact',
'card.exchange': 'Exchange Card',
'card.ai_chat': 'AI Assistant',
@@ -209,9 +219,9 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'radar.visitor_trace': 'Visitor Trail',
'radar.lead_score': 'Lead Score',
'crm.contacts': 'Contacts',
'crm.leads': 'Customers',
'crm.leads': 'Leads',
'crm.follow_up': 'Follow-up',
'portal.unavailable': 'This page is unavailable',
'portal.unavailable': 'This page is currently unavailable',
'portal.retry': 'Reload',
'portal.pronounce': 'Pronounce',
'portal.pronouncing': 'Playing',
@@ -222,12 +232,12 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.wechat': 'WeChat',
'portal.email': 'Email',
'portal.phone': 'Call',
'portal.case': 'Case study',
'portal.case': 'Case Study',
'portal.article': 'Insight',
'portal.social': 'Social',
'portal.video': 'Video',
'portal.block': 'Content',
'portal.contact_unset': 'This contact method is not set',
'portal.contact_unset': 'This contact method is not configured',
'portal.copied': 'Copied',
'portal.copied_link': 'Link copied',
'portal.copied_whatsapp': 'WhatsApp link copied',
@@ -238,15 +248,15 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.copied_wechat': 'WeChat ID copied',
'portal.collect': 'Save to Hall',
'portal.collected': 'Saved to Hall',
'portal.appointment': 'Schedule Meeting',
'portal.appointment_desc': 'Book a 30-min global video sync or in-person talk',
'portal.appointment': 'Book Appointment',
'portal.appointment_desc': 'Book a 30-min global video meeting or in-person chat',
'portal.book_now': 'Book Now',
'portal.google_cal': 'Google Calendar',
'portal.outlook_cal': 'Outlook Calendar',
'portal.ics_download': 'Download ICS File',
'portal.copied_meeting_link': 'Calendar link copied',
'portal.appointment_form_title': 'Schedule Business Meeting',
'portal.appointment_form_desc': 'Submit your meeting intent & requirements to the card owner',
'portal.appointment_form_title': 'Book a Meeting',
'portal.appointment_form_desc': 'Share your project goals and preferred time directly with the card owner',
'portal.appointment_name': 'Your Name',
'portal.appointment_contact': 'Phone / WhatsApp / Email',
'portal.appointment_company': 'Company (Optional)',
@@ -256,6 +266,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'Quick Phone Call',
'portal.appointment_notes': 'Topics & Requirements',
'portal.appointment_notes_placeholder': 'Briefly describe your project or partnership goals...',
'portal.appointment_photos': 'Card / Photo Attachments (Optional)',
'portal.appointment_photos_tip': 'Upload business card, project brief or product sample (up to 3)',
'portal.appointment_submit': 'Submit Appointment',
'portal.appointment_submitting': 'Submitting...',
'portal.appointment_success_tip': 'Appointment submitted! The card owner will contact you shortly.',
@@ -269,6 +281,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': 'تأكيد',
'common.cancel': 'إلغاء',
'common.back': 'رجوع',
'common.save': 'حفظ',
'common.delete': 'حذف',
'common.edit': 'تعديل',
@@ -278,6 +291,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'حدث خطأ',
'common.empty': 'لا توجد بيانات',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'معاينة',
'card.save_contact': 'حفظ جهة الاتصال',
'card.exchange': 'تبادل البطاقات',
'card.ai_chat': 'مساعد الذكاء الاصطناعي',
@@ -336,6 +350,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'مكالمة هاتفية سريعة',
'portal.appointment_notes': 'المتطلبات وموضوع النقاش',
'portal.appointment_notes_placeholder': 'صف بإيجاز احتياجاتك أو أهداف التعاون...',
'portal.appointment_photos': 'صور البطاقة أو متطلبات العمل (اختياري)',
'portal.appointment_photos_tip': 'يمكنك إرفاق بطاقة عملك أو متطلباتك (بحد أقصى 3 صور)',
'portal.appointment_submit': 'إرسال طلب الموعد',
'portal.appointment_submitting': 'جاري الإرسال...',
'portal.appointment_success_tip': 'تم إرسال طلبك بنجاح! سيتواصل معك صاحب البطاقة قريباً.',
@@ -349,6 +365,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': 'Confirmar',
'common.cancel': 'Cancelar',
'common.back': 'Volver',
'common.save': 'Guardar',
'common.delete': 'Eliminar',
'common.edit': 'Editar',
@@ -358,6 +375,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'Ha ocurrido un error',
'common.empty': 'No hay datos',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'Vista previa',
'card.save_contact': 'Guardar contacto',
'card.exchange': 'Intercambiar tarjeta',
'card.ai_chat': 'Asistente IA',
@@ -416,6 +434,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'Llamada Rápida',
'portal.appointment_notes': 'Requisitos y Temas',
'portal.appointment_notes_placeholder': 'Describe brevemente tus necesidades o propuesta...',
'portal.appointment_photos': 'Fotos de tarjeta o requisitos (Opcional)',
'portal.appointment_photos_tip': 'Sube tu tarjeta de presentación o fotos del proyecto (hasta 3)',
'portal.appointment_submit': 'Enviar Solicitud',
'portal.appointment_submitting': 'Enviando...',
'portal.appointment_success_tip': '¡Solicitud enviada! El titular se pondrá en contacto pronto.',
@@ -429,6 +449,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': 'Bestätigen',
'common.cancel': 'Abbrechen',
'common.back': 'Zurück',
'common.save': 'Speichern',
'common.delete': 'Löschen',
'common.edit': 'Bearbeiten',
@@ -438,6 +459,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'Ein Fehler ist aufgetreten',
'common.empty': 'Keine Daten vorhanden',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'Vorschau',
'card.save_contact': 'Kontakt speichern',
'card.exchange': 'Visitenkarte tauschen',
'card.ai_chat': 'KI-Assistent',
@@ -496,6 +518,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'Kurzer Telefonanruf',
'portal.appointment_notes': 'Themen & Anforderungen',
'portal.appointment_notes_placeholder': 'Beschreiben Sie kurz Ihr Vorhaben oder Ihre Fragen...',
'portal.appointment_photos': 'Visitenkarte / Projektfotos (Optional)',
'portal.appointment_photos_tip': 'Visitenkarte, Projektskizze oder Produktfotos hochladen (max. 3)',
'portal.appointment_submit': 'Termin anfragen',
'portal.appointment_submitting': 'Wird gesendet...',
'portal.appointment_success_tip': 'Anfrage gesendet! Der Karteninhaber meldet sich in Kürze.',
@@ -509,6 +533,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': 'Confirmer',
'common.cancel': 'Annuler',
'common.back': 'Retour',
'common.save': 'Enregistrer',
'common.delete': 'Supprimer',
'common.edit': 'Modifier',
@@ -518,6 +543,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'Une erreur est survenue',
'common.empty': 'Aucune donnée',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'Aperçu',
'card.save_contact': 'Enregistrer le contact',
'card.exchange': 'Échanger la carte',
'card.ai_chat': 'Assistant IA',
@@ -576,6 +602,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'Appel rapide',
'portal.appointment_notes': 'Besoins & Sujets',
'portal.appointment_notes_placeholder': 'Décrivez brièvement votre projet ou vos attentes...',
'portal.appointment_photos': 'Carte de visite / Photos (Optionnel)',
'portal.appointment_photos_tip': 'Ajoutez votre carte ou photos du projet (jusqu\'à 3)',
'portal.appointment_submit': 'Envoyer la demande',
'portal.appointment_submitting': 'Envoi en cours...',
'portal.appointment_success_tip': 'Demande envoyée ! Le titulaire vous contactera sous peu.',
@@ -589,6 +617,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
messages: {
'common.confirm': '確認',
'common.cancel': 'キャンセル',
'common.back': '戻る',
'common.save': '保存',
'common.delete': '削除',
'common.edit': '編集',
@@ -598,6 +627,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'common.error': 'エラーが発生しました',
'common.empty': 'データがありません',
'common.app_name': 'OmniCard',
'portal.preview_mode': 'プレビュー',
'card.save_contact': '連絡先に保存',
'card.exchange': '名刺を交換',
'card.ai_chat': 'AI アシスタント',
@@ -656,6 +686,8 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
'portal.appointment_time_call': 'お電話でのクイック相談',
'portal.appointment_notes': 'ご相談内容・ご要望',
'portal.appointment_notes_placeholder': '事業内容やご興味のある点をご記入ください...',
'portal.appointment_photos': '名刺・資料写真(任意)',
'portal.appointment_photos_tip': '名刺や商品、仕様書の写真を最大3枚まで添付できます',
'portal.appointment_submit': '予約・相談を送信',
'portal.appointment_submitting': '送信中...',
'portal.appointment_success_tip': 'ご予約を送信しました。担当者より追ってご連絡いたします。',

查看文件

@@ -125,12 +125,12 @@
</view>
</view>
<!-- 3. AI Copilot Icebreaker Script Bento Card -->
<!-- 3. Follow-up Script Bento Card -->
<view class="lead-card lead-section ai-card">
<view class="ai-header-row">
<view class="ai-identity">
<view class="ai-pulse-dot"></view>
<text class="ai-badge-text">AI Copilot</text>
<text class="ai-badge-text">{{ aiScriptTitle }}</text>
</view>
<view class="ai-gen-action" @click="handleGenerateAiScript">
<text class="ai-gen-btn-text">{{ generateAiLabel }}</text>
@@ -424,6 +424,10 @@ const saveLabel = computed((): string => {
return resolveLeadDetailText('save');
});
const aiScriptTitle = computed((): string => {
return resolveLeadDetailText('aiScriptTitle');
});
function t(key: LeadDetailCopyKey): string {
return resolveLeadDetailText(key);
}

查看文件

@@ -1475,6 +1475,7 @@ onShow(() => {
color: #0B5CFF;
background-color: #F7F8FA;
border-color: #E3E7ED;
box-shadow: 0 0 0 1px #667085;
}
/* Error Notice */

查看文件

@@ -51,7 +51,8 @@ export type LeadDetailCopyKey =
| 'applyScriptAction'
| 'scriptCopiedToast'
| 'savedContactToast'
| 'leadTitle';
| 'leadTitle'
| 'aiScriptTitle';
export type LeadDetailPageOptions = {
id?: string;
@@ -217,6 +218,9 @@ export function resolveLeadDetailText(key: LeadDetailCopyKey): string {
if (key === 'leadTitle') {
return zh ? '潜客档案' : 'Lead Profile';
}
if (key === 'aiScriptTitle') {
return zh ? '智能跟进建议' : 'AI Copilot';
}
if (key === 'noteSavedToast') {
return zh ? '备忘已保存' : 'Note saved';
}

查看文件

@@ -3,10 +3,24 @@
<!-- 顶部受控局部极光微光晕 (面积 < 8%) -->
<view class="ambient-aurora-glow"></view>
<!-- 顶栏:多语言与主客态操作 -->
<view class="locale-bar">
<view class="locale-selector-container">
<i18n-selector :model-value="currentLang" @change="switchLang" />
<!-- 顶栏:返回按钮、名片主预览微标签、多语言与主客态操作 -->
<view class="locale-bar portal-top-bar">
<view class="top-bar-left">
<view
class="portal-back-btn"
role="button"
:aria-label="backLabel"
@click="handleGoBack"
>
<text class="portal-back-arrow">‹</text>
</view>
<view v-if="isOwner" class="preview-badge">
<view class="preview-dot"></view>
<text class="preview-badge-text">{{ previewModeLabel }}</text>
</view>
<view class="locale-selector-container">
<i18n-selector :model-value="currentLang" @change="switchLang" />
</view>
</view>
<view class="locale-exchange-wrap" @click="handleHeaderAction">
<text class="locale-exchange-link">{{ headerLinkText }}</text>
@@ -203,7 +217,7 @@
</view>
</scroll-view>
<!-- 4. AI 智能顾问微光悬浮入口 (Secondary CTA) -->
<!-- 4. 在线顾问悬浮入口 (Secondary CTA) -->
<view
v-if="state.card != null"
class="ai-entry-capsule"
@@ -239,88 +253,107 @@
</view>
<!-- 预约与需求提报表单 -->
<view v-else class="appointment-form-wrap">
<view class="sheet-header">
<view class="sheet-title-row">
<text class="sheet-title">{{ textOf('portal.appointment_form_title') }}</text>
<text class="sheet-close" @click="closeAppointmentSheet">×</text>
<scroll-view v-else scroll-y="true" class="appointment-sheet-scroll">
<view class="appointment-form-wrap">
<view class="sheet-header">
<view class="sheet-title-row">
<text class="sheet-title">{{ textOf('portal.appointment_form_title') }}</text>
<text class="sheet-close" @click="closeAppointmentSheet">×</text>
</view>
<text class="sheet-subtitle">{{ textOf('portal.appointment_form_desc') }}</text>
</view>
<text class="sheet-subtitle">{{ textOf('portal.appointment_form_desc') }}</text>
</view>
<!-- 期望会谈形式快捷胶囊 -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_time') }}</text>
<view class="sheet-time-chips">
<view
v-for="opt in appointmentTimeOptions"
:key="opt.key"
class="sheet-time-chip"
:class="{ 'sheet-time-chip-active': appointmentForm.preferredTime === opt.label }"
@click="selectPreferredTime(opt.label)"
>
<text
class="sheet-time-chip-text"
:class="{ 'sheet-time-chip-text-active': appointmentForm.preferredTime === opt.label }"
>{{ opt.label }}</text>
<!-- 期望会谈形式快捷胶囊 -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_time') }}</text>
<view class="sheet-time-chips">
<view
v-for="opt in appointmentTimeOptions"
:key="opt.key"
class="sheet-time-chip"
:class="{ 'sheet-time-chip-active': appointmentForm.preferredTime === opt.label }"
@click="selectPreferredTime(opt.label)"
>
<text
class="sheet-time-chip-text"
:class="{ 'sheet-time-chip-text-active': appointmentForm.preferredTime === opt.label }"
>{{ opt.label }}</text>
</view>
</view>
</view>
</view>
<!-- 姓名输入 (必填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_name') }} *</text>
<input
class="sheet-field-input"
v-model="appointmentForm.name"
:placeholder="textOf('portal.appointment_name')"
<!-- 姓名输入 (必填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_name') }} *</text>
<input
class="sheet-field-input"
v-model="appointmentForm.name"
:placeholder="textOf('portal.appointment_name')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 联系方式输入 (必填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_contact') }} *</text>
<input
class="sheet-field-input"
v-model="appointmentForm.contact"
:placeholder="textOf('portal.appointment_contact')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 公司名称 (选填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_company') }}</text>
<input
class="sheet-field-input"
v-model="appointmentForm.company"
:placeholder="textOf('portal.appointment_company')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 需求与议题说明 (多行文本框) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_notes') }}</text>
<textarea
class="sheet-field-textarea"
v-model="appointmentForm.notes"
:placeholder="textOf('portal.appointment_notes_placeholder')"
:disabled="appointmentFormSubmitting"
:maxlength="500"
/>
</view>
<!-- 名片或需求照片 (选填附件) -->
<view class="sheet-field">
<view class="sheet-field-header-row">
<text class="sheet-field-label">{{ textOf('portal.appointment_photos') }}</text>
<text class="sheet-field-tip">{{ textOf('portal.appointment_photos_tip') }}</text>
</view>
<view class="sheet-uploader-wrap">
<ImageUploader
v-model="appointmentForm.photos"
:multiple="true"
:maxCount="3"
:disabled="appointmentFormSubmitting"
/>
</view>
</view>
<!-- 提交按钮 -->
<button
class="primary-cta-btn appointment-submit-btn"
:class="{ 'appointment-submit-btn-disabled': appointmentFormSubmitting }"
:disabled="appointmentFormSubmitting"
/>
@click="submitAppointmentForm"
>
<text class="primary-cta-btn-text">{{ appointmentFormSubmitting ? textOf('portal.appointment_submitting') : textOf('portal.appointment_submit') }}</text>
</button>
</view>
<!-- 联系方式输入 (必填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_contact') }} *</text>
<input
class="sheet-field-input"
v-model="appointmentForm.contact"
:placeholder="textOf('portal.appointment_contact')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 公司名称 (选填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_company') }}</text>
<input
class="sheet-field-input"
v-model="appointmentForm.company"
:placeholder="textOf('portal.appointment_company')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 需求与议题说明 (选填) -->
<view class="sheet-field">
<text class="sheet-field-label">{{ textOf('portal.appointment_notes') }}</text>
<textarea
class="sheet-field-textarea"
v-model="appointmentForm.notes"
:placeholder="textOf('portal.appointment_notes_placeholder')"
:disabled="appointmentFormSubmitting"
/>
</view>
<!-- 提交按钮 -->
<button
class="primary-cta-btn appointment-submit-btn"
:class="{ 'appointment-submit-btn-disabled': appointmentFormSubmitting }"
:disabled="appointmentFormSubmitting"
@click="submitAppointmentForm"
>
<text class="primary-cta-btn-text">{{ appointmentFormSubmitting ? textOf('portal.appointment_submitting') : textOf('portal.appointment_submit') }}</text>
</button>
</view>
</scroll-view>
</view>
</view>
</view>
@@ -347,6 +380,7 @@ import {
checkIsCardOwner,
resolvePortalCtaText,
resolvePortalHeaderLinkText,
executePortalGoBack,
trackAppointmentAction,
PortalQuickAction,
} from './usePortal.uts';
@@ -364,6 +398,7 @@ import {
} from '../../common/locale.uts';
import I18nSelector from '../../components/i18n-selector/i18n-selector.uvue';
import AudioPronounce from '../../components/audio-pronounce/audio-pronounce.uvue';
import ImageUploader from '../../components/image-uploader/image-uploader.uvue';
import { releaseAudioPlayer } from './useAudioPlayer.uts';
import { utsLightColors, utsRadius, utsSpacing, utsTypography } from '../../common/tokens.uts';
@@ -435,6 +470,14 @@ const headerLinkText = computed((): string => {
return resolvePortalHeaderLinkText(isOwner.value, currentLang.value);
});
const backLabel = computed((): string => {
return textOf('common.back');
});
const previewModeLabel = computed((): string => {
return textOf('portal.preview_mode');
});
const collectLabel = computed((): string => {
return resolvePortalCollectText('label', currentLang.value);
});
@@ -615,6 +658,10 @@ function openExchange(): void {
});
}
function handleGoBack(): void {
executePortalGoBack(isOwner.value);
}
function handleHeaderAction(): void {
if (!state.card) return;
const cardId = state.card!.id != null ? String(state.card!.id) : currentCardId.value;
@@ -642,6 +689,7 @@ type AppointmentFormState = {
company: string;
preferredTime: string;
notes: string;
photos: string[];
};
const isAppointmentSheetOpen = ref(false);
@@ -654,6 +702,7 @@ const appointmentForm = reactive<AppointmentFormState>({
company: '',
preferredTime: '',
notes: '',
photos: [] as string[],
});
const appointmentTimeOptions = computed((): Array<{ key: string; label: string }> => [
@@ -681,6 +730,7 @@ function closeAppointmentSheet(): void {
appointmentForm.contact = '';
appointmentForm.company = '';
appointmentForm.notes = '';
appointmentForm.photos = [] as string[];
appointmentSubmitted.value = false;
}
}
@@ -702,7 +752,10 @@ async function submitAppointmentForm(): Promise<void> {
appointmentFormSubmitting.value = true;
try {
const isEmail = contact.includes('@');
const fullNotes = `[商务预约] 期望形式: ${appointmentForm.preferredTime || '未指定'}${appointmentForm.notes.trim() ? `\n需求与议题: ${appointmentForm.notes.trim()}` : ''}`;
const photoNotes = appointmentForm.photos.length > 0
? `\n附带资料照片: ${appointmentForm.photos.join(' , ')}`
: '';
const fullNotes = `[商务预约] 期望形式: ${appointmentForm.preferredTime || '未指定'}${appointmentForm.notes.trim() ? `\n需求与议题: ${appointmentForm.notes.trim()}` : ''}${photoNotes}`;
await submitLead({
cardId: state.card!.id,
@@ -854,12 +907,13 @@ onUnload(() => {
}
.portal-rtl .hero-company-arrow,
.portal-rtl .locale-exchange-arrow,
.portal-rtl .bento-arrow-icon {
.portal-rtl .bento-arrow-icon,
.portal-rtl .portal-back-arrow {
display: inline-block;
transform: scaleX(-1);
}
/* 顶栏 */
/* 顶栏与返回入口 */
.locale-bar {
position: relative;
display: flex;
@@ -867,11 +921,63 @@ onUnload(() => {
justify-content: space-between;
align-items: center;
min-height: 44px;
padding-inline: 16px;
padding-inline: 12px 16px;
background-color: transparent;
box-sizing: border-box;
z-index: 10;
}
.top-bar-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.portal-back-btn {
width: 36px;
height: 36px;
min-width: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
background-color: rgba(255, 255, 255, 0.85);
border: 1px solid var(--oc-border, #EBF1F7);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
cursor: pointer;
transition: opacity 150ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.portal-back-btn:active {
opacity: 0.75;
}
.portal-back-arrow {
font-size: 24px;
font-weight: 400;
color: var(--oc-text, #101828);
line-height: 1;
margin-top: -2px;
}
.preview-badge {
display: flex;
flex-direction: row;
align-items: center;
padding-inline: 8px;
height: 24px;
background-color: var(--oc-primary-soft, #EBF5FF);
border: 1px solid rgba(0, 136, 255, 0.2);
border-radius: 9999px;
}
.preview-dot {
width: 6px;
height: 6px;
border-radius: 3px;
background-color: var(--oc-primary, #0088FF);
margin-inline-end: 4px;
}
.preview-badge-text {
font-size: 11px;
font-weight: 600;
color: var(--oc-primary, #0088FF);
}
.locale-selector-container {
display: flex;
align-items: center;
@@ -1284,7 +1390,7 @@ onUnload(() => {
height: calc(100px + var(--window-bottom, 0px) + env(safe-area-inset-bottom, 0px));
}
/* 4. AI 智能中枢入口胶囊 (次级入口,轻量极光微光晕) */
/* 4. 在线顾问入口胶囊 (次级入口,轻量极光微光晕) */
.ai-entry-capsule {
position: fixed;
inset-inline-end: 16px;
@@ -1494,6 +1600,10 @@ onUnload(() => {
color: var(--oc-text-2, #64748B);
}
/* 商务预约与需求提报表单样式 */
.appointment-sheet-scroll {
max-height: 75vh;
overflow-y: auto;
}
.appointment-form-wrap {
display: flex;
flex-direction: column;
@@ -1503,12 +1613,27 @@ onUnload(() => {
flex-direction: column;
margin-block-end: 12px;
}
.sheet-field-header-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-block-end: 6px;
}
.sheet-field-label {
font-size: 13px;
font-weight: 600;
color: var(--oc-text, #101828);
margin-block-end: 6px;
}
.sheet-field-tip {
font-size: 11px;
color: var(--oc-text-2, #64748B);
}
.sheet-uploader-wrap {
margin-top: 4px;
margin-bottom: 6px;
}
.sheet-time-chips {
display: flex;
flex-direction: row;
@@ -1552,12 +1677,13 @@ onUnload(() => {
box-sizing: border-box;
}
.sheet-field-textarea {
min-height: 72px;
height: 72px;
min-height: 96px;
height: 96px;
border: 1px solid var(--oc-border, #EBF1F7);
border-radius: 12px;
padding: 10px 14px;
font-size: 16px;
line-height: 1.5;
color: var(--oc-text, #101828);
background-color: #FFFFFF;
box-sizing: border-box;

查看文件

@@ -566,3 +566,38 @@ export function resolvePortalHeaderLinkText(isOwner: boolean, locale?: OmniLocal
return getOmniText(loc, 'card.exchange');
}
export type PortalGoBackResult = {
action: string;
targetUrl: string;
};
/**
* 微官网/预览页顶栏返回上一页或主控台智能路由处理
*/
export function executePortalGoBack(isOwner: boolean): PortalGoBackResult {
try {
if (typeof getCurrentPages === 'function') {
const pages = getCurrentPages();
if (pages != null && pages.length > 1) {
uni.navigateBack();
return { action: 'navigateBack', targetUrl: '' };
}
}
} catch (_e) {}
const target = isOwner ? '/pages/owner/card/index' : '/pages/owner/hall/index';
try {
uni.switchTab({
url: target,
});
} catch (_e) {
try {
uni.reLaunch({
url: target,
});
} catch (_err) {}
}
return { action: 'switchTab', targetUrl: target };
}