import { PAPER_SIZE } from '../constants/colors'; import { downloadPrint } from '../utils/downloadPrint'; import { BaseDrawService } from '../core/draw/baseDraw'; import tracker from '../utils/tracker'; import { defaultShareConfig } from '../config/config'; import { buildWorksheetPreviewCloudPath, isDebugPublishEnabled, type DebugPublishConfirmDetail, type DebugPublishMeta, } from '../utils/debugPublish'; /** * Canvas 相关的页面实例属性 */ export interface PageCanvasInstance { data: CanvasDataState; canvas: Canvas | null; ctx: RenderingContext | null; boxHeight: number; boxWidth: number; drawService: BaseDrawService | null; setData(data: any, callback?: () => void): void; route: string; getShareOptions(): ShareOptions; selectComponent(selector: string): any; getPublishMeta?(): DebugPublishMeta; } /** * Canvas 数据状态接口 */ export interface CanvasDataState { hasContent: boolean; showShareDialog: boolean; boxWidth: number; boxHeight: number; functionId: string; currentMode?: string; pageTitle: string; subTitle?: string; data: any; setData(data: any, callback?: () => void): void; route: string; isDevEnv?: boolean; debugPublishVisible?: boolean; debugPublishLoading?: boolean; debugPublishMeta?: DebugPublishMeta | null; } /** * Canvas 初始化选项 */ export interface InitCanvasOptions { /** * 创建绘制服务的工厂函数 */ createDrawService: ( canvas: Canvas, ctx: RenderingContext, options?: Record, ) => BaseDrawService; /** * 绘制服务的选项 */ drawServiceOptions?: Record; /** * Canvas 初始化完成后回调 */ onCanvasReady?: () => void; } /** * 分享配置选项 */ export interface ShareOptions { /** * 页面路径(相对于 mathPages 目录,例如:'missingNumber/missingNumber') */ path: string; title: string; imageUrl: string; } /** * 页面信息接口 */ export interface PageInfo { title: string; desc?: string; } /** * 页面信息查找函数类型 */ export type PageInfoLookup = (functionId: string) => PageInfo | undefined; /** * 页面公共方法配置 */ export interface PageCommonMethodsConfig { /** * 分享配置 */ shareConfig?: { title: string; imageUrl: string; }; /** * 页面信息查找函数(可选) * 如果提供,initPageInfo 方法会使用它来查找页面信息 */ pageInfoLookup?: PageInfoLookup; } /** * 获取页面公共方法 * 这些方法可以在所有Canvas绘制页面中复用 * @param config 配置选项 */ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { const { shareConfig = defaultShareConfig, pageInfoLookup } = config; return { /** * 初始化 Canvas */ initCanvas(this: PageCanvasInstance, options: InitCanvasOptions) { const query = wx.createSelectorQuery(); query .select('#canvasWrapper') .boundingClientRect((rect) => { if (!rect) return; const { width, height } = PAPER_SIZE['A4']; const boxWidth = rect.width; const boxHeight = boxWidth / (width / height); this.boxHeight = boxHeight; this.boxWidth = boxWidth; this.setData({ boxWidth, boxHeight }); const canvas = wx .createSelectorQuery() .select('#canvasContent'); canvas.fields({ node: true, size: true }).exec((res) => { if (res[0]) { const canvasNode = res[0].node; const ctx = canvasNode.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; canvasNode.width = boxWidth * dpr; canvasNode.height = boxHeight * dpr; ctx.scale(dpr, dpr); this.canvas = canvasNode; this.ctx = ctx; // 创建绘制服务 const drawServiceOptions = { title: this.data.pageTitle, subTitle: this.data.subTitle || '', ...options.drawServiceOptions, }; this.drawService = options.createDrawService( canvasNode, ctx, drawServiceOptions, ); // 执行初始化完成回调 if (options.onCanvasReady) { options.onCanvasReady.call(this); } } }); }) .exec(); }, /** * 从 preview-card 组件的 canvas-ready 事件初始化 Canvas * 用于已将 canvas 节点下沉到 preview-card 组件的页面 */ initCanvasFromComponent( this: PageCanvasInstance, detail: { canvas: Canvas; ctx: RenderingContext; width: number; height: number; }, options: InitCanvasOptions, ) { this.canvas = detail.canvas; this.ctx = detail.ctx; this.boxWidth = detail.width; this.boxHeight = detail.height; this.setData({ boxWidth: detail.width, boxHeight: detail.height, }); const drawServiceOptions = { title: this.data.pageTitle, subTitle: this.data.subTitle || '', ...options.drawServiceOptions, }; this.drawService = options.createDrawService( detail.canvas, detail.ctx, drawServiceOptions, ); if (options.onCanvasReady) { options.onCanvasReady.call(this); } }, /** * 导出打印 */ async exportToPrint(this: PageCanvasInstance) { await downloadPrint(this.canvas, { errorToast: '请先生成内容', trackerName: this.data.pageTitle, trackerMode: this.data.currentMode, }); }, getShareOptions(this: PageCanvasInstance): ShareOptions { const route = this.route || ''; const { functionId, currentMode } = this.data; const currentModeQuery = currentMode ? `&mode=${currentMode}` : ''; const path = `${route}?id=${functionId}${currentModeQuery}`; console.log('getShareOptions path', path); return { ...shareConfig, path, }; }, /** * 分享小程序 */ onShareAppMessage(this: PageCanvasInstance) { // 上报分享埋点 tracker.reportShare(this.data.pageTitle); return this.getShareOptions(); }, /** * 分享到朋友圈 */ onShareTimeline(this: PageCanvasInstance) { console.log('onShareTimeline'); // 上报分享埋点 tracker.reportShare(this.data.pageTitle); return this.getShareOptions(); }, /** * 关闭分享引导弹窗 */ onCloseShareDialog(this: PageCanvasInstance) { this.setData({ showShareDialog: false }); }, /** * 分享成功回调 */ async onShareSuccess(this: PageCanvasInstance) { this.setData({ showShareDialog: false }); if (this.canvas) { // 分享成功后下载(不限制次数) await downloadPrint(this.canvas, { errorToast: '请先生成内容', trackerName: this.data.pageTitle, trackerMode: this.data.currentMode, }); } }, syncDebugPublishEnv(this: PageCanvasInstance) { this.setData({ isDevEnv: isDebugPublishEnabled() }); }, onOpenDebugPublish(this: PageCanvasInstance) { if (!isDebugPublishEnabled()) return; if (typeof this.getPublishMeta !== 'function') { wx.showToast({ title: '页面未实现发布元数据', icon: 'none', }); return; } try { const meta = this.getPublishMeta(); this.setData({ isDevEnv: true, debugPublishVisible: true, debugPublishMeta: meta, }); } catch (error) { wx.showToast({ title: error instanceof Error ? error.message : '准备发布数据失败', icon: 'none', }); } }, onCloseDebugPublish(this: PageCanvasInstance) { this.setData({ debugPublishVisible: false }); }, /** * 确认发布 */ async onConfirmDebugPublish( this: PageCanvasInstance, e: WechatMiniprogram.CustomEvent, ) { const previewCard = this.selectComponent('#previewCard'); const debugPublishTools = this.selectComponent('#debugPublishTools'); const baseMeta = this.data.debugPublishMeta || (typeof this.getPublishMeta === 'function' ? this.getPublishMeta() : null); if (!previewCard?.exportToTempFile) { wx.showToast({ title: '预览组件不可用', icon: 'none', }); return; } if (!debugPublishTools?.processImage) { wx.showToast({ title: '发布工具不可用', icon: 'none', }); return; } if (!baseMeta) { wx.showToast({ title: '缺少发布元数据', icon: 'none', }); return; } const detail = e.detail; const publishMeta: DebugPublishMeta = { ...baseMeta, title: detail.meta.title, subtitle: detail.meta.subtitle, tags: detail.meta.tags, status: detail.meta.status, }; this.setData({ debugPublishLoading: true }); wx.showLoading({ title: '发布中...' }); try { const sourcePath = await previewCard.exportToTempFile({ fileType: 'jpg', quality: 1, }); console.log('sourcePath', sourcePath); const processed = await debugPublishTools.processImage({ sourcePath, settings: detail.settings, }); console.log('processed', processed); const cloudPath = buildWorksheetPreviewCloudPath( publishMeta.category, publishMeta.id, ); console.log('cloudPath', cloudPath); const uploadRes = await wx.cloud.uploadFile({ cloudPath, filePath: processed.tempFilePath, }); console.log('uploadRes', uploadRes); const cloudCall = (await wx.cloud.callFunction({ name: 'worksheetsPublish', data: { ...publishMeta, previewImg: uploadRes.fileID, }, })) as { result?: { success?: boolean; message?: string }; }; console.log('cloudCall', cloudCall); if (!cloudCall.result?.success) { throw new Error( cloudCall.result?.message || '云端写入失败', ); } console.log('success'); this.setData({ debugPublishVisible: false, debugPublishMeta: { ...publishMeta, previewImg: uploadRes.fileID, }, }); wx.showToast({ title: `发布成功 ${Math.round(processed.size / 1024)}KB`, icon: 'success', }); } catch (error) { console.log('onConfirmDebugPublish error', error); wx.showToast({ title: error instanceof Error ? error.message : '发布失败', icon: 'none', }); } finally { wx.hideLoading(); this.setData({ debugPublishLoading: false }); } }, /** * 初始化页面信息 * * 支持两种调用方式: * 1. initPageInfo(functionId, defaultTitle) - 如果有 pageInfoLookup,会尝试查找;否则使用 defaultTitle * 2. initPageInfo({ title, desc, functionId }) - 直接提供页面信息 */ initPageInfo( this: PageCanvasInstance, functionIdOrOptions: | string | { title: string; desc?: string; functionId: string }, defaultTitle?: string, ) { let functionId: string; let title: string; let desc: string = ''; // 判断调用方式 if (typeof functionIdOrOptions === 'string') { // 方式1: initPageInfo(functionId, defaultTitle) functionId = functionIdOrOptions; title = defaultTitle || ''; // 如果有 pageInfoLookup,尝试查找 if (pageInfoLookup) { const pageInfo = pageInfoLookup(functionId); if (pageInfo) { title = pageInfo.title; desc = pageInfo.desc || ''; } } } else { // 方式2: initPageInfo({ title, desc, functionId }) functionId = functionIdOrOptions.functionId; title = functionIdOrOptions.title; desc = functionIdOrOptions.desc || ''; } this.setData({ pageTitle: title, subTitle: desc, functionId, }); wx.setNavigationBarTitle({ title }); }, }; } /** * 应用页面公共方法的辅助函数 * 自动混入公共方法,简化页面代码 * @param pageOptions 页面选项 * @param config 公共方法配置 * @returns 合并后的页面选项 */ export function applyPageMixin( pageOptions: any, config: PageCommonMethodsConfig = {}, ) { const commonMethods = getPageCommonMethods(config); // 提取 pageOptions 中的 data(如果有) const pageData = pageOptions.data || {}; // 合并页面选项和公共方法 // 注意:pageOptions 放在后面,这样页面可以覆盖公共方法 const mergedOptions: any = { ...commonMethods, ...pageOptions, // 处理 data 的合并(需要特殊处理,避免覆盖) data: { ...pageData, }, }; return mergedOptions; } /** * 创建页面的便捷函数 * 自动应用公共方法并注册为页面 * @param pageOptions 页面选项 * @param config 公共方法配置 */ export function createPage( pageOptions: any, config: PageCommonMethodsConfig = {}, ) { Page(applyPageMixin(pageOptions, config)); }