feat: 优化字帖功能
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
import { PAPER_SIZE } from '../constants/colors';
|
import { PAPER_SIZE } from '../constants/colors';
|
||||||
import { checkAndSaveImage } from '../utils/saveImage';
|
import { downloadPrint } from '../utils/downloadPrint';
|
||||||
import { shouldShowShareGuide } from '../utils/shareGuide';
|
|
||||||
import { BaseDrawService } from '../service/baseDraw';
|
import { BaseDrawService } from '../service/baseDraw';
|
||||||
import tracker from '../utils/tracker';
|
import tracker from '../utils/tracker';
|
||||||
import { defaultShareConfig } from '../config/config';
|
import { defaultShareConfig } from '../config/config';
|
||||||
@@ -172,20 +171,12 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
|||||||
/**
|
/**
|
||||||
* 导出打印
|
* 导出打印
|
||||||
*/
|
*/
|
||||||
exportToPrint(this: PageCanvasInstance) {
|
async exportToPrint(this: PageCanvasInstance) {
|
||||||
if (!this.canvas || !this.data.hasContent) {
|
await downloadPrint(this.canvas, {
|
||||||
return;
|
errorToast: '请先生成内容',
|
||||||
}
|
trackerName: this.data.pageTitle,
|
||||||
|
trackerMode: this.data.currentMode,
|
||||||
if (shouldShowShareGuide()) {
|
});
|
||||||
this.setData({ showShareDialog: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上报下载埋点
|
|
||||||
tracker.reportDownload(this.data.pageTitle, this.data.currentMode);
|
|
||||||
|
|
||||||
checkAndSaveImage(this.canvas);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getShareOptions(this: PageCanvasInstance): ShareOptions {
|
getShareOptions(this: PageCanvasInstance): ShareOptions {
|
||||||
@@ -231,12 +222,15 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
|||||||
/**
|
/**
|
||||||
* 分享成功回调
|
* 分享成功回调
|
||||||
*/
|
*/
|
||||||
onShareSuccess(this: PageCanvasInstance) {
|
async onShareSuccess(this: PageCanvasInstance) {
|
||||||
this.setData({ showShareDialog: false });
|
this.setData({ showShareDialog: false });
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
// 上报下载埋点(分享成功后下载)
|
// 分享成功后下载(不限制次数)
|
||||||
tracker.reportDownload(this.data.pageTitle);
|
await downloadPrint(this.canvas, {
|
||||||
checkAndSaveImage(this.canvas);
|
errorToast: '请先生成内容',
|
||||||
|
trackerName: this.data.pageTitle,
|
||||||
|
trackerMode: this.data.currentMode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,27 @@ Component({
|
|||||||
type: String,
|
type: String,
|
||||||
value: '请输入文字',
|
value: '请输入文字',
|
||||||
},
|
},
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
value: '', // 输入框的值(支持外部控制)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 组件的初始数据
|
* 组件的初始数据
|
||||||
*/
|
*/
|
||||||
data: {
|
data: {
|
||||||
|
innerValue: '', // 内部维护的输入值
|
||||||
|
},
|
||||||
|
observers: {
|
||||||
|
// 监听外部传入的 value 变化
|
||||||
|
value(newVal: string) {
|
||||||
|
this.setData({ innerValue: newVal });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
lifetimes: {
|
lifetimes: {
|
||||||
attached() {
|
attached() {
|
||||||
(this as any).value = '';
|
// 初始化时同步外部传入的 value
|
||||||
|
this.setData({ innerValue: this.properties.value || '' });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -32,17 +44,14 @@ Component({
|
|||||||
onInput(e: WechatMiniprogram.Input) {
|
onInput(e: WechatMiniprogram.Input) {
|
||||||
let { value } = e.detail;
|
let { value } = e.detail;
|
||||||
value = value.replace(/\s/g, '');
|
value = value.replace(/\s/g, '');
|
||||||
if (value.length > 0) {
|
this.setData({ innerValue: value });
|
||||||
(this as any).value = value;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onConfirm() {
|
onConfirm() {
|
||||||
const value = (this as any).value;
|
const value = this.data.innerValue;
|
||||||
|
|
||||||
if (value.length > 0) {
|
// 触发确认事件(即使值为空也触发,让父组件处理清空逻辑)
|
||||||
this.triggerEvent('onConfirm', { value });
|
this.triggerEvent('onConfirm', { value });
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// onTap() {
|
// onTap() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
<input
|
<input
|
||||||
class="word-input"
|
class="word-input"
|
||||||
type="text"
|
type="text"
|
||||||
|
value="{{innerValue}}"
|
||||||
placeholder="{{placeholder}}"
|
placeholder="{{placeholder}}"
|
||||||
bindinput="onInput"
|
bindinput="onInput"
|
||||||
bindconfirm="onConfirm" />
|
bindconfirm="onConfirm" />
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { SHAPES, ShapeCard } from '../../constants/shapes';
|
import { SHAPES, ShapeCard } from '../../constants/shapes';
|
||||||
import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors';
|
import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors';
|
||||||
import ShapeDrawService from '../../service/shapeDrawService';
|
import ShapeDrawService from '../../service/shapeDrawService';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { downloadPrint } from '../../utils/downloadPrint';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
|
||||||
import tracker from '../../utils/tracker';
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
canvas: null as Canvas | null,
|
canvas: null as Canvas | null,
|
||||||
@@ -286,22 +284,15 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** 下载打印 */
|
/** 下载打印 */
|
||||||
exportToPrint() {
|
async exportToPrint() {
|
||||||
if (!this.canvas || this.data.shapeList.length <= 0) {
|
if (!this.canvas || this.data.shapeList.length <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要显示分享引导
|
await downloadPrint(this.canvas, {
|
||||||
if (shouldShowShareGuide()) {
|
errorToast: '请先生成图形',
|
||||||
this.setData({ showShareDialog: true });
|
trackerName: '图形涂色',
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 上报下载埋点
|
|
||||||
tracker.reportDownload('图形涂色');
|
|
||||||
|
|
||||||
// 直接下载
|
|
||||||
checkAndSaveImage(this.canvas);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 关闭分享引导弹窗 */
|
/** 关闭分享引导弹窗 */
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import WordDrawService from '../../service/wordDrawService';
|
import WordDrawService from '../../service/wordDrawService';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { downloadPrint } from '../../utils/downloadPrint';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
|
||||||
import { PAPER_SIZE } from '../../constants/colors';
|
import { PAPER_SIZE } from '../../constants/colors';
|
||||||
import { getWordsSvgData } from '../../utils/getWordsSvgJson';
|
import { getWordsSvgData } from '../../utils/getWordsSvgJson';
|
||||||
import { WORDS } from '../../constants/words';
|
import { WORDS } from '../../constants/words';
|
||||||
@@ -108,6 +107,7 @@ Page({
|
|||||||
{
|
{
|
||||||
inputWords: [],
|
inputWords: [],
|
||||||
words: updatedWords,
|
words: updatedWords,
|
||||||
|
inputValue: '', // 同步清空输入框值
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.renderPracticeContent().catch(console.error);
|
this.renderPracticeContent().catch(console.error);
|
||||||
@@ -143,11 +143,12 @@ Page({
|
|||||||
// 3. 替换输入汉字(不是追加),合并已选择的汉字
|
// 3. 替换输入汉字(不是追加),合并已选择的汉字
|
||||||
const updatedWords = [...newInputChars, ...selectedWords];
|
const updatedWords = [...newInputChars, ...selectedWords];
|
||||||
|
|
||||||
// 4. 更新数据并提示
|
// 4. 更新数据并提示(同步更新 inputValue 保持一致)
|
||||||
this.setData(
|
this.setData(
|
||||||
{
|
{
|
||||||
inputWords: newInputChars,
|
inputWords: newInputChars,
|
||||||
words: updatedWords,
|
words: updatedWords,
|
||||||
|
inputValue: text, // 同步输入框值
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.renderPracticeContent().catch(console.error);
|
this.renderPracticeContent().catch(console.error);
|
||||||
@@ -436,32 +437,12 @@ Page({
|
|||||||
return characterData;
|
return characterData;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 记录待下载状态(当需要分享引导时) */
|
|
||||||
pendingDownload: false,
|
|
||||||
|
|
||||||
// 下载打印练字贴
|
// 下载打印练字贴
|
||||||
exportToPrint() {
|
async exportToPrint() {
|
||||||
if (!this.canvas) {
|
await downloadPrint(this.canvas, {
|
||||||
wx.showToast({ title: '请先生成练字贴', icon: 'none' });
|
errorToast: '请先生成练字贴',
|
||||||
return;
|
trackerName: '练字贴',
|
||||||
}
|
});
|
||||||
|
|
||||||
// 检查是否需要显示分享引导
|
|
||||||
if (shouldShowShareGuide()) {
|
|
||||||
this.setData({ showShareDialog: true });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 上报下载埋点
|
|
||||||
tracker.reportDownload('练字贴');
|
|
||||||
|
|
||||||
// 直接下载
|
|
||||||
checkAndSaveImage(this.canvas);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 关闭分享引导弹窗 */
|
|
||||||
onCloseShareDialog() {
|
|
||||||
this.setData({ showShareDialog: false });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 分享功能
|
// 分享功能
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<view class="wrapper">
|
<view class="wrapper">
|
||||||
<text class="wrapper-title">自定义练字贴</text>
|
<text class="wrapper-title">自定义练字贴</text>
|
||||||
<view class="word-input-box">
|
<view class="word-input-box">
|
||||||
<word-input bind:onConfirm="onConfirmInput" style="width: 100%;"/>
|
<word-input value="{{inputValue}}" bind:onConfirm="onConfirmInput" style="width: 100%;"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="action-buttons-box">
|
<view class="action-buttons-box">
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
IDrawService,
|
IDrawService,
|
||||||
TemplateType,
|
TemplateType,
|
||||||
} from '../../service/drawServiceFactory';
|
} from '../../service/drawServiceFactory';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { downloadPrint } from '../../utils/downloadPrint';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
|
||||||
import tracker from '../../utils/tracker';
|
import tracker from '../../utils/tracker';
|
||||||
import { defaultShareConfig } from '../../config/config';
|
import { defaultShareConfig } from '../../config/config';
|
||||||
|
|
||||||
@@ -120,7 +119,7 @@ Page({
|
|||||||
if (currentCardList.length > 0) {
|
if (currentCardList.length > 0) {
|
||||||
this.drawService
|
this.drawService
|
||||||
.draw(currentCardList)
|
.draw(currentCardList)
|
||||||
.catch((err) => {
|
.catch((err: any) => {
|
||||||
console.error('绘制失败:', err);
|
console.error('绘制失败:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -168,7 +167,7 @@ Page({
|
|||||||
}
|
}
|
||||||
// 如果canvas已初始化且有内容,则绘制
|
// 如果canvas已初始化且有内容,则绘制
|
||||||
if (this.drawService && updatedCardList!.length > 0) {
|
if (this.drawService && updatedCardList!.length > 0) {
|
||||||
this.drawService.draw(updatedCardList!).catch((err) => {
|
this.drawService.draw(updatedCardList!).catch((err: any) => {
|
||||||
console.error('绘制失败:', err);
|
console.error('绘制失败:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -186,7 +185,7 @@ Page({
|
|||||||
}
|
}
|
||||||
// 如果canvas已初始化且有内容,则绘制
|
// 如果canvas已初始化且有内容,则绘制
|
||||||
if (this.drawService && currentCardList.length > 0) {
|
if (this.drawService && currentCardList.length > 0) {
|
||||||
this.drawService.draw(currentCardList).catch((err) => {
|
this.drawService.draw(currentCardList).catch((err: any) => {
|
||||||
console.error('绘制失败:', err);
|
console.error('绘制失败:', err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -288,27 +287,16 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** 下载打印 */
|
/** 下载打印 */
|
||||||
exportToPrint() {
|
async exportToPrint() {
|
||||||
if (!this.canvas || this.data.cardList.length <= 0) {
|
if (!this.canvas || this.data.cardList.length <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否需要显示分享引导
|
await downloadPrint(this.canvas, {
|
||||||
if (shouldShowShareGuide()) {
|
errorToast: '请先生成涂色卡',
|
||||||
this.setData({ showShareDialog: true });
|
trackerName: '涂色识字',
|
||||||
return;
|
trackerMode: this.data.selectedTemplate,
|
||||||
}
|
});
|
||||||
|
|
||||||
// 上报下载埋点
|
|
||||||
tracker.reportDownload('涂色识字', this.data.selectedTemplate);
|
|
||||||
|
|
||||||
// 直接下载
|
|
||||||
checkAndSaveImage(this.canvas);
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 关闭分享引导弹窗 */
|
|
||||||
onCloseShareDialog() {
|
|
||||||
this.setData({ showShareDialog: false });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 分享成功回调(由组件触发) */
|
/** 分享成功回调(由组件触发) */
|
||||||
|
|||||||
@@ -2,120 +2,118 @@ import { BaseDrawService } from './baseDraw';
|
|||||||
import { CharacterItem } from '../types/characterType';
|
import { CharacterItem } from '../types/characterType';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
|
* cnchar-data 坐标系配置
|
||||||
|
* 参考 hanzi-writer 的实现逻辑
|
||||||
|
* 字符数据的边界框:左上角 (0, -124),右下角 (1024, 900)
|
||||||
*/
|
*/
|
||||||
interface DrawSvgPathCommandsParams {
|
const CHAR_BOUNDS = {
|
||||||
ctx: RenderingContext;
|
minX: 0,
|
||||||
pathD: string;
|
minY: -124,
|
||||||
offsetX: number;
|
maxX: 1024,
|
||||||
offsetY: number;
|
maxY: 900,
|
||||||
|
};
|
||||||
|
const CHAR_WIDTH = CHAR_BOUNDS.maxX - CHAR_BOUNDS.minX; // 1024
|
||||||
|
const CHAR_HEIGHT = CHAR_BOUNDS.maxY - CHAR_BOUNDS.minY; // 1024
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算缩放变换参数(模仿 cnchar.draw 的 getScalingTransform 函数)
|
||||||
|
* 用于将 1024x1024 坐标系的字符数据变换到目标画布尺寸
|
||||||
|
*/
|
||||||
|
interface ScalingTransform {
|
||||||
|
xOffset: number;
|
||||||
|
yOffset: number;
|
||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function drawSvgPathCommands({
|
function getScalingTransform(
|
||||||
ctx,
|
width: number,
|
||||||
pathD,
|
height: number,
|
||||||
offsetX,
|
padding: number,
|
||||||
offsetY,
|
): ScalingTransform {
|
||||||
scale,
|
// 计算可用空间
|
||||||
}: DrawSvgPathCommandsParams) {
|
const availableWidth = width - 2 * padding;
|
||||||
// console.log('drawSvgPathCommands 参数:', { pathD, offsetX, offsetY, scale });
|
const availableHeight = height - 2 * padding;
|
||||||
|
|
||||||
// 精度处理函数:避免浮点数精度问题(提高到6位小数精度,确保高精度)
|
// 计算缩放比例(取较小的比例以确保字符完整显示)
|
||||||
const roundToPrecision = (num: number, precision: number = 6): number => {
|
const scaleX = availableWidth / CHAR_WIDTH;
|
||||||
return (
|
const scaleY = availableHeight / CHAR_HEIGHT;
|
||||||
Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision)
|
const scale = Math.min(scaleX, scaleY);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 像素对齐函数:优化对齐策略,提升平滑度
|
// 计算偏移量(居中显示)
|
||||||
// 对于Fill+Stroke模式,使用更精细的对齐策略,避免过度对齐导致的锯齿
|
const scaledWidth = CHAR_WIDTH * scale;
|
||||||
// 对齐到0.5像素可以获得更好的平滑效果
|
const scaledHeight = CHAR_HEIGHT * scale;
|
||||||
const alignToPixel = (num: number): number => {
|
const centerX = padding + (availableWidth - scaledWidth) / 2;
|
||||||
// 对齐到0.5像素,而不是整数像素,可以获得更平滑的线条
|
const centerY = padding + (availableHeight - scaledHeight) / 2;
|
||||||
return Math.round(num * 2) / 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新的解析方法:按命令片段解析
|
// 计算 xOffset 和 yOffset(考虑字符边界框的偏移)
|
||||||
// 使用正则表达式匹配从字母开头到下一个字母(或结尾)的片段
|
const xOffset = -CHAR_BOUNDS.minX * scale + centerX;
|
||||||
const commandRegex = /([MLZ])([^MLZ]*?)(?=[MLZ]|$)/g;
|
const yOffset = -CHAR_BOUNDS.minY * scale + centerY;
|
||||||
|
|
||||||
|
return { xOffset, yOffset, scale };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG 路径解析与绘制(支持 M/L/Q/Z 命令)
|
||||||
|
* 参考 cnchar.draw 的实现,使用 Canvas transform 进行坐标变换
|
||||||
|
*/
|
||||||
|
interface DrawSvgPathParams {
|
||||||
|
ctx: RenderingContext;
|
||||||
|
pathD: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSvgPath({ ctx, pathD }: DrawSvgPathParams) {
|
||||||
|
// 使用正则表达式匹配 SVG 路径命令
|
||||||
|
// 支持 M(moveTo)、L(lineTo)、Q(二次贝塞尔曲线)、Z(closePath)
|
||||||
|
const commandRegex = /([MLQZ])([^MLQZ]*?)(?=[MLQZ]|$)/gi;
|
||||||
const commands: Array<{ cmd: string; coords: string }> = [];
|
const commands: Array<{ cmd: string; coords: string }> = [];
|
||||||
|
|
||||||
let match;
|
let match;
|
||||||
while ((match = commandRegex.exec(pathD)) !== null) {
|
while ((match = commandRegex.exec(pathD)) !== null) {
|
||||||
const cmd = match[1];
|
const cmd = match[1].toUpperCase();
|
||||||
const coords = match[2].trim();
|
const coords = match[2].trim();
|
||||||
commands.push({ cmd, coords });
|
commands.push({ cmd, coords });
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('解析的命令片段:', commands);
|
// 解析坐标的辅助函数
|
||||||
|
const parseCoords = (coords: string): number[] => {
|
||||||
let commandCount = 0;
|
return coords
|
||||||
|
.split(/[\s,]+/)
|
||||||
// 优化的坐标解析和绘制函数
|
.filter((part) => part.trim() !== '')
|
||||||
const parseAndDrawCoords = (
|
.map((part) => parseFloat(part));
|
||||||
coords: string,
|
|
||||||
cmd: string,
|
|
||||||
drawFunction: (x: number, y: number) => void,
|
|
||||||
) => {
|
|
||||||
// 更严格的坐标解析:支持负数和小数
|
|
||||||
const coordParts = coords
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter((part) => part.trim() !== '');
|
|
||||||
|
|
||||||
if (coordParts.length >= 2) {
|
|
||||||
const x = parseFloat(coordParts[0]);
|
|
||||||
const y = parseFloat(coordParts[1]);
|
|
||||||
|
|
||||||
if (!isNaN(x) && !isNaN(y) && isFinite(x) && isFinite(y)) {
|
|
||||||
// 计算最终坐标:先进行高精度计算,再对齐到整数像素
|
|
||||||
// 使用高精度计算确保路径的准确性,然后对齐到像素避免亚像素渲染
|
|
||||||
const calculatedX = offsetX + x * scale;
|
|
||||||
const calculatedY = offsetY + y * scale;
|
|
||||||
const preciseX = roundToPrecision(calculatedX);
|
|
||||||
const preciseY = roundToPrecision(calculatedY);
|
|
||||||
const finalX = alignToPixel(preciseX);
|
|
||||||
const finalY = alignToPixel(preciseY);
|
|
||||||
|
|
||||||
// console.log(`${cmd}: 绘制到 (${finalX}, ${finalY}) [原始: (${x}, ${y})]`);
|
|
||||||
drawFunction(finalX, finalY);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`${cmd}: 坐标解析失败 - x=${x}, y=${y}, 原始坐标: "${coords}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
`${cmd}: 坐标数量不足,需要2个,实际${coordParts.length}个,原始: "${coords}"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 命令处理映射表,提高可读性和可扩展性
|
|
||||||
const commandHandlers: Record<string, (coords: string) => void> = {
|
|
||||||
M: (coords) =>
|
|
||||||
parseAndDrawCoords(coords, 'M', (x, y) => ctx.moveTo(x, y)),
|
|
||||||
L: (coords) =>
|
|
||||||
parseAndDrawCoords(coords, 'L', (x, y) => ctx.lineTo(x, y)),
|
|
||||||
Z: () => {
|
|
||||||
// 关键修复:处理Z命令,闭合路径
|
|
||||||
ctx.closePath();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const { cmd, coords } of commands) {
|
for (const { cmd, coords } of commands) {
|
||||||
commandCount++;
|
const coordValues = parseCoords(coords);
|
||||||
// console.log(`命令 ${commandCount}: ${cmd}, 坐标: "${coords}"`);
|
|
||||||
|
|
||||||
const handler = commandHandlers[cmd];
|
switch (cmd) {
|
||||||
if (handler) {
|
case 'M':
|
||||||
handler(coords);
|
if (coordValues.length >= 2) {
|
||||||
} else {
|
ctx.moveTo(coordValues[0], coordValues[1]);
|
||||||
console.warn(`未知命令: ${cmd}`);
|
}
|
||||||
|
break;
|
||||||
|
case 'L':
|
||||||
|
if (coordValues.length >= 2) {
|
||||||
|
ctx.lineTo(coordValues[0], coordValues[1]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Q':
|
||||||
|
// 二次贝塞尔曲线:Q cpx cpy x y
|
||||||
|
if (coordValues.length >= 4) {
|
||||||
|
ctx.quadraticCurveTo(
|
||||||
|
coordValues[0],
|
||||||
|
coordValues[1],
|
||||||
|
coordValues[2],
|
||||||
|
coordValues[3],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Z':
|
||||||
|
ctx.closePath();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`未知的 SVG 路径命令: ${cmd}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`总共处理了 ${commandCount} 个命令`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DrawStrokesParams {
|
interface DrawStrokesParams {
|
||||||
@@ -130,6 +128,10 @@ interface DrawStrokesParams {
|
|||||||
lineWidth: number;
|
lineWidth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绘制笔画(参考 cnchar.draw 的实现方式)
|
||||||
|
* 使用 Canvas transform 进行坐标变换,支持 1024x1024 坐标系
|
||||||
|
*/
|
||||||
function drawStrokes({
|
function drawStrokes({
|
||||||
ctx,
|
ctx,
|
||||||
strokes,
|
strokes,
|
||||||
@@ -137,93 +139,51 @@ function drawStrokes({
|
|||||||
offsetY,
|
offsetY,
|
||||||
size,
|
size,
|
||||||
uptoInclusive,
|
uptoInclusive,
|
||||||
fillStyle, // 填充颜色,用于 Fill + Stroke 模式
|
fillStyle,
|
||||||
strokeStyle, // 描边颜色,用于绘制笔画
|
strokeStyle: _strokeStyle, // 保留接口兼容性,cnchar 模式只使用 fill
|
||||||
lineWidth, // 线条宽度,控制笔画粗细
|
lineWidth: _lineWidth, // 保留接口兼容性,cnchar 模式只使用 fill
|
||||||
}: DrawStrokesParams) {
|
}: DrawStrokesParams) {
|
||||||
// 设置 Canvas 绘制质量(关键优化:提升线条流畅度)
|
// 抑制未使用变量警告
|
||||||
|
void _strokeStyle;
|
||||||
|
void _lineWidth;
|
||||||
|
// 设置 Canvas 绘制质量
|
||||||
ctx.imageSmoothingEnabled = true;
|
ctx.imageSmoothingEnabled = true;
|
||||||
ctx.imageSmoothingQuality = 'high'; // 高质量平滑
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
// 设置路径绘制属性(关键:这些设置直接影响笔画粗细和流畅度)
|
|
||||||
// 注意:不使用 save/restore,与测试页面保持一致,避免状态管理问题
|
|
||||||
ctx.lineCap = 'round'; // 圆角端点,使笔画末端更自然
|
|
||||||
ctx.lineJoin = 'round'; // 圆角连接,使转折处更平滑
|
|
||||||
ctx.miterLimit = 10; // 斜接限制
|
|
||||||
|
|
||||||
// 优化渲染策略:使用 Fill + Stroke 模式
|
|
||||||
// 先 fill 填充内部,再 stroke 描边,stroke 宽度略小,避免双重渲染导致的毛糙
|
|
||||||
// 这样可以获得自然的笔画粗细,同时保持流畅度
|
|
||||||
|
|
||||||
// 模板中使用 54x54 的视窗尺寸
|
// 设置路径绘制属性
|
||||||
const viewBoxSize = 54;
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.miterLimit = 10;
|
||||||
|
|
||||||
// 添加内边距:在田字格四周预留空间,避免笔画贴边
|
// 添加内边距:在田字格四周预留空间,避免笔画贴边
|
||||||
const padding = size * 0.1; // 内边距为田字格大小的10%
|
const padding = size * 0.1;
|
||||||
const contentSize = size - padding * 2; // 实际绘制区域大小
|
|
||||||
|
|
||||||
// 优化缩放比例:使用更精确的缩放,不进行四舍五入,保持原始精度
|
|
||||||
const contentScale = contentSize / viewBoxSize;
|
|
||||||
|
|
||||||
// 计算内容区域的起始位置(居中),优化对齐策略提升平滑度
|
// 计算缩放变换参数(参考 cnchar.draw 的 getScalingTransform)
|
||||||
const rawOffsetX = offsetX + padding;
|
const transform = getScalingTransform(size, size, padding);
|
||||||
const rawOffsetY = offsetY + padding;
|
|
||||||
// 对齐到0.5像素,而不是整数像素,可以获得更平滑的渲染效果
|
|
||||||
const contentOffsetX = Math.round(rawOffsetX * 2) / 2;
|
|
||||||
const contentOffsetY = Math.round(rawOffsetY * 2) / 2;
|
|
||||||
|
|
||||||
// console.log('drawStrokes 参数:', {
|
|
||||||
// strokesCount: strokes.length,
|
|
||||||
// uptoInclusive,
|
|
||||||
// offsetX,
|
|
||||||
// offsetY,
|
|
||||||
// size,
|
|
||||||
// padding,
|
|
||||||
// contentSize,
|
|
||||||
// contentOffsetX,
|
|
||||||
// contentOffsetY,
|
|
||||||
// originalScale: scale,
|
|
||||||
// contentScale,
|
|
||||||
// fillStyle,
|
|
||||||
// strokeStyle,
|
|
||||||
// lineWidth
|
|
||||||
// });
|
|
||||||
|
|
||||||
for (let s = 0; s <= uptoInclusive && s < strokes.length; s++) {
|
for (let s = 0; s <= uptoInclusive && s < strokes.length; s++) {
|
||||||
// console.log(`绘制第 ${s} 个笔画:`, strokes[s]);
|
ctx.save();
|
||||||
|
|
||||||
|
// 应用变换矩阵(参考 cnchar.draw 的方式)
|
||||||
|
// 1. 先平移到目标位置
|
||||||
|
// 2. 再应用 Y 轴翻转和缩放
|
||||||
|
// transform: translate(xOffset, height - yOffset) scale(scale, -scale)
|
||||||
|
ctx.translate(
|
||||||
|
offsetX + transform.xOffset,
|
||||||
|
offsetY + size - transform.yOffset,
|
||||||
|
);
|
||||||
|
ctx.scale(transform.scale, -transform.scale);
|
||||||
|
|
||||||
|
// 绘制路径
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
drawSvgPath({ ctx, pathD: strokes[s] });
|
||||||
// 记录路径是否包含Z命令(已闭合)
|
|
||||||
const hasClosePath = strokes[s].includes('Z');
|
// 使用 fill 绘制(cnchar 只使用 fill)
|
||||||
|
|
||||||
drawSvgPathCommands({
|
|
||||||
ctx,
|
|
||||||
pathD: strokes[s],
|
|
||||||
offsetX: contentOffsetX,
|
|
||||||
offsetY: contentOffsetY,
|
|
||||||
scale: contentScale,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 优化渲染策略:使用 Fill + Stroke 模式
|
|
||||||
// 先 fill 填充内部,再 stroke 描边,stroke 宽度略小,避免双重渲染导致的毛糙
|
|
||||||
// 如果路径没有Z命令,手动闭合(fill需要闭合路径)
|
|
||||||
if (!hasClosePath) {
|
|
||||||
ctx.closePath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 先填充内部
|
|
||||||
ctx.fillStyle = fillStyle;
|
ctx.fillStyle = fillStyle;
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// 2. 再描边(stroke宽度设为fill视觉宽度的70%,避免明显的双重渲染)
|
ctx.restore();
|
||||||
// 与测试页面保持一致:使用相同的stroke宽度计算方式
|
|
||||||
ctx.strokeStyle = strokeStyle;
|
|
||||||
const strokeWidth = Math.max(1.5, lineWidth * 0.7);
|
|
||||||
ctx.lineWidth = strokeWidth;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注意:不使用 restore,与测试页面保持一致
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DrawTianZiGridParams {
|
interface DrawTianZiGridParams {
|
||||||
@@ -580,7 +540,7 @@ class WordDrawService extends BaseDrawService {
|
|||||||
uptoInclusive: strokes.length - 1,
|
uptoInclusive: strokes.length - 1,
|
||||||
fillStyle: 'rgb(85,85,85)', // 深灰色填充(#555555),比#666666更深一点
|
fillStyle: 'rgb(85,85,85)', // 深灰色填充(#555555),比#666666更深一点
|
||||||
strokeStyle: 'rgb(85,85,85)', // 深灰色描边
|
strokeStyle: 'rgb(85,85,85)', // 深灰色描边
|
||||||
lineWidth: 0.6, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
lineWidth: 1.2, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -626,7 +586,7 @@ class WordDrawService extends BaseDrawService {
|
|||||||
uptoInclusive: strokeIndex,
|
uptoInclusive: strokeIndex,
|
||||||
fillStyle: 'rgb(170,170,170)', // 浅灰色填充(#aaaaaa),参考练字贴颜色
|
fillStyle: 'rgb(170,170,170)', // 浅灰色填充(#aaaaaa),参考练字贴颜色
|
||||||
strokeStyle: 'rgb(170,170,170)', // 浅灰色描边
|
strokeStyle: 'rgb(170,170,170)', // 浅灰色描边
|
||||||
lineWidth: 0.6, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
lineWidth: 1.2, // Fill + Stroke 模式,线条宽度调小一点(逻辑像素)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* 下载打印通用工具
|
||||||
|
* 支持下载次数限制和激励广告
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { checkAndSaveImage } from './saveImage';
|
||||||
|
import tracker from './tracker';
|
||||||
|
|
||||||
|
// 存储键名
|
||||||
|
const STORAGE_KEY_DOWNLOAD_COUNT = 'downloadCount';
|
||||||
|
const STORAGE_KEY_DOWNLOAD_DATE = 'downloadDate';
|
||||||
|
const STORAGE_KEY_WATCHED_AD_TODAY = 'watchedAdToday';
|
||||||
|
|
||||||
|
// 免费下载次数限制
|
||||||
|
const FREE_DOWNLOAD_LIMIT = 5;
|
||||||
|
|
||||||
|
// 激励视频广告单元ID
|
||||||
|
const AD_UNIT_ID = 'adunit-eff5626f8b68c2e0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今天的日期字符串(YYYY-MM-DD格式)
|
||||||
|
*/
|
||||||
|
function getTodayDateString(): string {
|
||||||
|
const today = new Date();
|
||||||
|
const year = today.getFullYear();
|
||||||
|
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(today.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取今日下载次数
|
||||||
|
*/
|
||||||
|
function getTodayDownloadCount(): number {
|
||||||
|
const today = getTodayDateString();
|
||||||
|
const downloadDate = wx.getStorageSync(STORAGE_KEY_DOWNLOAD_DATE);
|
||||||
|
|
||||||
|
// 如果不是今天,重置计数
|
||||||
|
if (downloadDate !== today) {
|
||||||
|
wx.setStorageSync(STORAGE_KEY_DOWNLOAD_COUNT, 0);
|
||||||
|
wx.setStorageSync(STORAGE_KEY_DOWNLOAD_DATE, today);
|
||||||
|
wx.setStorageSync(STORAGE_KEY_WATCHED_AD_TODAY, false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wx.getStorageSync(STORAGE_KEY_DOWNLOAD_COUNT) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加今日下载次数
|
||||||
|
*/
|
||||||
|
function incrementDownloadCount(): void {
|
||||||
|
const today = getTodayDateString();
|
||||||
|
const count = getTodayDownloadCount();
|
||||||
|
wx.setStorageSync(STORAGE_KEY_DOWNLOAD_COUNT, count + 1);
|
||||||
|
wx.setStorageSync(STORAGE_KEY_DOWNLOAD_DATE, today);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查今日是否已观看广告
|
||||||
|
*/
|
||||||
|
function hasWatchedAdToday(): boolean {
|
||||||
|
const today = getTodayDateString();
|
||||||
|
const watchedDate = wx.getStorageSync(STORAGE_KEY_WATCHED_AD_TODAY);
|
||||||
|
return watchedDate === today;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记今日已观看广告
|
||||||
|
*/
|
||||||
|
function markWatchedAdToday(): void {
|
||||||
|
const today = getTodayDateString();
|
||||||
|
wx.setStorageSync(STORAGE_KEY_WATCHED_AD_TODAY, today);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建激励视频广告实例
|
||||||
|
*/
|
||||||
|
function createVideoAd() {
|
||||||
|
if (!wx.createRewardedVideoAd) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const videoAd = wx.createRewardedVideoAd({
|
||||||
|
adUnitId: AD_UNIT_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
videoAd.onLoad(() => {
|
||||||
|
console.log('激励视频广告加载成功');
|
||||||
|
});
|
||||||
|
|
||||||
|
videoAd.onError((err) => {
|
||||||
|
console.error('激励视频广告加载失败', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return videoAd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示激励视频广告
|
||||||
|
*/
|
||||||
|
function showVideoAd(): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const videoAd = createVideoAd();
|
||||||
|
|
||||||
|
if (!videoAd) {
|
||||||
|
wx.showToast({
|
||||||
|
title: '当前环境不支持激励广告',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 loading
|
||||||
|
wx.showLoading({
|
||||||
|
title: '加载中...',
|
||||||
|
mask: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听广告关闭事件
|
||||||
|
videoAd.onClose((res) => {
|
||||||
|
// 隐藏 loading(如果还在显示)
|
||||||
|
wx.hideLoading();
|
||||||
|
|
||||||
|
if (res && res.isEnded) {
|
||||||
|
// 用户完整观看了广告
|
||||||
|
markWatchedAdToday();
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
// 用户提前关闭了广告
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示广告
|
||||||
|
videoAd
|
||||||
|
.show()
|
||||||
|
.then(() => {
|
||||||
|
// 广告显示成功,隐藏 loading
|
||||||
|
wx.hideLoading();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 失败重试,保持 loading 显示
|
||||||
|
videoAd
|
||||||
|
.load()
|
||||||
|
.then(() => {
|
||||||
|
// 加载成功,继续显示广告
|
||||||
|
return videoAd.show();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// 广告显示成功,隐藏 loading
|
||||||
|
wx.hideLoading();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// 最终失败,隐藏 loading 并提示
|
||||||
|
wx.hideLoading();
|
||||||
|
console.error('激励视频广告显示失败', err);
|
||||||
|
wx.showToast({
|
||||||
|
title: '广告加载失败,请稍后重试',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行下载
|
||||||
|
*/
|
||||||
|
function doDownload(
|
||||||
|
canvas: Canvas,
|
||||||
|
options: {
|
||||||
|
successToast?: string;
|
||||||
|
errorToast?: string;
|
||||||
|
trackerName?: string;
|
||||||
|
trackerMode?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
// 上报下载埋点
|
||||||
|
if (options.trackerName) {
|
||||||
|
if (options.trackerMode) {
|
||||||
|
tracker.reportDownload(options.trackerName, options.trackerMode);
|
||||||
|
} else {
|
||||||
|
tracker.reportDownload(options.trackerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加下载次数
|
||||||
|
incrementDownloadCount();
|
||||||
|
|
||||||
|
// 执行下载(checkAndSaveImage 内部已有成功提示,这里不需要额外处理)
|
||||||
|
checkAndSaveImage(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载打印(通用方法)
|
||||||
|
* @param canvas Canvas 实例
|
||||||
|
* @param options 配置选项
|
||||||
|
* @param options.successToast 成功提示(可选,默认:'保存成功,请在相册中查看')
|
||||||
|
* @param options.errorToast 错误提示(可选,默认:'请先生成内容')
|
||||||
|
* @param options.trackerName 埋点名称(可选)
|
||||||
|
* @param options.trackerMode 埋点模式(可选)
|
||||||
|
*/
|
||||||
|
export async function downloadPrint(
|
||||||
|
canvas: Canvas | null,
|
||||||
|
options: {
|
||||||
|
successToast?: string;
|
||||||
|
errorToast?: string;
|
||||||
|
trackerName?: string;
|
||||||
|
trackerMode?: string;
|
||||||
|
} = {},
|
||||||
|
): Promise<void> {
|
||||||
|
// 检查 canvas 是否存在
|
||||||
|
if (!canvas) {
|
||||||
|
wx.showToast({
|
||||||
|
title: options.errorToast || '请先生成内容',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取今日下载次数
|
||||||
|
const downloadCount = getTodayDownloadCount();
|
||||||
|
const hasWatchedAd = hasWatchedAdToday();
|
||||||
|
|
||||||
|
// 如果已观看广告,直接下载
|
||||||
|
if (hasWatchedAd) {
|
||||||
|
doDownload(canvas, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果未超过免费次数,直接下载
|
||||||
|
if (downloadCount < FREE_DOWNLOAD_LIMIT) {
|
||||||
|
doDownload(canvas, options);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超过免费次数,需要观看广告
|
||||||
|
const watched = await showVideoAd();
|
||||||
|
if (watched) {
|
||||||
|
// 观看完成,可以下载
|
||||||
|
doDownload(canvas, options);
|
||||||
|
}
|
||||||
|
/* else {
|
||||||
|
// 未观看完成,提示用户
|
||||||
|
wx.showToast({
|
||||||
|
title: '需要完整观看广告才能下载',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
} */
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ const CACHE_CONFIG = {
|
|||||||
expireDays: 7, // 缓存过期天数
|
expireDays: 7, // 缓存过期天数
|
||||||
// url: 'https://cdn.joeyone.cn/doodle/words-svg-all.json' // 远程数据地址
|
// url: 'https://cdn.joeyone.cn/doodle/words-svg-all.json' // 远程数据地址
|
||||||
// url: 'https://cdn.joeyone.cn/doodle/words-svg-3000.json' // 远程数据地址
|
// url: 'https://cdn.joeyone.cn/doodle/words-svg-3000.json' // 远程数据地址
|
||||||
url: 'https://cdn.joeyone.cn/doodle/char_svg_3500.json' // 远程数据地址
|
// url: 'https://cdn.joeyone.cn/doodle/char_svg_3500.json' // 远程数据地址
|
||||||
|
url: 'https://cdn.joeyone.cn/doodle/char_common_stroke.json', // 远程数据地址
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +29,7 @@ interface CacheData {
|
|||||||
function isCacheExpired(timestamp: number): boolean {
|
function isCacheExpired(timestamp: number): boolean {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expireTime = CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000; // 7天的毫秒数
|
const expireTime = CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000; // 7天的毫秒数
|
||||||
return (now - timestamp) > expireTime;
|
return now - timestamp > expireTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +53,10 @@ function getCacheData(): CacheData | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('使用本地缓存的SVG汉字数据,缓存时间:', new Date(cacheData.timestamp).toLocaleString());
|
console.log(
|
||||||
|
'使用本地缓存的SVG汉字数据,缓存时间:',
|
||||||
|
new Date(cacheData.timestamp).toLocaleString(),
|
||||||
|
);
|
||||||
return cacheData;
|
return cacheData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取SVG汉字数据缓存失败:', error);
|
console.error('读取SVG汉字数据缓存失败:', error);
|
||||||
@@ -69,7 +73,7 @@ function saveCacheData(data: Record<string, string[]>): void {
|
|||||||
const cacheData: CacheData = {
|
const cacheData: CacheData = {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
version: '1.0' // 可以用于后续版本控制
|
version: '1.0', // 可以用于后续版本控制
|
||||||
};
|
};
|
||||||
|
|
||||||
wx.setStorageSync(CACHE_CONFIG.key, JSON.stringify(cacheData));
|
wx.setStorageSync(CACHE_CONFIG.key, JSON.stringify(cacheData));
|
||||||
@@ -91,13 +95,17 @@ async function fetchRemoteData(): Promise<Record<string, string[]>> {
|
|||||||
{
|
{
|
||||||
timeout: 15000, // 15秒超时
|
timeout: 15000, // 15秒超时
|
||||||
header: {
|
header: {
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
'Cache-Control': 'no-cache'
|
'Cache-Control': 'no-cache',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('远程SVG汉字数据获取成功,共', Object.keys(svgWordsData).length, '个汉字');
|
console.log(
|
||||||
|
'远程SVG汉字数据获取成功,共',
|
||||||
|
Object.keys(svgWordsData).length,
|
||||||
|
'个汉字',
|
||||||
|
);
|
||||||
return svgWordsData;
|
return svgWordsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +151,12 @@ export function clearWordsSvgCache(): void {
|
|||||||
* 获取缓存信息
|
* 获取缓存信息
|
||||||
* @returns 缓存信息对象
|
* @returns 缓存信息对象
|
||||||
*/
|
*/
|
||||||
export function getCacheInfo(): { exists: boolean; timestamp?: number; isExpired?: boolean; daysLeft?: number } {
|
export function getCacheInfo(): {
|
||||||
|
exists: boolean;
|
||||||
|
timestamp?: number;
|
||||||
|
isExpired?: boolean;
|
||||||
|
daysLeft?: number;
|
||||||
|
} {
|
||||||
try {
|
try {
|
||||||
const cacheStr = wx.getStorageSync(CACHE_CONFIG.key);
|
const cacheStr = wx.getStorageSync(CACHE_CONFIG.key);
|
||||||
if (!cacheStr) {
|
if (!cacheStr) {
|
||||||
@@ -152,13 +165,19 @@ export function getCacheInfo(): { exists: boolean; timestamp?: number; isExpired
|
|||||||
|
|
||||||
const cacheData: CacheData = JSON.parse(cacheStr);
|
const cacheData: CacheData = JSON.parse(cacheStr);
|
||||||
const isExpired = isCacheExpired(cacheData.timestamp);
|
const isExpired = isCacheExpired(cacheData.timestamp);
|
||||||
const daysLeft = isExpired ? 0 : Math.ceil((CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000 - (Date.now() - cacheData.timestamp)) / (24 * 60 * 60 * 1000));
|
const daysLeft = isExpired
|
||||||
|
? 0
|
||||||
|
: Math.ceil(
|
||||||
|
(CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000 -
|
||||||
|
(Date.now() - cacheData.timestamp)) /
|
||||||
|
(24 * 60 * 60 * 1000),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exists: true,
|
exists: true,
|
||||||
timestamp: cacheData.timestamp,
|
timestamp: cacheData.timestamp,
|
||||||
isExpired,
|
isExpired,
|
||||||
daysLeft
|
daysLeft,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取缓存信息失败:', error);
|
console.error('获取缓存信息失败:', error);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { recordDownloadSuccess } from './shareGuide';
|
|
||||||
|
|
||||||
const doSaveImage = (canvas: Canvas) => {
|
const doSaveImage = (canvas: Canvas) => {
|
||||||
wx.canvasToTempFilePath({
|
wx.canvasToTempFilePath({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
@@ -9,8 +7,7 @@ const doSaveImage = (canvas: Canvas) => {
|
|||||||
wx.saveImageToPhotosAlbum({
|
wx.saveImageToPhotosAlbum({
|
||||||
filePath: res.tempFilePath,
|
filePath: res.tempFilePath,
|
||||||
success: () => {
|
success: () => {
|
||||||
// 记录下载成功
|
// 下载成功提示(下载次数记录在 downloadPrint.ts 中处理)
|
||||||
recordDownloadSuccess();
|
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '保存成功,请在相册中查看',
|
title: '保存成功,请在相册中查看',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
|
|||||||
Reference in New Issue
Block a user