/** * OmniCard AI (CRMmp) - 名片主个人中心与消息通知配置业务驱动层 * 严格按照 AGENTS.md 规范与 uts 强类型约束 */ import { OmniLocale, PORTAL_LOCALE_STORAGE_KEY, getCurrentLocale, setAppLocale, getOmniText, } from '../../../common/locale.uts'; import { CardProfile, OwnerPageData, OwnerPageQuery, } from '../../../common/types.uts'; import { fetchOwnerCardPage } from '../../../common/api.uts'; export { PORTAL_LOCALE_STORAGE_KEY }; export type UserProfileAccount = { name: string; company: string; department: string; avatar: string; cardId: string; cardSlug: string; }; export type NotificationSettings = { radarPush: boolean; leadNotice: boolean; }; export type ProfileMetrics = { cardsCount: number; visitorsCount: number; leadsCount: number; }; export type ProfileState = { account: UserProfileAccount; notifications: NotificationSettings; metrics: ProfileMetrics; locale: OmniLocale; saving: boolean; loading: boolean; errorMessage: string; }; export type StorageApi = { getStorageSync: (key: string) => unknown; setStorageSync: (key: string, value: unknown) => void; }; export const PROFILE_ACCOUNT_STORAGE_KEY = 'omni_owner_profile_account'; export const PROFILE_NOTIFICATIONS_STORAGE_KEY = 'omni_owner_profile_notifications'; export const PROFILE_METRICS_STORAGE_KEY = 'omni_owner_profile_metrics'; function defaultStorage(): StorageApi { return { getStorageSync: (key: string): unknown => { try { if (typeof uni !== 'undefined' && uni.getStorageSync != null) { return uni.getStorageSync(key); } } catch (_e) {} return ''; }, setStorageSync: (key: string, value: unknown): void => { try { if (typeof uni !== 'undefined' && uni.setStorageSync != null) { uni.setStorageSync(key, value); } } catch (err: unknown) { throw err; } }, }; } export function getDefaultAccount(): UserProfileAccount { return { name: '', company: '', department: '', avatar: '', cardId: '', cardSlug: '', }; } export type LoadProfileAccountApi = { fetchOwnerCardPage: ( query: OwnerPageQuery ) => Promise>; }; /** * 读取接口可选组织字段:仅 trim,缺失或空白一律空串 */ export function readOptionalOrgText(value: string | null | undefined): string { if (value == null) { return ''; } return String(value).trim(); } /** * 公司/部门展示:有值原样返回,缺失或空串走 i18n 空态 */ export function displayOrgField(value: string | null | undefined): string { const text = readOptionalOrgText(value); if (text === '') { return resolveProfileText('empty'); } return text; } /** * 接口失败或无数据时清空公司与部门,禁止保留上次/示例值 */ export function clearOrgFields(state: ProfileState): void { state.account.company = ''; state.account.department = ''; } function readDepartmentFromCard(card: CardProfile): string { const extra = card as CardProfile & { department?: string | null }; return readOptionalOrgText(extra.department); } /** * 从名片分页接口拉取当前用户公司/部门(仅使用接口返回字段) */ export async function loadOwnerProfileAccount( state: ProfileState, api?: LoadProfileAccountApi ): Promise { const fetchPage = api != null ? api.fetchOwnerCardPage : fetchOwnerCardPage; state.loading = true; try { const data = await fetchPage({ page: 1, size: 1 } as OwnerPageQuery); const list = data.list != null ? data.list : []; if (list.length === 0) { clearOrgFields(state); state.account.cardId = ''; state.account.cardSlug = ''; state.errorMessage = ''; state.loading = false; return true; } const card = list[0]; state.account.company = readOptionalOrgText(card.company); state.account.department = readDepartmentFromCard(card); state.account.cardId = card.id != null ? String(card.id) : ''; const slugText = card.slug != null ? String(card.slug).trim() : ''; state.account.cardSlug = slugText; if (card.name != null && String(card.name).trim() !== '') { state.account.name = String(card.name).trim(); } if (card.avatarUrl != null && String(card.avatarUrl).trim() !== '') { state.account.avatar = String(card.avatarUrl).trim(); } state.errorMessage = ''; state.loading = false; return true; } catch (err: unknown) { clearOrgFields(state); state.account.cardId = ''; state.account.cardSlug = ''; const msg = err instanceof Error && err.message !== '' ? err.message : resolveProfileText('error'); state.errorMessage = msg; state.loading = false; return false; } } export function getDefaultNotifications(): NotificationSettings { return { radarPush: true, leadNotice: true, }; } export function getDefaultMetrics(): ProfileMetrics { return { cardsCount: 0, visitorsCount: 0, leadsCount: 0, }; } export function loadProfileMetrics(storage?: StorageApi): ProfileMetrics { const store = storage != null ? storage : defaultStorage(); try { const raw = store.getStorageSync(PROFILE_METRICS_STORAGE_KEY); const parsed = safeParseJson>(raw); if (parsed != null && typeof parsed === 'object') { const defaultM = getDefaultMetrics(); const cards = typeof parsed.cardsCount === 'number' ? parsed.cardsCount : defaultM.cardsCount; const visitors = typeof parsed.visitorsCount === 'number' ? parsed.visitorsCount : defaultM.visitorsCount; const leads = typeof parsed.leadsCount === 'number' ? parsed.leadsCount : defaultM.leadsCount; return { cardsCount: cards, visitorsCount: visitors, leadsCount: leads, }; } } catch (_e) {} return getDefaultMetrics(); } export function saveProfileMetrics(metrics: ProfileMetrics, storage?: StorageApi): boolean { const store = storage != null ? storage : defaultStorage(); try { store.setStorageSync(PROFILE_METRICS_STORAGE_KEY, JSON.stringify(metrics)); return true; } catch (_e) { return false; } } export function createProfileState(locale?: OmniLocale): ProfileState { const currentLocale = locale != null ? locale : getCurrentLocale(); return { account: getDefaultAccount(), notifications: getDefaultNotifications(), metrics: getDefaultMetrics(), locale: currentLocale, saving: false, loading: false, errorMessage: '', }; } function safeParseJson(raw: unknown): T | null { if (raw == null || typeof raw !== 'string') { return null; } const trimmed = raw.trim(); if (trimmed === '' || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) { return null; } try { return JSON.parse(trimmed) as T; } catch (_e) { return null; } } /** * 从本地存储加载个人名片主资料与通知设置,具备完整防御性容错 */ export function loadProfileSettings(storage?: StorageApi): ProfileState { const store = storage != null ? storage : defaultStorage(); const state = createProfileState(); try { const rawAccount = store.getStorageSync(PROFILE_ACCOUNT_STORAGE_KEY); const parsedAccount = safeParseJson>(rawAccount); if (parsedAccount != null && typeof parsedAccount === 'object') { const safeName = typeof parsedAccount.name === 'string' ? parsedAccount.name.trim() : ''; const safeCompany = typeof parsedAccount.company === 'string' ? parsedAccount.company.trim() : ''; const safeDept = typeof parsedAccount.department === 'string' ? parsedAccount.department.trim() : ''; const safeAvatar = typeof parsedAccount.avatar === 'string' ? parsedAccount.avatar.trim() : ''; const safeCardId = typeof parsedAccount.cardId === 'string' ? parsedAccount.cardId.trim() : ''; const safeCardSlug = typeof parsedAccount.cardSlug === 'string' ? parsedAccount.cardSlug.trim() : ''; state.account = { name: safeName, company: safeCompany, department: safeDept, avatar: safeAvatar, cardId: safeCardId, cardSlug: safeCardSlug, }; } } catch (_e) { // 降级使用默认账户 state.account = getDefaultAccount(); } try { const rawNotice = store.getStorageSync(PROFILE_NOTIFICATIONS_STORAGE_KEY); const parsedNotice = safeParseJson>(rawNotice); if (parsedNotice != null && typeof parsedNotice === 'object') { const defaultNotif = getDefaultNotifications(); state.notifications = { radarPush: typeof parsedNotice.radarPush === 'boolean' ? parsedNotice.radarPush : defaultNotif.radarPush, leadNotice: typeof parsedNotice.leadNotice === 'boolean' ? parsedNotice.leadNotice : defaultNotif.leadNotice, }; } } catch (_e) { // 降级使用默认通知 state.notifications = getDefaultNotifications(); } state.locale = getCurrentLocale(); state.metrics = loadProfileMetrics(store); return state; } /** * 保存名片主资料与通知设置到本地存储 */ export function saveProfileSettings(state: ProfileState, storage?: StorageApi): boolean { const store = storage != null ? storage : defaultStorage(); try { store.setStorageSync( PROFILE_ACCOUNT_STORAGE_KEY, JSON.stringify(state.account) ); store.setStorageSync( PROFILE_NOTIFICATIONS_STORAGE_KEY, JSON.stringify(state.notifications) ); saveProfileMetrics(state.metrics, store); state.errorMessage = ''; return true; } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Storage save failed'; state.errorMessage = msg; return false; } } /** * 更新账户资料字段,具备非空与空格修剪防御保护 */ export function updateAccount( state: ProfileState, patch: Partial ): void { if (patch == null) { return; } if (typeof patch.name === 'string' && patch.name.trim() !== '') { state.account.name = patch.name.trim(); } if (typeof patch.company === 'string' && patch.company.trim() !== '') { state.account.company = patch.company.trim(); } if (typeof patch.department === 'string' && patch.department.trim() !== '') { state.account.department = patch.department.trim(); } if (typeof patch.avatar === 'string' && patch.avatar.trim() !== '') { state.account.avatar = patch.avatar.trim(); } } /** * 切换雷达即时推送通知开关,并同步记忆持久化 */ export function toggleRadarPush( state: ProfileState, value?: boolean, storage?: StorageApi ): boolean { const store = storage != null ? storage : defaultStorage(); const nextVal = value !== undefined ? Boolean(value) : !state.notifications.radarPush; state.notifications.radarPush = nextVal; try { store.setStorageSync( PROFILE_NOTIFICATIONS_STORAGE_KEY, JSON.stringify(state.notifications) ); state.errorMessage = ''; } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Failed to persist radar push'; state.errorMessage = msg; } return nextVal; } /** * 切换新线索提醒开关,并同步记忆持久化 */ export function toggleLeadNotice( state: ProfileState, value?: boolean, storage?: StorageApi ): boolean { const store = storage != null ? storage : defaultStorage(); const nextVal = value !== undefined ? Boolean(value) : !state.notifications.leadNotice; state.notifications.leadNotice = nextVal; try { store.setStorageSync( PROFILE_NOTIFICATIONS_STORAGE_KEY, JSON.stringify(state.notifications) ); state.errorMessage = ''; } catch (err: unknown) { const msg = err instanceof Error ? err.message : 'Failed to persist lead notice'; state.errorMessage = msg; } return nextVal; } /** * 切换首选语言并记忆持久化 */ export function setPreferredLocale( state: ProfileState, locale: string, storage?: StorageApi ): OmniLocale { const safeLocales: OmniLocale[] = [ 'zh-CN', 'en-US', 'ar-SA', 'es-ES', 'de-DE', 'fr-FR', 'ja-JP', ]; let targetLocale: OmniLocale = 'zh-CN'; if (locale != null) { const found = safeLocales.find((l: OmniLocale) => l === locale); if (found != null) { targetLocale = found; } else { const lower = locale.toLowerCase(); if (lower.startsWith('en')) { targetLocale = 'en-US'; } } } state.locale = targetLocale; setAppLocale(targetLocale); if (storage != null) { try { storage.setStorageSync(PORTAL_LOCALE_STORAGE_KEY, targetLocale); } catch (_e) {} } return targetLocale; } /** * 预览个人头像 */ export function previewAccountAvatar(state: ProfileState): void { if (state.account.avatar !== '') { // #ifndef UNI-APP-X-TEST try { if (typeof uni !== 'undefined' && typeof uni.previewImage === 'function') { uni.previewImage({ urls: [state.account.avatar], current: state.account.avatar, }); } } catch (_e) {} // #endif } } /** * 移除个人头像 */ export function removeAccountAvatar(state: ProfileState): void { state.account.avatar = ''; } /** * 计算配额使用百分比 (0-100),防溢出与除零防御 */ export function calculateQuotaPercent(used: number, limit: number): number { if (limit <= 0) { return 100; } if (used <= 0) { return 0; } const pct = Math.round((used / limit) * 100); if (pct > 100) { return 100; } return pct; } /** * 瑞士国际主义多语言文案解析器,无 emoji */ export function resolveProfileText(key: string): string { const locale = getCurrentLocale(); if (key === 'save') { return getOmniText(locale, 'common.save'); } if (key === 'saved') { return getOmniText(locale, 'common.success'); } if (key === 'loading') { return getOmniText(locale, 'common.loading'); } if (key === 'error') { return getOmniText(locale, 'common.error'); } if (key === 'empty') { return getOmniText(locale, 'common.empty'); } const isZh = locale === 'zh-CN'; switch (key) { case 'title': return isZh ? '个人中心' : 'Profile'; case 'subtitle': return isZh ? '管理名片主信息与系统通知' : 'Manage account and notifications'; case 'accountSection': return isZh ? '基本资料' : 'Account'; case 'nameLabel': return isZh ? '姓名' : 'Name'; case 'companyLabel': return isZh ? '所属企业' : 'Company'; case 'departmentLabel': return isZh ? '部门' : 'Department'; case 'avatarLabel': return isZh ? '头像' : 'Avatar'; case 'tierPro': return isZh ? '商业专业版' : 'Pro Plan'; case 'tierBadge': return 'PRO'; case 'previewPortal': return isZh ? '预览微官网' : 'Preview site'; case 'metricsCards': return isZh ? '活跃名片' : 'Active Cards'; case 'metricsVisitors': return isZh ? '雷达访客' : 'Radar Visitors'; case 'metricsLeads': return isZh ? '沉淀线索' : 'Captured Leads'; case 'notificationsSection': return isZh ? '消息通知' : 'Notifications'; case 'radarPushTitle': return isZh ? '雷达即时推送' : 'Radar Push'; case 'radarPushDesc': return isZh ? '国际访客访问名片时立即通知' : 'Instant alert when visitors view card'; case 'leadNoticeTitle': return isZh ? '新线索提醒' : 'Lead Notice'; case 'leadNoticeDesc': return isZh ? '客商留资或互换名片时实时触达' : 'Real-time notice for new leads'; case 'languageSection': return isZh ? '偏好设置' : 'Preferences'; case 'languageLabel': return isZh ? '界面语言' : 'Display Language'; case 'logout': return isZh ? '退出登录' : 'Log out'; case 'workspaceSection': return isZh ? '设备与团队' : 'Hardware & Team'; case 'cardStudioSection': return isZh ? '名片内容管理' : 'Digital Card Studio'; case 'myCards': return isZh ? '我的数字名片' : 'Digital Cards'; case 'cardEdit': return isZh ? '名片编辑' : 'Edit Card'; case 'productCases': return isZh ? '产品相册与案例' : 'Portfolio & Cases'; case 'articleDynamics': return isZh ? '文章与商务动态' : 'Insights & News'; case 'sharePoster': return isZh ? '分享海报' : 'Share Poster'; case 'exchangeHall': return isZh ? '互换大厅' : 'Exchange Hall'; case 'editProfile': return isZh ? '编辑资料' : 'Edit'; case 'nfcSection': return isZh ? 'NFC 设备' : 'NFC Fleet'; case 'nfcScan': return isZh ? 'NFC 贴卡激活' : 'Activate NFC card'; case 'nfcMode': return isZh ? 'NFC 场景切换' : 'NFC scene mode'; case 'nfcSecurity': return isZh ? 'NFC 挂失' : 'Freeze NFC card'; case 'teamBriefing': return isZh ? '展会数据' : 'Team briefing'; case 'quotaSection': return isZh ? '套餐与配额' : 'Plan & Quota'; case 'quotaCards': return isZh ? '名片用量' : 'Card usage'; case 'quotaUpgrade': return isZh ? '已达上限,请升级套餐' : 'Limit reached. Upgrade to continue.'; case 'upgradeQuota': return isZh ? '升级扩容' : 'Upgrade Plan'; case 'planActive': return isZh ? '生效中' : 'Active'; case 'confirmLogoutTitle': return isZh ? '确认退出' : 'Sign Out'; case 'confirmLogoutContent': return isZh ? '确定要退出当前账号吗?' : 'Are you sure you want to sign out?'; case 'myCardsSub': return isZh ? '查看和切换名片' : 'View and switch cards'; case 'cardEditSub': return isZh ? '改姓名、职位和联系方式' : 'Name, role, and contact details'; case 'productCasesSub': return isZh ? '名片上展示的案例' : 'Cases shown on your card'; case 'articleDynamicsSub': return isZh ? '名片上展示的文章' : 'Articles shown on your card'; case 'sharePosterSub': return isZh ? '全屏出示二维码' : 'Full-screen QR'; case 'exchangeHallSub': return isZh ? '当面交换名片' : 'Exchange in person'; case 'nfcScanSub': return isZh ? '把实体卡绑到名片' : 'Bind a physical card'; case 'nfcModeSub': return isZh ? '更换这张卡打开的页面' : 'Choose what the card opens'; case 'nfcSecuritySub': return isZh ? '卡片丢失时远程停用' : 'Freeze a lost card'; case 'teamBriefingSub': return isZh ? '团队今天的获客' : "Today's team results"; case 'avatarActionSheetTitle': return isZh ? '头像管理' : 'Avatar Options'; case 'avatarActionPreview': return isZh ? '查看大图' : 'View Full Photo'; case 'avatarActionChoose': return isZh ? '从相册更换头像' : 'Change from Photos'; case 'avatarActionRemove': return isZh ? '移除当前头像' : 'Remove Avatar'; case 'aboutApp': return isZh ? '关于 OmniCard' : 'About OmniCard'; case 'aboutBody': return isZh ? '数字名片、微官网和访客雷达' : 'Digital cards, microsites, and visitor radar'; case 'confirm': return isZh ? '知道了' : 'OK'; case 'appVersion': return 'OmniCard'; case 'securitySafe': return isZh ? '保护中' : 'Protected'; case 'livePulse': return isZh ? '实时' : 'Live'; default: return key; } }