Files
EasyCard/frontend_miniprogram/miniprogram/pages/lawyer-detail/index.ts
陈子默 f39fcd05aa feat: 优化名片分享海报与详情交互
- 为律师详情页增加分享海报生成与分享封面图

- 新增律所简介入口并调整个人简介为页面整体滚动
2026-03-21 22:01:33 +08:00

399 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getFirmProfile, getLawyerDetail, recordCardShare } from '../../api/open';
import type { FirmInfo, Lawyer } from '../../types/card';
import { appendCardViewRecord } from '../../utils/history';
import { generateSharePoster, SHARE_POSTER_CANVAS_ID } from '../../utils/share-poster';
interface DetailPageData {
firm: FirmInfo;
lawyer: Lawyer | null;
notFound: boolean;
specialtiesText: string;
detailBgStyle: string;
loading: boolean;
sharePosterPath: string;
}
interface DetailPageCustom {
handleDockAction(event: WechatMiniprogram.CustomEvent<{ action: string }>): void;
saveContact(): void;
callPhone(): void;
navigateOffice(): void;
previewWechatQr(): void;
goFirmProfile(): void;
goLawyerList(): void;
getSharePosterCanvas(): Promise<WechatMiniprogram.Canvas>;
loadFirmForShare(): Promise<FirmInfo>;
prepareSharePoster(): void;
ensureSharePoster(): Promise<string>;
}
type DetailPageInstance = WechatMiniprogram.Page.Instance<DetailPageData, DetailPageCustom>;
const sharePosterPromiseMap = new WeakMap<DetailPageInstance, Promise<string>>();
const shareFirmPromiseMap = new WeakMap<DetailPageInstance, Promise<FirmInfo>>();
const shareCanvasPromiseMap = new WeakMap<DetailPageInstance, Promise<WechatMiniprogram.Canvas>>();
const pageReadyMap = new WeakMap<DetailPageInstance, boolean>();
function createEmptyFirm(): FirmInfo {
return {
id: '',
name: '',
logo: '',
intro: '',
hotlinePhone: '',
hqAddress: '',
hqLatitude: 0,
hqLongitude: 0,
officeCount: 0,
lawyerCount: 0,
heroImage: '',
officeList: [],
practiceAreas: [],
};
}
function buildDetailBgStyle(coverImage?: string): string {
const cover = typeof coverImage === 'string' ? coverImage.trim() : '';
if (!cover) {
return '';
}
return `background-image: url("${cover}"); background-size: cover; background-position: center;`;
}
Page<DetailPageData, DetailPageCustom>({
data: {
firm: createEmptyFirm(),
lawyer: null,
notFound: false,
specialtiesText: '',
detailBgStyle: '',
loading: false,
sharePosterPath: '',
},
async onLoad(options) {
const lawyerId = typeof options.id === 'string' ? options.id : '';
const sourceType = typeof options.sourceType === 'string' ? options.sourceType : 'DIRECT';
const shareFromCardId = typeof options.shareFromCardId === 'string' ? options.shareFromCardId : '';
if (!lawyerId) {
this.setData({ notFound: true, specialtiesText: '', detailBgStyle: '' });
return;
}
this.setData({ loading: true });
try {
const payload = await getLawyerDetail(lawyerId, sourceType, shareFromCardId);
this.setData({
firm: payload.firm,
lawyer: payload.lawyer,
notFound: false,
specialtiesText: payload.lawyer.specialties.join(''),
detailBgStyle: buildDetailBgStyle(payload.lawyer.coverImage),
});
appendCardViewRecord(payload.lawyer);
wx.showShareMenu({ menus: ['shareAppMessage'] });
this.prepareSharePoster();
} catch (error) {
const message = error instanceof Error ? error.message : '律师信息不存在';
this.setData({ notFound: true, specialtiesText: '', detailBgStyle: '' });
wx.showToast({
title: message,
icon: 'none',
});
} finally {
this.setData({ loading: false });
}
},
onReady() {
pageReadyMap.set(this, true);
this.prepareSharePoster();
},
onUnload() {
pageReadyMap.delete(this);
sharePosterPromiseMap.delete(this);
shareFirmPromiseMap.delete(this);
shareCanvasPromiseMap.delete(this);
},
handleDockAction(event) {
const action = event.detail.action;
switch (action) {
case 'home':
this.goFirmProfile();
break;
case 'save':
this.saveContact();
break;
case 'location':
this.navigateOffice();
break;
default:
break;
}
},
saveContact() {
const lawyer = this.data.lawyer;
if (!lawyer) {
return;
}
wx.addPhoneContact({
firstName: lawyer.name,
mobilePhoneNumber: lawyer.phone,
email: lawyer.email,
organization: this.data.firm.name,
title: lawyer.title,
workAddressStreet: lawyer.address,
remark: lawyer.specialties.join('、'),
success: () => {
wx.showToast({ title: '已添加到通讯录', icon: 'success' });
},
fail: () => {
wx.showToast({ title: '添加失败', icon: 'none' });
},
});
},
callPhone() {
const phone = this.data.lawyer ? this.data.lawyer.phone : '';
if (!phone) {
wx.showToast({ title: '暂无联系电话', icon: 'none' });
return;
}
wx.makePhoneCall({
phoneNumber: phone,
fail: () => {
wx.showToast({ title: '拨号失败', icon: 'none' });
},
});
},
navigateOffice() {
if (!this.data.firm.hqLatitude || !this.data.firm.hqLongitude) {
wx.showToast({ title: '暂未配置地图位置', icon: 'none' });
return;
}
wx.openLocation({
latitude: this.data.firm.hqLatitude,
longitude: this.data.firm.hqLongitude,
name: this.data.firm.name,
address: this.data.firm.hqAddress,
scale: 18,
fail: () => {
wx.showToast({ title: '打开地图失败', icon: 'none' });
},
});
},
previewWechatQr() {
const url = this.data.lawyer ? this.data.lawyer.wechatQrImage : '';
if (!url) {
wx.showToast({ title: '二维码未配置', icon: 'none' });
return;
}
wx.previewImage({
current: url,
urls: [url],
});
},
goFirmProfile() {
wx.navigateTo({ url: '/pages/firm/index' });
},
goLawyerList() {
wx.navigateTo({ url: '/pages/lawyer-list/index' });
},
async getSharePosterCanvas() {
const cachedPromise = shareCanvasPromiseMap.get(this);
if (cachedPromise) {
return cachedPromise;
}
const canvasPromise = new Promise<WechatMiniprogram.Canvas>((resolve, reject) => {
const query = wx.createSelectorQuery();
query
.in(this)
.select(`#${SHARE_POSTER_CANVAS_ID}`)
.node((result) => {
const canvas = result && 'node' in result ? (result.node as WechatMiniprogram.Canvas | undefined) : undefined;
if (canvas) {
resolve(canvas);
return;
}
reject(new Error('分享海报画布未就绪'));
})
.exec();
}).then(
(canvas) => {
shareCanvasPromiseMap.delete(this);
return canvas;
},
(error: unknown) => {
shareCanvasPromiseMap.delete(this);
throw error;
}
);
shareCanvasPromiseMap.set(this, canvasPromise);
return canvasPromise;
},
async loadFirmForShare() {
const cachedPromise = shareFirmPromiseMap.get(this);
if (cachedPromise) {
return cachedPromise;
}
const currentFirm = this.data.firm;
if (currentFirm.logo) {
return currentFirm;
}
const firmPromise = getFirmProfile()
.then((firmProfile) => ({
...currentFirm,
...firmProfile,
name: firmProfile.name || currentFirm.name,
hqAddress: currentFirm.hqAddress || firmProfile.hqAddress,
}))
.catch(() => currentFirm);
const firmPromiseWithCleanup = firmPromise.then(
(result) => {
shareFirmPromiseMap.delete(this);
return result;
},
(error: unknown) => {
shareFirmPromiseMap.delete(this);
throw error;
}
);
shareFirmPromiseMap.set(this, firmPromiseWithCleanup);
return firmPromiseWithCleanup;
},
prepareSharePoster() {
if (!pageReadyMap.get(this) || !this.data.lawyer || this.data.notFound) {
return;
}
this.ensureSharePoster().catch(() => {
// 海报生成失败时使用默认头像兜底
});
},
async ensureSharePoster() {
if (this.data.sharePosterPath) {
return this.data.sharePosterPath;
}
const currentPromise = sharePosterPromiseMap.get(this);
if (currentPromise) {
return currentPromise;
}
const lawyer = this.data.lawyer;
if (!lawyer) {
throw new Error('名片数据未就绪');
}
const posterPromise = this.loadFirmForShare()
.then((firm) =>
this.getSharePosterCanvas().then((canvas) =>
generateSharePoster({
canvas,
firm: {
name: firm.name,
logo: firm.logo,
hqAddress: firm.hqAddress,
},
lawyer: {
name: lawyer.name,
title: lawyer.title,
office: lawyer.office,
phone: lawyer.phone,
email: lawyer.email,
address: lawyer.address,
avatar: lawyer.avatar,
},
})
)
)
.then((posterPath) => {
if (posterPath) {
this.setData({ sharePosterPath: posterPath });
}
return posterPath;
});
const posterPromiseWithCleanup = posterPromise.then(
(result) => {
sharePosterPromiseMap.delete(this);
return result;
},
(error: unknown) => {
sharePosterPromiseMap.delete(this);
throw error;
}
);
sharePosterPromiseMap.set(this, posterPromiseWithCleanup);
return posterPromiseWithCleanup;
},
onShareAppMessage() {
const lawyer = this.data.lawyer;
const firmName = this.data.firm.name || '电子名片';
if (!lawyer) {
return {
title: firmName,
path: '/pages/firm/index',
};
}
const sharePath = `/pages/lawyer-detail/index?id=${lawyer.id}&sourceType=SHARE&shareFromCardId=${lawyer.id}`;
recordCardShare(lawyer.id, sharePath).catch(() => {
// 分享埋点失败不阻断分享动作
});
const title = firmName;
const fallbackImage = lawyer.avatar;
if (this.data.sharePosterPath) {
return {
title,
path: sharePath,
imageUrl: this.data.sharePosterPath,
};
}
const promise = this.ensureSharePoster()
.then((posterPath) => ({
title,
path: sharePath,
imageUrl: posterPath || fallbackImage,
}))
.catch(() => ({
title,
path: sharePath,
imageUrl: fallbackImage,
}));
return {
title,
path: sharePath,
imageUrl: fallbackImage,
promise,
};
},
});