文件
OmniCard/mpuniapp/pages/owner/card/useCardEdit.uts
wikiboss 62e0ae4435 feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 18:01:47 +08:00

864 行
20 KiB
Plaintext

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 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<CardProfile>;
};
export type LoadCardResult = {
card: CardProfile | null;
empty: boolean;
errorMessage: string;
};
export type SaveCardApi = {
updateOwnerCard?: (
payload: CardProfile & { id: string | number }
) => Promise<CardProfile>;
};
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<string, boolean> = {};
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<LoadCardResult> {
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<SaveCardResult> {
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<LoadCardResult> {
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<boolean> {
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',
});
},
});
}