补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
724 行
18 KiB
Plaintext
724 行
18 KiB
Plaintext
/**
|
||
* OmniCard AI (CRMmp) - 微官网 Bento 首页与 Hero 身份区业务驱动层
|
||
* 严格遵循 uts 强类型规范与 AGENTS.md 约束
|
||
*/
|
||
|
||
import {
|
||
CardPortalDetail,
|
||
BentoBlockPublic,
|
||
RadarTrack,
|
||
} from '../../common/types.uts';
|
||
import {
|
||
fetchPortalCard,
|
||
recordRadarTrack,
|
||
} from '../../common/api.uts';
|
||
import { OmniLocale, getCurrentLocale, getOmniText } from '../../common/locale.uts';
|
||
import {
|
||
getCachedPortal,
|
||
setCachedPortal,
|
||
isCacheValid,
|
||
CachedPortalData,
|
||
} from './useCache.uts';
|
||
|
||
export const PORTAL_LOCALE_STORAGE_KEY = 'omni_locale';
|
||
|
||
type PortalCardWithSuggested = CardPortalDetail & {
|
||
suggestedLocale?: string;
|
||
};
|
||
|
||
/**
|
||
* 门户合法语言:zh-CN / en-US / ar-SA / es-ES / de-DE / fr-FR / ja-JP
|
||
*/
|
||
export function parsePortalLocale(raw: string): OmniLocale | null {
|
||
if (raw == null) {
|
||
return null;
|
||
}
|
||
const code = raw.trim();
|
||
if (
|
||
code === 'zh-CN' ||
|
||
code === 'en-US' ||
|
||
code === 'ar-SA' ||
|
||
code === 'es-ES' ||
|
||
code === 'de-DE' ||
|
||
code === 'fr-FR' ||
|
||
code === 'ja-JP'
|
||
) {
|
||
return code;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 进入页优先级:query.lang > storage > suggestedLocale > fallback(detectLocale)
|
||
*/
|
||
export function resolvePortalLocale(
|
||
queryLang: string,
|
||
stored: string,
|
||
suggested: string,
|
||
fallback: OmniLocale
|
||
): OmniLocale {
|
||
const fromQuery = parsePortalLocale(queryLang);
|
||
if (fromQuery != null) {
|
||
return fromQuery;
|
||
}
|
||
const fromStored = parsePortalLocale(stored);
|
||
if (fromStored != null) {
|
||
return fromStored;
|
||
}
|
||
const fromSuggested = parsePortalLocale(suggested);
|
||
if (fromSuggested != null) {
|
||
return fromSuggested;
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
export function readStoredPortalLocale(): string {
|
||
try {
|
||
const stored = uni.getStorageSync(PORTAL_LOCALE_STORAGE_KEY) as string;
|
||
if (stored == null) {
|
||
return '';
|
||
}
|
||
return stored;
|
||
} catch (_e) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
export function persistPortalLocale(code: OmniLocale): void {
|
||
try {
|
||
uni.setStorageSync(PORTAL_LOCALE_STORAGE_KEY, code);
|
||
} catch (_e) {}
|
||
}
|
||
|
||
export function readCardSuggestedLocale(card: CardPortalDetail | null): string {
|
||
if (card == null) {
|
||
return '';
|
||
}
|
||
const extra = card as PortalCardWithSuggested;
|
||
if (extra.suggestedLocale == null) {
|
||
return '';
|
||
}
|
||
return extra.suggestedLocale;
|
||
}
|
||
|
||
export type PortalState = {
|
||
card: CardPortalDetail | null;
|
||
loading: boolean;
|
||
errorMessage: string;
|
||
visitorId: string;
|
||
enterTime: number;
|
||
scrollDepth: number;
|
||
touchType: string;
|
||
isPlayingAudio: boolean;
|
||
audioContext: any | null;
|
||
};
|
||
|
||
const VISITOR_STORAGE_KEY = 'omni_visitor_id';
|
||
|
||
/**
|
||
* 获取或初始化访客唯一识别码 (持久化在本地 Storage)
|
||
*/
|
||
export function getOrCreateVisitorId(): string {
|
||
let visitorId = '';
|
||
try {
|
||
visitorId = uni.getStorageSync(VISITOR_STORAGE_KEY) as string;
|
||
} catch (e) {
|
||
visitorId = '';
|
||
}
|
||
|
||
if (!visitorId || visitorId.trim() === '') {
|
||
const randomPart = Math.random().toString(36).substring(2, 10);
|
||
visitorId = `vis_${Date.now()}_${randomPart}`;
|
||
try {
|
||
uni.setStorageSync(VISITOR_STORAGE_KEY, visitorId);
|
||
} catch (e) {}
|
||
}
|
||
return visitorId;
|
||
}
|
||
|
||
/**
|
||
* 创建微官网响应式状态初始对象
|
||
*/
|
||
export function createPortalState(): PortalState {
|
||
return {
|
||
card: null,
|
||
loading: false,
|
||
errorMessage: '',
|
||
visitorId: getOrCreateVisitorId(),
|
||
enterTime: Date.now(),
|
||
scrollDepth: 0,
|
||
touchType: 'link',
|
||
isPlayingAudio: false,
|
||
audioContext: null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 加载微官网名片聚合数据并上报首次触碰 (支持离线 LocalStorage 极速预加载与静默更新)
|
||
*/
|
||
export async function loadCardPortal(
|
||
state: PortalState,
|
||
cardIdOrSlug: string,
|
||
source: string = 'link'
|
||
): Promise<CardPortalDetail | null> {
|
||
state.errorMessage = '';
|
||
state.touchType = source;
|
||
state.enterTime = Date.now();
|
||
state.scrollDepth = 0;
|
||
|
||
// 1. 优先尝试秒级读取本地缓存快照,实现首屏 0ms 呈现
|
||
const cached: CachedPortalData | null = getCachedPortal(cardIdOrSlug);
|
||
let hasValidCache = false;
|
||
if (cached != null && isCacheValid(cached)) {
|
||
state.card = cached.data;
|
||
state.loading = false;
|
||
hasValidCache = true;
|
||
} else {
|
||
state.loading = true;
|
||
}
|
||
|
||
try {
|
||
const networkCardData = await fetchPortalCard(cardIdOrSlug);
|
||
if (networkCardData != null) {
|
||
state.card = networkCardData;
|
||
// 静默将最新名片快照持久化至本地
|
||
setCachedPortal(cardIdOrSlug, networkCardData);
|
||
if (networkCardData.slug != null && networkCardData.slug !== cardIdOrSlug) {
|
||
setCachedPortal(networkCardData.slug!, networkCardData);
|
||
}
|
||
}
|
||
state.loading = false;
|
||
|
||
// 载入成功后上报雷达初次触碰轨迹
|
||
if (state.card != null && state.card!.id != null) {
|
||
recordRadarTrack({
|
||
cardId: state.card!.id,
|
||
visitorId: state.visitorId,
|
||
touchType: state.touchType,
|
||
stayDurationMs: 0,
|
||
scrollDepth: 0,
|
||
viewedBlocks: ['hero'],
|
||
}).catch((_e: unknown) => {
|
||
// 轨迹上报容错,不阻断主流程
|
||
});
|
||
}
|
||
|
||
return state.card;
|
||
} catch (err: unknown) {
|
||
state.loading = false;
|
||
if (hasValidCache) {
|
||
// 命中有效缓存时弱网/异常平滑降级,维持缓存内容展示,避免阻断性白屏
|
||
return state.card;
|
||
}
|
||
state.card = null;
|
||
const msg =
|
||
err != null && typeof err === 'object' && 'message' in err
|
||
? String((err as { message: unknown }).message)
|
||
: '获取名片微官网详情失败';
|
||
state.errorMessage = msg;
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录门户滚动深度,只升不降
|
||
*/
|
||
export function updatePortalScrollDepth(
|
||
state: PortalState,
|
||
scrollTop: number,
|
||
scrollHeight: number,
|
||
windowHeight: number
|
||
): number {
|
||
if (scrollHeight <= 0) {
|
||
return state.scrollDepth;
|
||
}
|
||
const maxScroll = Math.max(1, scrollHeight - windowHeight);
|
||
const currentRatio = Math.min(1, Math.max(0, scrollTop / maxScroll));
|
||
const depth = Math.round(currentRatio * 100);
|
||
if (depth > state.scrollDepth) {
|
||
state.scrollDepth = depth;
|
||
}
|
||
return state.scrollDepth;
|
||
}
|
||
|
||
/**
|
||
* 按真实滚动深度收录已进入视口的模块,未滚动只记名片主页
|
||
*/
|
||
export function collectViewedBlockKeys(
|
||
card: CardPortalDetail | null,
|
||
scrollDepth: number
|
||
): string[] {
|
||
const keys: string[] = ['hero'];
|
||
if (card == null) {
|
||
return keys;
|
||
}
|
||
const depth = Math.min(100, Math.max(0, Math.round(scrollDepth)));
|
||
const blocks = filterVisibleBentoBlocks(card.bentoBlocks);
|
||
const sequence: string[] = [];
|
||
if (depth > 0) {
|
||
sequence.push('appointment');
|
||
}
|
||
const visibleCount = depth <= 0 ? 0 : Math.max(1, Math.ceil((depth / 100) * Math.max(blocks.length, 1)));
|
||
const take = Math.min(blocks.length, visibleCount);
|
||
for (let i = 0; i < take; i++) {
|
||
const block = blocks[i];
|
||
const id = block.id != null ? String(block.id).trim() : '';
|
||
if (id !== '') {
|
||
sequence.push(id);
|
||
} else if (block.type != null && String(block.type).trim() !== '') {
|
||
sequence.push(String(block.type).trim());
|
||
}
|
||
}
|
||
const seen: Record<string, boolean> = { hero: true };
|
||
for (let i = 0; i < sequence.length; i++) {
|
||
const key = sequence[i];
|
||
if (seen[key] === true) {
|
||
continue;
|
||
}
|
||
seen[key] = true;
|
||
keys.push(key);
|
||
}
|
||
return keys;
|
||
}
|
||
|
||
/**
|
||
* 上报访客停留时长与阅读深度,并带上已查看模块
|
||
*/
|
||
export async function reportPortalStay(
|
||
state: PortalState,
|
||
scrollDepth: number = 0
|
||
): Promise<RadarTrack | null> {
|
||
if (!state.card || !state.card.id) {
|
||
return null;
|
||
}
|
||
|
||
const depth = Math.min(100, Math.max(0, Math.round(scrollDepth)));
|
||
if (depth > state.scrollDepth) {
|
||
state.scrollDepth = depth;
|
||
}
|
||
const stayDurationMs = Math.max(0, Date.now() - state.enterTime);
|
||
try {
|
||
const track = await recordRadarTrack({
|
||
cardId: state.card.id,
|
||
visitorId: state.visitorId,
|
||
touchType: state.touchType,
|
||
stayDurationMs,
|
||
scrollDepth: state.scrollDepth,
|
||
viewedBlocks: collectViewedBlockKeys(state.card, state.scrollDepth),
|
||
});
|
||
return track;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 名字语音发音试听控制
|
||
*/
|
||
export function toggleAudioPronunciation(state: PortalState, audioUrl?: string): void {
|
||
if (!audioUrl || audioUrl.trim() === '') {
|
||
return;
|
||
}
|
||
|
||
if (state.isPlayingAudio && state.audioContext) {
|
||
try {
|
||
state.audioContext.stop();
|
||
} catch (e) {}
|
||
state.isPlayingAudio = false;
|
||
return;
|
||
}
|
||
|
||
if (!state.audioContext) {
|
||
state.audioContext = uni.createInnerAudioContext();
|
||
state.audioContext.onPlay(() => {
|
||
state.isPlayingAudio = true;
|
||
});
|
||
state.audioContext.onEnded(() => {
|
||
state.isPlayingAudio = false;
|
||
});
|
||
state.audioContext.onError(() => {
|
||
state.isPlayingAudio = false;
|
||
});
|
||
}
|
||
|
||
try {
|
||
state.audioContext.src = audioUrl;
|
||
state.audioContext.play();
|
||
state.isPlayingAudio = true;
|
||
} catch (e) {
|
||
state.isPlayingAudio = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 快捷动作条呼出处理
|
||
*/
|
||
export type PortalQuickAction =
|
||
| 'whatsapp'
|
||
| 'linkedin'
|
||
| 'email'
|
||
| 'phone'
|
||
| 'telegram'
|
||
| 'twitter'
|
||
| 'wechat'
|
||
| 'line'
|
||
| 'kakao';
|
||
|
||
/**
|
||
* Line / KakaoTalk 等外链入口:仅合法 http(s) URL 可渲染与触发
|
||
*/
|
||
export function isRenderableHttpUrl(value?: string | null): boolean {
|
||
if (value == null) {
|
||
return false;
|
||
}
|
||
const trimmed = value.trim();
|
||
if (trimmed === '') {
|
||
return false;
|
||
}
|
||
return /^https?:\/\//i.test(trimmed);
|
||
}
|
||
|
||
export function triggerQuickAction(
|
||
type: PortalQuickAction,
|
||
value?: string
|
||
): boolean {
|
||
if (!value || value.trim() === '') {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.contact_unset'),
|
||
icon: 'none',
|
||
});
|
||
return false;
|
||
}
|
||
|
||
if ((type === 'line' || type === 'kakao') && !isRenderableHttpUrl(value)) {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.contact_unset'),
|
||
icon: 'none',
|
||
});
|
||
return false;
|
||
}
|
||
|
||
if (type === 'phone') {
|
||
uni.makePhoneCall({
|
||
phoneNumber: value,
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'whatsapp') {
|
||
const cleanNumber = value.replace(/[^0-9+]/g, '');
|
||
const waUrl = `https://wa.me/${cleanNumber.replace('+', '')}`;
|
||
uni.setClipboardData({
|
||
data: waUrl,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_whatsapp'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'linkedin') {
|
||
uni.setClipboardData({
|
||
data: value,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_linkedin'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'email') {
|
||
uni.setClipboardData({
|
||
data: value,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_email'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'telegram') {
|
||
const raw = value.trim();
|
||
const handle = raw.replace(/^@+/, '');
|
||
const link = /https?:\/\//i.test(raw) ? raw : `https://t.me/${handle}`;
|
||
uni.setClipboardData({
|
||
data: link,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_telegram'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'twitter') {
|
||
uni.setClipboardData({
|
||
data: value,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_twitter'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'wechat') {
|
||
uni.setClipboardData({
|
||
data: value,
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_wechat'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'line') {
|
||
uni.setClipboardData({
|
||
data: value.trim(),
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_line'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
if (type === 'kakao') {
|
||
uni.setClipboardData({
|
||
data: value.trim(),
|
||
success: () => {
|
||
uni.showToast({
|
||
title: getOmniText(getCurrentLocale(), 'portal.copied_kakao'),
|
||
icon: 'none',
|
||
});
|
||
},
|
||
});
|
||
return true;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 根据 colSpan 计算 Bento Grid 列宽样式类
|
||
*/
|
||
export function getBentoColSpanClass(colSpan: number): string {
|
||
if (colSpan === 2) {
|
||
return 'bento-col-2';
|
||
}
|
||
return 'bento-col-1';
|
||
}
|
||
|
||
/**
|
||
* 过滤非 Hero 的 Bento 卡片并按 sortOrder 升序排列
|
||
*/
|
||
export function filterVisibleBentoBlocks(blocks?: BentoBlockPublic[]): BentoBlockPublic[] {
|
||
if (!blocks || !Array.isArray(blocks)) {
|
||
return [];
|
||
}
|
||
return blocks
|
||
.filter((b) => b.type !== 'hero')
|
||
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||
}
|
||
|
||
export type PortalCollectCopyKey = 'label' | 'saved';
|
||
|
||
/**
|
||
* 收藏大厅文案:只解析 locale,不改 load / save / 上报
|
||
*/
|
||
export function resolvePortalCollectText(
|
||
key: PortalCollectCopyKey,
|
||
locale?: OmniLocale
|
||
): string {
|
||
const loc = locale != null ? locale : getCurrentLocale();
|
||
if (key === 'saved') {
|
||
return getOmniText(loc, 'portal.collected');
|
||
}
|
||
return getOmniText(loc, 'portal.collect');
|
||
}
|
||
|
||
/**
|
||
* 预约动作雷达轨迹上报,自动推进 CRM 商机流转
|
||
*/
|
||
export async function trackAppointmentAction(
|
||
state: PortalState,
|
||
meetingSummary?: string
|
||
): Promise<boolean> {
|
||
if (state.card == null || state.card!.id == null) {
|
||
return false;
|
||
}
|
||
try {
|
||
await recordRadarTrack({
|
||
cardId: state.card!.id,
|
||
visitorId: state.visitorId,
|
||
touchType: 'appointment',
|
||
stayDurationMs: Math.max(0, Date.now() - state.enterTime),
|
||
scrollDepth: 100,
|
||
});
|
||
return true;
|
||
} catch (_e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
|
||
export const OWNER_CARD_IDS_STORAGE_KEY = 'omni_owner_card_ids';
|
||
|
||
/**
|
||
* 读取本地名片主人 ID 集合缓存
|
||
*/
|
||
export function getOwnerCardIds(): string[] {
|
||
try {
|
||
const raw = uni.getStorageSync(OWNER_CARD_IDS_STORAGE_KEY);
|
||
if (raw == null) {
|
||
return [];
|
||
}
|
||
if (Array.isArray(raw)) {
|
||
const list: string[] = [];
|
||
for (let i = 0; i < raw.length; i++) {
|
||
list.push(String(raw[i]));
|
||
}
|
||
return list;
|
||
}
|
||
if (typeof raw === 'string') {
|
||
const str = (raw as string).trim();
|
||
if (str.startsWith('[')) {
|
||
const parsed = JSON.parse(str);
|
||
if (Array.isArray(parsed)) {
|
||
const list: string[] = [];
|
||
for (let i = 0; i < parsed.length; i++) {
|
||
list.push(String(parsed[i]));
|
||
}
|
||
return list;
|
||
}
|
||
}
|
||
}
|
||
} catch (_e) {}
|
||
return [];
|
||
}
|
||
|
||
/**
|
||
* 缓存名片主人名下的全部卡片 ID 与 Slug
|
||
*/
|
||
export function setOwnerCardIds(ids: string[]): void {
|
||
try {
|
||
uni.setStorageSync(OWNER_CARD_IDS_STORAGE_KEY, ids);
|
||
} catch (_e) {}
|
||
}
|
||
|
||
export type CheckIsOwnerParams = {
|
||
hasToken: boolean;
|
||
queryId?: string;
|
||
queryMode?: string;
|
||
cardId?: string | number;
|
||
cardSlug?: string;
|
||
ownerCardIds?: string[];
|
||
};
|
||
|
||
/**
|
||
* 精准判定当前浏览者是否为该名片主人
|
||
* 1. 显式参数带 mode=owner -> true
|
||
* 2. 具备登录 Token 且:
|
||
* - queryId 为空或 'default'(TabBar 默认进入) -> true
|
||
* - 或 cardId / cardSlug 存在于名片主卡片 ID 缓存集合中 -> true
|
||
* 3. 其余外部访客场景 -> false
|
||
*/
|
||
export function checkIsCardOwner(params: CheckIsOwnerParams): boolean {
|
||
if (params.queryMode === 'owner') {
|
||
return true;
|
||
}
|
||
if (!params.hasToken) {
|
||
return false;
|
||
}
|
||
const qId = params.queryId != null ? params.queryId.trim().toLowerCase() : '';
|
||
if (qId === '' || qId === 'default') {
|
||
return true;
|
||
}
|
||
const currentId = params.cardId != null ? String(params.cardId).trim() : '';
|
||
const currentSlug = params.cardSlug != null ? String(params.cardSlug).trim() : '';
|
||
const list = params.ownerCardIds != null ? params.ownerCardIds : getOwnerCardIds();
|
||
for (let i = 0; i < list.length; i++) {
|
||
const item = String(list[i]).trim();
|
||
if (item !== '' && (item === currentId || item === currentSlug)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 动态解析主 CTA 按钮文案 (主人态: 分享名片 / 访客态: 保存到通讯录)
|
||
*/
|
||
export function resolvePortalCtaText(isOwner: boolean, locale?: OmniLocale): string {
|
||
const loc = locale != null ? locale : getCurrentLocale();
|
||
if (isOwner) {
|
||
return getOmniText(loc, 'card.share');
|
||
}
|
||
return getOmniText(loc, 'card.save_contact');
|
||
}
|
||
|
||
/**
|
||
* 动态解析微官网顶栏辅助操作文案 (主人态: 编辑 / 访客态: 交换名片)
|
||
*/
|
||
export function resolvePortalHeaderLinkText(isOwner: boolean, locale?: OmniLocale): string {
|
||
const loc = locale != null ? locale : getCurrentLocale();
|
||
if (isOwner) {
|
||
return getOmniText(loc, 'common.edit');
|
||
}
|
||
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 };
|
||
}
|
||
|
||
|