/** * OmniCard AI (CRMmp) - 名片主极速编辑业务驱动层 * 拉名片详情,仅提交已填字段,不做 Bento 搭建 */ import { CardProfile } from '../../../common/types.uts'; import { fetchOwnerCardInfo, updateOwnerCard } from '../../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../../common/locale.uts'; export { fetchOwnerCardInfo, updateOwnerCard }; export type CardEditCopyKey = | 'heading' | 'name' | 'jobTitle' | 'company' | 'slogan' | 'phone' | 'email' | 'whatsapp' | 'linkedin' | 'timezone' | 'save' | 'loading' | 'success' | 'error' | 'empty' | 'missingId' | 'back' | 'avatarUpload' | 'avatarChange' | 'avatarPlaceholder' | 'lockedHint' | 'moduleOrderSection' | 'moveUp' | 'moveDown'; /** 极速编辑页可重排模块(无服务端排序字段,仅客户端顺序) */ export type CardEditModuleKey = 'basic' | 'social' | 'privacy'; export const DEFAULT_MODULE_ORDER: CardEditModuleKey[] = [ 'basic', 'social', 'privacy', ]; const MODULE_ORDER_STORAGE_PREFIX = 'omni_card_module_order_'; /** 品牌套件可锁定字段(接口 lockedFields / whitelist 原样或别名) */ export type BrandLockFieldKey = | 'company' | 'companyName' | 'logo' | 'logoUrl' | 'primaryColor' | 'avatarUrl' | 'name' | 'title' | 'slogan' | 'phone' | 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'twitter' | 'wechat'; export type BrandKitLockSource = { logoUrl?: string; primaryColor?: string; lockedFields?: string[] | null; whitelist?: string[] | null; editableFields?: string[] | null; fieldWhitelist?: string[] | null; }; export type CardEditForm = { id?: string | number | null; name?: string; title?: string; company?: string; slogan?: string; avatarUrl?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; timezone?: string; lockedFields?: string[] | null; }; export type CardUpdatePayload = { id: string | number; name?: string; title?: string; company?: string; slogan?: string; avatarUrl?: string; phone?: string; email?: string; whatsapp?: string; linkedin?: string; telegram?: string; twitter?: string; wechat?: string; timezone?: string; }; export type LoadCardApi = { fetchOwnerCardInfo?: (id: string | number) => Promise; }; export type LoadCardResult = { card: CardProfile | null; empty: boolean; errorMessage: string; }; export type SaveCardApi = { updateOwnerCard?: ( payload: CardProfile & { id: string | number } ) => Promise; }; export type SaveCardResult = { ok: boolean; code?: number; card: CardProfile | null; errorMessage: string; }; export type CardEditPageOptions = { id?: string; }; export type CardEditState = { rawId: string; cardId: number | null; name: string; title: string; company: string; slogan: string; avatarUrl: string; phone: string; email: string; whatsapp: string; linkedin: string; timezone: string; moduleOrder: string[]; lockedFields: string[]; loading: boolean; saving: boolean; empty: boolean; missingId: boolean; errorMessage: string; }; function isZhLocale(): boolean { return getCurrentLocale() === 'zh-CN'; } /** * 页面文案:保存 / 社交走词典,字段标签走本地词典 */ export function resolveCardEditText(key: CardEditCopyKey): string { const locale = getCurrentLocale(); if (key === 'save') { return getOmniText(locale, 'common.save'); } if (key === 'loading') { return getOmniText(locale, 'common.loading'); } if (key === 'success') { return getOmniText(locale, 'common.success'); } if (key === 'error') { return getOmniText(locale, 'common.error'); } if (key === 'empty') { return getOmniText(locale, 'common.empty'); } if (key === 'whatsapp') { return getOmniText(locale, 'portal.whatsapp'); } if (key === 'linkedin') { return getOmniText(locale, 'portal.linkedin'); } const zh = isZhLocale(); if (key === 'heading') { return zh ? '极速编辑' : 'Quick edit'; } if (key === 'name') { return zh ? '姓名' : 'Name'; } if (key === 'jobTitle') { return zh ? '头衔' : 'Title'; } if (key === 'company') { return zh ? '公司' : 'Company'; } if (key === 'slogan') { return zh ? '口号' : 'Slogan'; } if (key === 'phone') { return zh ? '电话' : 'Phone'; } if (key === 'email') { return zh ? '邮箱' : 'Email'; } if (key === 'timezone') { return zh ? '时区' : 'Timezone'; } if (key === 'missingId') { return zh ? '缺少名片编号' : 'Card ID is missing'; } if (key === 'back') { return zh ? '返回名片' : 'Back to card'; } if (key === 'avatarUpload') { return zh ? '点击拍照/上传' : 'Upload photo'; } if (key === 'avatarChange') { return zh ? '更换头像' : 'Change avatar'; } if (key === 'avatarPlaceholder') { return zh ? '上传头像' : 'Upload avatar'; } if (key === 'lockedHint') { return zh ? '品牌规范已锁定,不可修改' : 'Locked by brand kit'; } if (key === 'moduleOrderSection') { return zh ? '模块顺序' : 'Module order'; } if (key === 'moveUp') { return zh ? '上移' : 'Move up'; } if (key === 'moveDown') { return zh ? '下移' : 'Move down'; } return ''; } function isKnownModuleKey(value: string): boolean { return ( value === 'basic' || value === 'social' || value === 'privacy' ); } /** * 归一化模块顺序:去重、仅保留已知键,缺项按默认补齐 */ export function normalizeModuleOrder(raw: unknown): string[] { const seen: Record = {}; const out: string[] = []; if (Array.isArray(raw)) { for (let i = 0; i < raw.length; i++) { const item = raw[i]; if (typeof item !== 'string') { continue; } const key = item.trim(); if (!isKnownModuleKey(key) || seen[key] === true) { continue; } seen[key] = true; out.push(key); } } for (let i = 0; i < DEFAULT_MODULE_ORDER.length; i++) { const key = DEFAULT_MODULE_ORDER[i]; if (seen[key] !== true) { out.push(key); } } return out; } /** * 上下移动模块。越界时原样返回。 */ export function moveModuleInOrder( order: string[], index: number, delta: number ): string[] { const normalized = normalizeModuleOrder(order); const target = index + delta; if (index < 0 || index >= normalized.length) { return normalized; } if (target < 0 || target >= normalized.length) { return normalized; } const next: string[] = []; for (let i = 0; i < normalized.length; i++) { next.push(normalized[i]); } const tmp = next[index]; next[index] = next[target]; next[target] = tmp; return next; } export function moduleOrderStorageKey(cardId: string | number): string { return MODULE_ORDER_STORAGE_PREFIX + String(cardId); } export function readStoredModuleOrder( cardId: string | number | null | undefined ): string[] { if (cardId == null) { return normalizeModuleOrder(null); } try { if (typeof uni === 'undefined' || uni.getStorageSync == null) { return normalizeModuleOrder(null); } const cached = uni.getStorageSync(moduleOrderStorageKey(cardId)); return normalizeModuleOrder(cached); } catch (_e) { return normalizeModuleOrder(null); } } export function writeStoredModuleOrder( cardId: string | number | null | undefined, order: string[] ): void { if (cardId == null) { return; } try { if (typeof uni === 'undefined' || uni.setStorageSync == null) { return; } uni.setStorageSync( moduleOrderStorageKey(cardId), normalizeModuleOrder(order) ); } catch (_e) {} } /** * 解析为正整数。非正整数返回 null */ export function parseCardId( raw: string | number | null | undefined ): number | null { if (raw == null) { return null; } if (typeof raw === 'number') { if (raw <= 0) { return null; } if (Math.floor(raw) !== raw) { return null; } return raw; } const text = String(raw).trim(); if (text === '') { return null; } if (!/^[1-9]\d*$/.test(text)) { return null; } return Number(text); } function pickFilled(value: string | null | undefined): string | null { if (value == null) { return null; } const text = String(value).trim(); if (text === '') { return null; } return text; } /** 品牌管控字段:whitelist 模式下未列入则锁定 */ const BRAND_CONTROL_FIELDS: string[] = ['company', 'logoUrl', 'primaryColor']; function normalizeLockToken(raw: string): string { return String(raw).trim(); } function fieldAliasGroup(field: string): string[] { const key = normalizeLockToken(field); if (key === 'company' || key === 'companyName') { return ['company', 'companyName']; } if (key === 'logo' || key === 'logoUrl') { return ['logo', 'logoUrl']; } return [key]; } function listIncludesField( list: string[] | null | undefined, field: string ): boolean { if (list == null || list.length === 0) { return false; } const aliases = fieldAliasGroup(field); for (let i = 0; i < list.length; i++) { const item = normalizeLockToken(list[i]); if (item === '') { continue; } for (let j = 0; j < aliases.length; j++) { if (item === aliases[j]) { return true; } } } return false; } function readStringArray(value: unknown): string[] | null { if (value == null) { return null; } if (!Array.isArray(value)) { return null; } const out: string[] = []; for (let i = 0; i < value.length; i++) { const item = value[i]; if (typeof item === 'string') { const text = item.trim(); if (text !== '') { out.push(text); } } } return out; } function readWhitelist(kit: BrandKitLockSource | null | undefined): string[] | null { if (kit == null) { return null; } const fromWhitelist = readStringArray(kit.whitelist); if (fromWhitelist != null) { return fromWhitelist; } const fromEditable = readStringArray(kit.editableFields); if (fromEditable != null) { return fromEditable; } return readStringArray(kit.fieldWhitelist); } function deriveLocksFromWhitelist(whitelist: string[]): string[] { const locked: string[] = []; for (let i = 0; i < BRAND_CONTROL_FIELDS.length; i++) { const field = BRAND_CONTROL_FIELDS[i]; if (!listIncludesField(whitelist, field)) { locked.push(field); } } return locked; } /** * 从名片或品牌套件提取锁定字段。 * 优先 lockedFields;否则用 whitelist/editableFields 反推品牌管控字段。 * 无锁定数据返回 [],不因仅有 brandKit 资产而锁全部。 */ export function extractLockedFields( source: | (CardProfile & { lockedFields?: string[] | null; brandKit?: BrandKitLockSource | null; }) | BrandKitLockSource | null | undefined ): string[] { if (source == null) { return []; } const asRecord = source as { lockedFields?: string[] | null; brandKit?: BrandKitLockSource | null; whitelist?: string[] | null; editableFields?: string[] | null; fieldWhitelist?: string[] | null; }; const topLocked = readStringArray(asRecord.lockedFields); if (topLocked != null) { return topLocked; } const kit = asRecord.brandKit != null ? asRecord.brandKit : (source as BrandKitLockSource); if (kit != null) { const kitLocked = readStringArray(kit.lockedFields); if (kitLocked != null) { return kitLocked; } const whitelist = readWhitelist(kit); if (whitelist != null && whitelist.length > 0) { return deriveLocksFromWhitelist(whitelist); } } return []; } /** * 判断表单字段是否被品牌套件锁定(含 company/companyName、logo/logoUrl 别名) */ export function isFieldLocked( lockedFields: string[] | null | undefined, field: string ): boolean { return listIncludesField(lockedFields, field); } function assignFilled( payload: CardUpdatePayload, key: | 'name' | 'title' | 'company' | 'slogan' | 'avatarUrl' | 'phone' | 'email' | 'whatsapp' | 'linkedin' | 'telegram' | 'twitter' | 'wechat' | 'timezone', value: string | null | undefined, lockedFields?: string[] | null ): void { if (isFieldLocked(lockedFields, key)) { return; } const filled = pickFilled(value); if (filled != null) { payload[key] = filled; } } /** * 只带已填字段与解析后的 id。缺 id 返回 null。 * 锁定字段(含 companyName→company 别名)不进入 payload。 */ export function buildUpdatePayload( form: CardEditForm, lockedFields?: string[] | null ): CardUpdatePayload | null { const id = parseCardId(form.id); if (id == null) { return null; } const locks = lockedFields != null ? lockedFields : form.lockedFields != null ? form.lockedFields : []; const payload: CardUpdatePayload = { id }; assignFilled(payload, 'name', form.name, locks); assignFilled(payload, 'title', form.title, locks); assignFilled(payload, 'company', form.company, locks); assignFilled(payload, 'slogan', form.slogan, locks); assignFilled(payload, 'avatarUrl', form.avatarUrl, locks); assignFilled(payload, 'phone', form.phone, locks); assignFilled(payload, 'email', form.email, locks); assignFilled(payload, 'whatsapp', form.whatsapp, locks); assignFilled(payload, 'linkedin', form.linkedin, locks); assignFilled(payload, 'telegram', form.telegram, locks); assignFilled(payload, 'twitter', form.twitter, locks); assignFilled(payload, 'wechat', form.wechat, locks); assignFilled(payload, 'timezone', form.timezone, locks); return payload; } function readErrorMessage(err: unknown): string { if (err instanceof Error && err.message !== '') { return err.message; } return resolveCardEditText('error'); } function asUpdatePayload( payload: CardUpdatePayload ): CardProfile & { id: string | number } { return payload as CardProfile & { id: string | number }; } /** * 拉取名片详情。可注入 fetchOwnerCardInfo。缺 id 不请求 */ export async function loadCard( id: string | number | null | undefined, api?: LoadCardApi ): Promise { const cardId = parseCardId(id); if (cardId == null) { return { card: null, empty: true, errorMessage: '', }; } const fetchInfo = api != null && api.fetchOwnerCardInfo != null ? api.fetchOwnerCardInfo : fetchOwnerCardInfo; try { const card = await fetchInfo(cardId); return { card, empty: false, errorMessage: '', }; } catch (err) { return { card: null, empty: true, errorMessage: readErrorMessage(err), }; } } /** * 保存名片。可注入 updateOwnerCard。缺 id 不请求。 * lockedFields 可显式传入,否则用 form.lockedFields。 */ export async function saveCard( form: CardEditForm, api?: SaveCardApi, lockedFields?: string[] | null ): Promise { const payload = buildUpdatePayload(form, lockedFields); if (payload == null) { return { ok: false, code: undefined, card: null, errorMessage: '', }; } const update = api != null && api.updateOwnerCard != null ? api.updateOwnerCard : updateOwnerCard; try { const card = await update(asUpdatePayload(payload)); return { ok: true, // Cool-Admin:仅 code === 1000 视为更新成功(http 层已解包) code: 1000, card, errorMessage: '', }; } catch (err) { return { ok: false, code: undefined, card: null, errorMessage: readErrorMessage(err), }; } } export function createCardEditState(): CardEditState { return { rawId: '', cardId: null, name: '', title: '', company: '', slogan: '', avatarUrl: '', phone: '', email: '', whatsapp: '', linkedin: '', timezone: '', moduleOrder: normalizeModuleOrder(null), lockedFields: [], loading: false, saving: false, empty: false, missingId: false, errorMessage: '', }; } export function applyCardEditOptions( state: CardEditState, options: CardEditPageOptions | null ): boolean { if (options == null || options.id == null) { state.rawId = ''; state.cardId = null; state.missingId = true; state.empty = true; return false; } const parsed = parseCardId(options.id); state.rawId = String(options.id).trim(); state.cardId = parsed; state.missingId = parsed == null; state.empty = parsed == null; if (parsed != null) { state.moduleOrder = readStoredModuleOrder(parsed); } return parsed != null; } function readOptionalText(value: string | null | undefined): string { if (value == null) { return ''; } return String(value); } function applyCardToState(state: CardEditState, card: CardProfile): void { if (card.id != null) { const parsed = parseCardId(card.id); if (parsed != null) { state.cardId = parsed; } } state.name = card.name != null ? String(card.name) : ''; state.title = readOptionalText(card.title); state.company = readOptionalText(card.company); state.slogan = readOptionalText(card.slogan); state.avatarUrl = readOptionalText(card.avatarUrl); state.phone = readOptionalText(card.phone); state.email = readOptionalText(card.email); state.whatsapp = readOptionalText(card.whatsapp); state.linkedin = readOptionalText(card.linkedin); state.timezone = readOptionalText(card.timezone); state.lockedFields = extractLockedFields( card as CardProfile & { lockedFields?: string[] | null; brandKit?: BrandKitLockSource | null; } ); } export function applyLoadResult( state: CardEditState, result: LoadCardResult ): void { if (result.card != null) { applyCardToState(state, result.card); state.empty = false; state.missingId = false; } else { state.empty = true; state.lockedFields = []; } state.errorMessage = result.errorMessage; } export function formFromState(state: CardEditState): CardEditForm { return { id: state.cardId, name: state.name, title: state.title, company: state.company, slogan: state.slogan, avatarUrl: state.avatarUrl, phone: state.phone, email: state.email, whatsapp: state.whatsapp, linkedin: state.linkedin, timezone: state.timezone, lockedFields: state.lockedFields, }; } export async function refreshCardEdit( state: CardEditState, api?: LoadCardApi ): Promise { if (state.missingId || state.cardId == null) { const skipped: LoadCardResult = { card: null, empty: true, errorMessage: '', }; applyLoadResult(state, skipped); state.missingId = true; return skipped; } if (state.loading) { return { card: null, empty: state.empty, errorMessage: state.errorMessage, }; } state.loading = true; state.errorMessage = ''; const result = await loadCard(state.cardId, api); applyLoadResult(state, result); state.loading = false; return result; } export async function submitCardEdit( state: CardEditState, api?: SaveCardApi ): Promise { if (state.cardId == null || state.saving) { return false; } state.saving = true; state.errorMessage = ''; const result = await saveCard( formFromState(state), api, state.lockedFields ); // 仅接口 code === 1000 视为保存成功 if (result.ok && result.code === 1000 && result.card != null) { applyCardToState(state, result.card); writeStoredModuleOrder(state.cardId, state.moduleOrder); state.saving = false; return true; } state.saving = false; state.errorMessage = result.errorMessage; return false; } export function goBackFromCardEdit(): void { uni.navigateBack({ delta: 1, fail: () => { uni.switchTab({ url: '/pages/owner/card/index', }); }, }); }