feat(portal): 优化微官网首页名片主客态自适应与主CTA动态切换
这个提交包含在:
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../../common/types.uts';
|
||||
import { fetchOwnerCardPage } from '../../../common/api.uts';
|
||||
import { getCurrentLocale, getOmniText } from '../../../common/locale.uts';
|
||||
import { setOwnerCardIds } from '../../portal/usePortal.uts';
|
||||
|
||||
export type CardHomeItem = {
|
||||
id?: string | number;
|
||||
@@ -86,7 +87,7 @@ function isZhLocale(): boolean {
|
||||
*/
|
||||
export function resolveCardHomeText(key: CardHomeCopyKey): string {
|
||||
const locale = getCurrentLocale();
|
||||
if (key === 'poster' || key === 'share') {
|
||||
if (key === 'share') {
|
||||
return getOmniText(locale, 'card.share');
|
||||
}
|
||||
if (key === 'preview') {
|
||||
@@ -103,6 +104,9 @@ export function resolveCardHomeText(key: CardHomeCopyKey): string {
|
||||
}
|
||||
|
||||
const zh = isZhLocale();
|
||||
if (key === 'poster') {
|
||||
return zh ? '分享海报' : 'Share poster';
|
||||
}
|
||||
if (key === 'title') {
|
||||
return zh ? '我的名片' : 'My cards';
|
||||
}
|
||||
@@ -113,7 +117,7 @@ export function resolveCardHomeText(key: CardHomeCopyKey): string {
|
||||
return zh ? '搜索姓名、职位或公司' : 'Search name, title or company';
|
||||
}
|
||||
if (key === 'refresh') {
|
||||
return zh ? '分享名片' : 'Share card';
|
||||
return zh ? '刷新' : 'Refresh';
|
||||
}
|
||||
if (key === 'statusDraft') {
|
||||
return zh ? '草稿' : 'Draft';
|
||||
@@ -204,8 +208,19 @@ export async function loadCards(
|
||||
const data = await fetchPage(sanitizeCardQuery(query));
|
||||
const rawList = data.list != null ? data.list : [];
|
||||
const list: CardHomeItem[] = [];
|
||||
const ownerIds: string[] = [];
|
||||
for (let i = 0; i < rawList.length; i++) {
|
||||
list.push(toCardHomeItem(rawList[i]));
|
||||
const item = toCardHomeItem(rawList[i]);
|
||||
list.push(item);
|
||||
if (item.id != null) {
|
||||
ownerIds.push(String(item.id));
|
||||
}
|
||||
if (item.slug != null && item.slug !== '') {
|
||||
ownerIds.push(item.slug);
|
||||
}
|
||||
}
|
||||
if (ownerIds.length > 0) {
|
||||
setOwnerCardIds(ownerIds);
|
||||
}
|
||||
const pagination =
|
||||
data.pagination != null
|
||||
@@ -323,12 +338,12 @@ export function openCardQrcode(card: CardHomeItem): void {
|
||||
|
||||
export function buildPortalPreviewUrl(card: CardHomeItem): string {
|
||||
if (card.slug != null && card.slug !== '') {
|
||||
return `/pages/portal/index?slug=${card.slug}`;
|
||||
return `/pages/portal/index?slug=${card.slug}&mode=owner`;
|
||||
}
|
||||
if (card.id != null) {
|
||||
return `/pages/portal/index?id=${card.id}`;
|
||||
return `/pages/portal/index?id=${card.id}&mode=owner`;
|
||||
}
|
||||
return '/pages/portal/index';
|
||||
return '/pages/portal/index?mode=owner';
|
||||
}
|
||||
|
||||
export function openCardPreview(card: CardHomeItem): void {
|
||||
|
||||
文件差异内容过多而无法显示
加载差异
@@ -418,3 +418,126 @@ export function filterVisibleBentoBlocks(blocks?: BentoBlockPublic[]): BentoBloc
|
||||
.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');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
|
||||
const portalUtsPath = path.join(MP_ROOT, 'pages/portal/usePortal.uts');
|
||||
const portalUvuePath = path.join(MP_ROOT, 'pages/portal/index.uvue');
|
||||
const cardHomeUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardHome.uts');
|
||||
|
||||
test('1. usePortal.uts 文件必须存在且导出主客态核心函数', () => {
|
||||
assert.ok(fs.existsSync(portalUtsPath), '必须存在 usePortal.uts');
|
||||
const content = fs.readFileSync(portalUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('export function checkIsCardOwner'), '必须导出 checkIsCardOwner');
|
||||
assert.ok(content.includes('export function resolvePortalCtaText'), '必须导出 resolvePortalCtaText');
|
||||
assert.ok(content.includes('export function resolvePortalHeaderLinkText'), '必须导出 resolvePortalHeaderLinkText');
|
||||
assert.ok(content.includes('export function getOwnerCardIds'), '必须导出 getOwnerCardIds');
|
||||
assert.ok(content.includes('export function setOwnerCardIds'), '必须导出 setOwnerCardIds');
|
||||
});
|
||||
|
||||
test('2. 主客态精准判定算法逻辑正确性 (checkIsCardOwner)', () => {
|
||||
// 模拟纯逻辑实现契约测试
|
||||
type CheckParams = {
|
||||
hasToken: boolean;
|
||||
queryId?: string;
|
||||
queryMode?: string;
|
||||
cardId?: string | number;
|
||||
cardSlug?: string;
|
||||
ownerCardIds?: string[];
|
||||
};
|
||||
|
||||
function simulateCheckIsCardOwner(p: CheckParams): boolean {
|
||||
if (p.queryMode === 'owner') {
|
||||
return true;
|
||||
}
|
||||
if (!p.hasToken) {
|
||||
return false;
|
||||
}
|
||||
const qId = (p.queryId || '').trim().toLowerCase();
|
||||
if (qId === '' || qId === 'default') {
|
||||
return true;
|
||||
}
|
||||
const currentId = p.cardId != null ? String(p.cardId).trim() : '';
|
||||
const currentSlug = p.cardSlug != null ? String(p.cardSlug).trim() : '';
|
||||
const owners = p.ownerCardIds || [];
|
||||
for (let i = 0; i < owners.length; i++) {
|
||||
const oid = String(owners[i]).trim();
|
||||
if (oid !== '' && (oid === currentId || oid === currentSlug)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 场景 A:外部访客未登录访问任意名片 -> false
|
||||
assert.equal(
|
||||
simulateCheckIsCardOwner({ hasToken: false, queryId: '1001', cardId: 1001 }),
|
||||
false,
|
||||
'外部未登录访客必须为访客态'
|
||||
);
|
||||
|
||||
// 场景 B:已登录用户从 App TabBar 默认进入 ('default') -> true
|
||||
assert.equal(
|
||||
simulateCheckIsCardOwner({ hasToken: true, queryId: 'default', cardId: 1001 }),
|
||||
true,
|
||||
'已登录用户默认访问自身名片必须为主人态'
|
||||
);
|
||||
|
||||
// 场景 C:携带 mode=owner 参数预览 -> true
|
||||
assert.equal(
|
||||
simulateCheckIsCardOwner({ hasToken: false, queryMode: 'owner', cardId: 2002 }),
|
||||
true,
|
||||
'显式声明 mode=owner 必须为主人态'
|
||||
);
|
||||
|
||||
// 场景 D:已登录用户访问属于自己的名片 ID -> true
|
||||
assert.equal(
|
||||
simulateCheckIsCardOwner({
|
||||
hasToken: true,
|
||||
queryId: '1005',
|
||||
cardId: 1005,
|
||||
ownerCardIds: ['1001', '1005', 'alex-morgan'],
|
||||
}),
|
||||
true,
|
||||
'访问自身名片库中的 ID 必须为主人态'
|
||||
);
|
||||
|
||||
// 场景 E:已登录用户访问他人的名片 -> false
|
||||
assert.equal(
|
||||
simulateCheckIsCardOwner({
|
||||
hasToken: true,
|
||||
queryId: '9999',
|
||||
cardId: 9999,
|
||||
ownerCardIds: ['1001', '1005'],
|
||||
}),
|
||||
false,
|
||||
'已登录用户访问他人名片必须为访客态'
|
||||
);
|
||||
});
|
||||
|
||||
test('3. index.uvue 视图层主客态自适应规范审查', () => {
|
||||
assert.ok(fs.existsSync(portalUvuePath), '必须存在 index.uvue');
|
||||
const content = fs.readFileSync(portalUvuePath, 'utf-8');
|
||||
|
||||
// 必须引入 checkIsCardOwner
|
||||
assert.ok(
|
||||
content.includes('checkIsCardOwner'),
|
||||
'index.uvue 必须调用 checkIsCardOwner'
|
||||
);
|
||||
|
||||
// 必须具备 isOwner 响应式状态或计算属性
|
||||
assert.ok(
|
||||
content.includes('isOwner'),
|
||||
'index.uvue 必须具备 isOwner 判断'
|
||||
);
|
||||
|
||||
// 主 CTA 在主人态下调用分享/海报逻辑,访客态调用 saveToContacts
|
||||
assert.ok(
|
||||
content.includes('handlePrimaryCta') || content.includes('handleSaveContact'),
|
||||
'index.uvue 必须包含主 CTA 统一处理函数'
|
||||
);
|
||||
|
||||
// 主人态必须可直接导航至二维码/海报页
|
||||
assert.ok(
|
||||
content.includes('/pages/owner/card/qrcode'),
|
||||
'index.uvue 主人态必须支持跳转至海报/展台页'
|
||||
);
|
||||
|
||||
// 主人态顶栏链接必须支持直接编辑微官网
|
||||
assert.ok(
|
||||
content.includes('/pages/owner/card/edit'),
|
||||
'index.uvue 主人态顶栏必须支持跳转至编辑微官网页'
|
||||
);
|
||||
|
||||
// 右上角收入大厅必须在主人态下受控隐藏
|
||||
assert.ok(
|
||||
content.includes('canCollect') || content.includes('!isOwner'),
|
||||
'index.uvue 右上角收入大厅必须在主人态下隐藏'
|
||||
);
|
||||
});
|
||||
|
||||
test('4. useCardHome.uts 必须在名片加载时持久化名片主人 ID 集合', () => {
|
||||
assert.ok(fs.existsSync(cardHomeUtsPath), '必须存在 useCardHome.uts');
|
||||
const content = fs.readFileSync(cardHomeUtsPath, 'utf-8');
|
||||
assert.ok(
|
||||
content.includes('setOwnerCardIds') || content.includes('omni_owner_card_ids'),
|
||||
'useCardHome.uts 必须持久化名片主人 ID 集合'
|
||||
);
|
||||
});
|
||||
|
||||
test('5. 多语言文案解析与主客态映射审查', () => {
|
||||
const portalContent = fs.readFileSync(portalUtsPath, 'utf-8');
|
||||
assert.ok(
|
||||
portalContent.includes("getOmniText(loc, 'card.share')"),
|
||||
'主人态主 CTA 必须映射 card.share 词条'
|
||||
);
|
||||
assert.ok(
|
||||
portalContent.includes("getOmniText(loc, 'card.save_contact')"),
|
||||
'访客态主 CTA 必须映射 card.save_contact 词条'
|
||||
);
|
||||
assert.ok(
|
||||
portalContent.includes("getOmniText(loc, 'common.edit')"),
|
||||
'主人态顶栏操作必须映射 common.edit 词条'
|
||||
);
|
||||
assert.ok(
|
||||
portalContent.includes("getOmniText(loc, 'card.exchange')"),
|
||||
'访客态顶栏操作必须映射 card.exchange 词条'
|
||||
);
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户