640 lines
19 KiB
TypeScript
640 lines
19 KiB
TypeScript
import { PAPER_SIZE } from '../constants/colors';
|
|
import { downloadPrint, tryUnlockWithRewardedAd } from '../utils/downloadPrint';
|
|
import { shouldShowShareGuide, shouldUnlockWithAd } from '../utils/shareGuide';
|
|
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;
|
|
getWorksheetStatsId?(): string;
|
|
}
|
|
|
|
/**
|
|
* 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<string, any>,
|
|
) => BaseDrawService;
|
|
/**
|
|
* 绘制服务的选项
|
|
*/
|
|
drawServiceOptions?: Record<string, any>;
|
|
/**
|
|
* Canvas 初始化完成后回调
|
|
*/
|
|
onCanvasReady?: () => void;
|
|
}
|
|
|
|
/**
|
|
* 分享配置选项(文案/图统一,path 为当前页)
|
|
*/
|
|
export interface ShareOptions {
|
|
path: string;
|
|
title: string;
|
|
imageUrl: string;
|
|
}
|
|
|
|
type SharePathData = {
|
|
functionId?: string;
|
|
worksheetId?: string;
|
|
currentMode?: string;
|
|
};
|
|
|
|
type SharePathPage = {
|
|
route?: string;
|
|
data?: SharePathData;
|
|
};
|
|
|
|
/** 禁止右上角转发/分享到朋友圈的页面 route(不含开头 /) */
|
|
export const SHARE_DISABLED_ROUTES = new Set([
|
|
'pages/favorites/favorites',
|
|
'pages/profile/profile',
|
|
'supportPages/settings/settings',
|
|
]);
|
|
|
|
/** 关闭当前页分享菜单(收藏、我的、设置等) */
|
|
export function disablePageShareMenu() {
|
|
wx.hideShareMenu({
|
|
menus: ['shareAppMessage', 'shareTimeline'],
|
|
});
|
|
}
|
|
|
|
/** 根据页面 route 与 data 生成分享 path */
|
|
export function buildPageSharePath(page?: SharePathPage): string {
|
|
const route = page?.route || '';
|
|
if (!route) {
|
|
return defaultShareConfig.path;
|
|
}
|
|
|
|
const normalized = route.startsWith('/') ? route : `/${route}`;
|
|
const data = page?.data || {};
|
|
const functionId = (data.functionId || data.worksheetId) as
|
|
| string
|
|
| undefined;
|
|
const currentMode = data.currentMode as string | undefined;
|
|
|
|
if (!functionId) {
|
|
return normalized;
|
|
}
|
|
|
|
const modeQuery =
|
|
currentMode && currentMode !== functionId
|
|
? `&mode=${currentMode}`
|
|
: '';
|
|
return `${normalized}?id=${functionId}${modeQuery}`;
|
|
}
|
|
|
|
/** 朋友圈分享 query(对应当前页 path 的查询串) */
|
|
export function buildPageShareTimelineQuery(page?: SharePathPage): string {
|
|
const path = buildPageSharePath(page);
|
|
const qIndex = path.indexOf('?');
|
|
return qIndex >= 0 ? path.slice(qIndex + 1) : '';
|
|
}
|
|
|
|
/** 全站统一分享文案与图;path 默认当前页 */
|
|
export function getAppShareAppMessage(
|
|
trackerLabel?: string,
|
|
page?: SharePathPage,
|
|
) {
|
|
if (trackerLabel) {
|
|
tracker.reportShare(trackerLabel);
|
|
}
|
|
return {
|
|
title: defaultShareConfig.title,
|
|
path: buildPageSharePath(page),
|
|
imageUrl: defaultShareConfig.imageUrl,
|
|
};
|
|
}
|
|
|
|
/** 全站统一朋友圈分享;query 对应当前页 */
|
|
export function getAppShareTimeline(
|
|
trackerLabel?: string,
|
|
page?: SharePathPage,
|
|
) {
|
|
if (trackerLabel) {
|
|
tracker.reportShare(trackerLabel);
|
|
}
|
|
return {
|
|
title: defaultShareConfig.title,
|
|
query: buildPageShareTimelineQuery(page),
|
|
imageUrl: defaultShareConfig.imageUrl,
|
|
};
|
|
}
|
|
|
|
/** 供 Page({}) 直接展开:统一转发 / 分享到朋友圈(path 为当前页) */
|
|
export const appSharePageMethods = {
|
|
onShareAppMessage(this: SharePathPage) {
|
|
return getAppShareAppMessage(undefined, this);
|
|
},
|
|
onShareTimeline(this: SharePathPage) {
|
|
return getAppShareTimeline(undefined, this);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 页面信息接口
|
|
*/
|
|
export interface PageInfo {
|
|
title: string;
|
|
desc?: string;
|
|
}
|
|
|
|
/**
|
|
* 页面信息查找函数类型
|
|
*/
|
|
export type PageInfoLookup = (functionId: string) => PageInfo | undefined;
|
|
|
|
/**
|
|
* 页面公共方法配置
|
|
*/
|
|
export interface PageCommonMethodsConfig {
|
|
/**
|
|
* 分享配置
|
|
*/
|
|
/** 可选;默认使用 config 中的 defaultShareConfig(统一首页 path / 文案 / 分享图) */
|
|
shareConfig?: {
|
|
title: string;
|
|
path: 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) {
|
|
const worksheetId =
|
|
typeof this.getWorksheetStatsId === 'function'
|
|
? this.getWorksheetStatsId()
|
|
: this.data.functionId;
|
|
const downloadOptions = {
|
|
errorToast: '请先生成内容',
|
|
trackerName: this.data.pageTitle,
|
|
trackerMode: this.data.currentMode,
|
|
worksheetId,
|
|
};
|
|
|
|
// 第 2 次及以后满批:直接拉激励广告
|
|
if (shouldUnlockWithAd()) {
|
|
const unlocked = await tryUnlockWithRewardedAd();
|
|
if (!unlocked) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 当日首次满批:仅分享解锁,弹窗引导
|
|
if (shouldShowShareGuide()) {
|
|
this.setData({ showShareDialog: true });
|
|
return;
|
|
}
|
|
|
|
await downloadPrint(this.canvas, downloadOptions);
|
|
},
|
|
|
|
getShareOptions(this: PageCanvasInstance): ShareOptions {
|
|
return {
|
|
title: shareConfig.title,
|
|
path: buildPageSharePath(this),
|
|
imageUrl: shareConfig.imageUrl,
|
|
};
|
|
},
|
|
|
|
/**
|
|
* 分享小程序(统一文案与分享图,path 为当前页)
|
|
*/
|
|
onShareAppMessage(this: PageCanvasInstance) {
|
|
return getAppShareAppMessage(this.data.pageTitle, this);
|
|
},
|
|
|
|
/**
|
|
* 分享到朋友圈(统一文案与分享图,query 为当前页参数)
|
|
*/
|
|
onShareTimeline(this: PageCanvasInstance) {
|
|
return getAppShareTimeline(this.data.pageTitle, this);
|
|
},
|
|
|
|
/**
|
|
* 关闭分享引导弹窗
|
|
*/
|
|
onCloseShareDialog(this: PageCanvasInstance) {
|
|
this.setData({ showShareDialog: false });
|
|
},
|
|
|
|
/**
|
|
* 分享成功回调
|
|
*/
|
|
async onShareSuccess(this: PageCanvasInstance) {
|
|
this.setData({ showShareDialog: false });
|
|
if (this.canvas) {
|
|
const worksheetId =
|
|
typeof this.getWorksheetStatsId === 'function'
|
|
? this.getWorksheetStatsId()
|
|
: this.data.functionId;
|
|
await downloadPrint(this.canvas, {
|
|
errorToast: '请先生成内容',
|
|
trackerName: this.data.pageTitle,
|
|
trackerMode: this.data.currentMode,
|
|
worksheetId,
|
|
});
|
|
}
|
|
},
|
|
|
|
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<DebugPublishConfirmDetail>,
|
|
) {
|
|
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,
|
|
});
|
|
|
|
const processed = await debugPublishTools.processImage({
|
|
sourcePath,
|
|
settings: detail.settings,
|
|
});
|
|
|
|
const cloudPath = buildWorksheetPreviewCloudPath(
|
|
publishMeta.category,
|
|
publishMeta.id,
|
|
);
|
|
const uploadRes = await wx.cloud.uploadFile({
|
|
cloudPath,
|
|
filePath: processed.tempFilePath,
|
|
});
|
|
|
|
const cloudCall = (await wx.cloud.callFunction({
|
|
name: 'worksheetsPublish',
|
|
data: {
|
|
...publishMeta,
|
|
previewImg: uploadRes.fileID,
|
|
},
|
|
})) as {
|
|
result?: { success?: boolean; message?: string };
|
|
};
|
|
if (!cloudCall.result?.success) {
|
|
throw new Error(
|
|
cloudCall.result?.message || '云端写入失败',
|
|
);
|
|
}
|
|
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));
|
|
}
|