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

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

394 行
9.6 KiB
Plaintext

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

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

/**
* OmniCard AI (CRMmp) - 名片主参展分享码海报业务驱动层
* 解析 slug / id,拉取公开短链二维码,复制链接,保存海报到相册
*/
import { fetchCardQrcodeSvg } from '../../../common/api.uts';
import {
getCurrentLocale,
getOmniText,
OmniLocale,
} from '../../../common/locale.uts';
export type QrcodeQuery = {
slug?: string;
id?: string;
};
export type CardQrcodePoster = {
svg: string;
url: string;
};
export type LoadPosterApi = {
fetchCardQrcodeSvg: (slugOrId: string) => Promise<CardQrcodePoster>;
};
export type LoadPosterResult = {
svg: string;
url: string;
empty: boolean;
errorMessage: string;
};
export type QrcodeState = {
slugOrId: string;
svg: string;
url: string;
loading: boolean;
empty: boolean;
errorMessage: string;
};
export type QrcodeCopyKey =
| 'copyCta'
| 'generated'
| 'urlLabel'
| 'empty'
| 'error'
| 'loading'
| 'title'
| 'saveSuccess'
| 'saveFailed'
| 'saveUnavailable';
const QRCODE_COPY: Record<OmniLocale, Record<QrcodeCopyKey, string>> = {
'zh-CN': {
title: '分享名片',
copyCta: '复制链接',
generated: '二维码已生成',
urlLabel: '短链',
empty: '暂无数据',
error: '出错了',
loading: '加载中...',
saveSuccess: '已保存到相册',
saveFailed: '保存失败',
saveUnavailable: '暂无可保存的图片',
},
'en-US': {
title: 'Share Card',
copyCta: 'Copy link',
generated: 'QR code ready',
urlLabel: 'Short link',
empty: 'Nothing here yet',
error: 'Something went wrong',
loading: 'Loading...',
saveSuccess: 'Saved to Photos',
saveFailed: 'Could not save',
saveUnavailable: 'No image to save',
},
'ar-SA': {
title: 'مشاركة البطاقة',
copyCta: 'نسخ الرابط',
generated: 'رمز الاستجابة جاهز',
urlLabel: 'الرابط المختصر',
empty: 'لا توجد بيانات بعد',
error: 'حدث خطأ',
loading: 'جار التحميل...',
saveSuccess: 'تم الحفظ في الصور',
saveFailed: 'تعذر الحفظ',
saveUnavailable: 'لا توجد صورة للحفظ',
},
'es-ES': {
title: 'Compartir tarjeta',
copyCta: 'Copiar enlace',
generated: 'Codigo QR listo',
urlLabel: 'Enlace corto',
empty: 'Aun no hay datos',
error: 'Algo salio mal',
loading: 'Cargando...',
saveSuccess: 'Guardado en Fotos',
saveFailed: 'No se pudo guardar',
saveUnavailable: 'No hay imagen para guardar',
},
'de-DE': {
title: 'Karte teilen',
copyCta: 'Link kopieren',
generated: 'QR-Code bereit',
urlLabel: 'Kurzlink',
empty: 'Noch keine Daten',
error: 'Etwas ist schiefgelaufen',
loading: 'Wird geladen...',
saveSuccess: 'In Fotos gespeichert',
saveFailed: 'Speichern fehlgeschlagen',
saveUnavailable: 'Kein Bild zum Speichern',
},
'fr-FR': {
title: 'Partager la carte',
copyCta: 'Copier le lien',
generated: 'QR code pret',
urlLabel: 'Lien court',
empty: 'Aucune donnee',
error: 'Une erreur est survenue',
loading: 'Chargement...',
saveSuccess: 'Enregistre dans Photos',
saveFailed: 'Echec de l enregistrement',
saveUnavailable: 'Aucune image a enregistrer',
},
'ja-JP': {
title: '名刺をシェア',
copyCta: 'リンクをコピー',
generated: 'QRコードを生成しました',
urlLabel: 'ショートリンク',
empty: 'データがありません',
error: 'エラーが発生しました',
loading: '読み込み中...',
saveSuccess: '写真に保存しました',
saveFailed: '保存に失敗しました',
saveUnavailable: '保存できる画像がありません',
},
};
function trimText(value: string | number | null | undefined): string {
if (value == null) {
return '';
}
return String(value).trim();
}
/**
* 页面文案:能复用 locale.uts 的走词典,其余走本地七语表
*/
export function resolveQrcodeText(key: QrcodeCopyKey): string {
if (key === 'title') {
return getOmniText(getCurrentLocale(), 'card.share');
}
if (key === 'empty') {
return getOmniText(getCurrentLocale(), 'common.empty');
}
if (key === 'error') {
return getOmniText(getCurrentLocale(), 'common.error');
}
if (key === 'loading') {
return getOmniText(getCurrentLocale(), 'common.loading');
}
const locale = getCurrentLocale();
const bag = QRCODE_COPY[locale] != null ? QRCODE_COPY[locale] : QRCODE_COPY['en-US'];
return bag[key];
}
/**
* 路由查询优先 slug,否则 id
*/
export function resolveSlugOrId(query: QrcodeQuery | null | undefined): string {
if (query == null) {
return '';
}
const slug = trimText(query.slug);
if (slug !== '') {
return slug;
}
return trimText(query.id);
}
function resolvePosterApi(api?: LoadPosterApi): LoadPosterApi {
if (api != null) {
return api;
}
return {
fetchCardQrcodeSvg,
};
}
export function createQrcodeState(): QrcodeState {
return {
slugOrId: '',
svg: '',
url: '',
loading: false,
empty: false,
errorMessage: '',
};
}
/**
* 拉取公开 SVG 海报:可注入 fetchCardQrcodeSvg
*/
export async function loadPoster(
slugOrId: string | null | undefined,
api?: LoadPosterApi
): Promise<LoadPosterResult> {
const key = trimText(slugOrId);
if (key === '') {
return {
svg: '',
url: '',
empty: true,
errorMessage: '',
};
}
try {
const poster = await resolvePosterApi(api).fetchCardQrcodeSvg(key);
const svg = poster != null && poster.svg != null ? String(poster.svg) : '';
const url = poster != null && poster.url != null ? String(poster.url) : '';
return {
svg,
url,
empty: svg === '' && url === '',
errorMessage: '',
};
} catch (err) {
const message =
err instanceof Error
? err.message
: getOmniText(getCurrentLocale(), 'common.error');
return {
svg: '',
url: '',
empty: false,
errorMessage: message,
};
}
}
export async function loadPosterIntoState(
state: QrcodeState,
slugOrId: string | null | undefined,
api?: LoadPosterApi
): Promise<LoadPosterResult> {
state.slugOrId = trimText(slugOrId);
state.loading = true;
state.errorMessage = '';
const result = await loadPoster(state.slugOrId, api);
state.svg = result.svg;
state.url = result.url;
state.empty = result.empty;
state.errorMessage = result.errorMessage;
state.loading = false;
return result;
}
export function applyQrcodeOptions(
state: QrcodeState,
options: QrcodeQuery | null | undefined
): string {
const slugOrId = resolveSlugOrId(options);
state.slugOrId = slugOrId;
return slugOrId;
}
/**
* 主 CTA:复制短链
*/
export function copyPosterUrl(url: string | null | undefined): boolean {
const text = trimText(url);
if (text === '') {
return false;
}
uni.setClipboardData({
data: text,
success: () => {
uni.showToast({
title: getOmniText(getCurrentLocale(), 'portal.copied_link'),
icon: 'none',
});
},
});
return true;
}
/**
* 仅本地路径 / 网络图 / data:image 可入库;纯 SVG 标记不可直接进相册
*/
export function isSaveablePosterImage(source: string | null | undefined): boolean {
const text = trimText(source);
if (text === '') {
return false;
}
const lower = text.toLowerCase();
if (lower.indexOf('<svg') >= 0 || lower.indexOf('<?xml') === 0) {
return false;
}
if (
lower.indexOf('http://') === 0 ||
lower.indexOf('https://') === 0 ||
lower.indexOf('data:image') === 0
) {
return true;
}
return true;
}
function toastSaveFailed(unavailable: boolean): void {
if (unavailable) {
uni.showToast({
title: resolveQrcodeText('saveUnavailable'),
icon: 'none',
});
return;
}
uni.showToast({
title: resolveQrcodeText('saveFailed'),
icon: 'none',
});
}
function invokeSaveImageToPhotosAlbum(filePath: string): Promise<boolean> {
return new Promise<boolean>(resolve => {
uni.saveImageToPhotosAlbum({
filePath: filePath,
success: () => {
uni.showToast({
title: resolveQrcodeText('saveSuccess'),
icon: 'none',
});
resolve(true);
},
fail: (_err: unknown) => {
toastSaveFailed(false);
resolve(false);
},
});
});
}
function downloadRemoteImage(url: string): Promise<string> {
return new Promise<string>(resolve => {
uni.downloadFile({
url: url,
success: (res: { statusCode?: number; tempFilePath?: string }) => {
const path =
res != null && res.tempFilePath != null
? String(res.tempFilePath).trim()
: '';
const code = res != null && res.statusCode != null ? res.statusCode : 0;
if (code === 200 && path !== '') {
resolve(path);
return;
}
resolve('');
},
fail: (_err: unknown) => {
resolve('');
},
});
});
}
/**
* 保存海报到相册:必须等 uni.saveImageToPhotosAlbum 成功才 toast 已保存
*/
export async function savePosterToAlbum(
imageSource: string | null | undefined
): Promise<boolean> {
const source = trimText(imageSource);
if (source === '' || !isSaveablePosterImage(source)) {
toastSaveFailed(true);
return false;
}
let filePath = source;
const lower = source.toLowerCase();
if (lower.indexOf('http://') === 0 || lower.indexOf('https://') === 0) {
filePath = await downloadRemoteImage(source);
if (filePath === '') {
toastSaveFailed(false);
return false;
}
}
return invokeSaveImageToPhotosAlbum(filePath);
}