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

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

查看文件

@@ -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 };
}