399 lines
10 KiB
TypeScript
399 lines
10 KiB
TypeScript
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,
|
||
};
|
||
},
|
||
});
|