From 643a7e23e75dec6a4bfaeba788dd4cd7b888833c Mon Sep 17 00:00:00 2001 From: R524809 Date: Mon, 27 Oct 2025 10:31:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E4=B8=BA=E4=BB=8E?= =?UTF-8?q?=E7=BD=91=E7=BB=9C=E7=AB=AF=E8=8E=B7=E5=8F=96=E5=AD=97=E4=BD=93?= =?UTF-8?q?svg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/pages/copyBook/copyBook.ts | 58 +++++---- miniprogram/utils/getWordsSvgJson.ts | 165 +++++++++++++++++++++++++ miniprogram/utils/http.ts | 123 ++++++++++++++++++ miniprogram/utils/index.ts | 6 + 4 files changed, 329 insertions(+), 23 deletions(-) create mode 100644 miniprogram/utils/getWordsSvgJson.ts create mode 100644 miniprogram/utils/http.ts diff --git a/miniprogram/pages/copyBook/copyBook.ts b/miniprogram/pages/copyBook/copyBook.ts index cc0e52d..e2d4738 100644 --- a/miniprogram/pages/copyBook/copyBook.ts +++ b/miniprogram/pages/copyBook/copyBook.ts @@ -1,6 +1,7 @@ import WordDrawService from '../../service/wordDrawService'; import { checkAndSaveImage } from '../../utils/saveImage'; import { PAPER_SIZE } from '../../constants/colors'; +import { getWordsSvgData } from '../../utils/getWordsSvgJson'; Page({ canvas: null as Canvas | null, @@ -35,30 +36,41 @@ Page({ this.initCanvas() }, - loadSvgWords() { - const fs = wx.getFileSystemManager(); - fs.readFile({ - filePath: 'constants/svgWords.json', // JSON文件路径,相对于项目根目录 - encoding: 'utf8', // 重要:指定编码为utf8以读取文本内容 - success: (res) => { - try { - wx.showToast({ title: '加载中...', icon: 'loading' }); - const parsedData = JSON.parse(res.data as string); - this.svgWords = parsedData; - wx.hideToast(); - const { words } = this.data as any; - if (words && words.length > 0) { - this.drawPracticeSheet().catch(console.error); - } - } catch (e) { - console.error('解析JSON失败', e); - } - }, - fail: (err) => { - console.error('读取文件失败', err); - wx.hideToast(); + async loadSvgWords() { + try { + wx.showToast({ title: '加载字体中...', icon: 'loading' }); + + // 使用带缓存的SVG汉字数据获取函数 + const svgWordsData = await getWordsSvgData(); + + this.svgWords = svgWordsData; + wx.hideToast(); + + console.log('SVG汉字数据加载成功,共', Object.keys(this.svgWords).length, '个汉字'); + + const { words } = this.data as any; + if (words && words.length > 0) { + this.drawPracticeSheet().catch(console.error); } - }); + } catch (error) { + console.error('加载SVG汉字数据失败:', error); + wx.hideToast(); + + // 显示错误提示 + wx.showModal({ + title: '加载失败', + content: '无法加载汉字数据,请检查网络连接后重试', + showCancel: true, + cancelText: '取消', + confirmText: '重试', + success: (res) => { + if (res.confirm) { + // 用户点击重试,重新加载 + this.loadSvgWords(); + } + } + }); + } }, // 输入组件回调 diff --git a/miniprogram/utils/getWordsSvgJson.ts b/miniprogram/utils/getWordsSvgJson.ts new file mode 100644 index 0000000..5932475 --- /dev/null +++ b/miniprogram/utils/getWordsSvgJson.ts @@ -0,0 +1,165 @@ +import { getJson } from './http'; + +/** + * SVG汉字数据缓存配置 + */ +const CACHE_CONFIG = { + key: 'svgWordsData', // 缓存键名 + expireDays: 7, // 缓存过期天数 + url: 'https://www.ice-sea.com/doodle/words-svg/words-svg-3000.json' // 远程数据地址 +}; + +/** + * 缓存数据结构 + */ +interface CacheData { + data: Record; // SVG汉字数据 + timestamp: number; // 缓存时间戳 + version?: string; // 数据版本(可选) +} + +/** + * 检查缓存是否过期 + * @param timestamp 缓存时间戳 + * @returns 是否过期 + */ +function isCacheExpired(timestamp: number): boolean { + const now = Date.now(); + const expireTime = CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000; // 7天的毫秒数 + return (now - timestamp) > expireTime; +} + +/** + * 从本地存储获取缓存数据 + * @returns 缓存数据或null + */ +function getCacheData(): CacheData | null { + try { + const cacheStr = wx.getStorageSync(CACHE_CONFIG.key); + if (!cacheStr) { + return null; + } + + const cacheData: CacheData = JSON.parse(cacheStr); + + // 检查缓存是否过期 + if (isCacheExpired(cacheData.timestamp)) { + console.log('SVG汉字数据缓存已过期,将重新获取'); + // 清除过期缓存 + wx.removeStorageSync(CACHE_CONFIG.key); + return null; + } + + console.log('使用本地缓存的SVG汉字数据,缓存时间:', new Date(cacheData.timestamp).toLocaleString()); + return cacheData; + } catch (error) { + console.error('读取SVG汉字数据缓存失败:', error); + return null; + } +} + +/** + * 保存数据到本地存储 + * @param data SVG汉字数据 + */ +function saveCacheData(data: Record): void { + try { + const cacheData: CacheData = { + data, + timestamp: Date.now(), + version: '1.0' // 可以用于后续版本控制 + }; + + wx.setStorageSync(CACHE_CONFIG.key, JSON.stringify(cacheData)); + console.log('SVG汉字数据已缓存到本地存储'); + } catch (error) { + console.error('保存SVG汉字数据缓存失败:', error); + } +} + +/** + * 从远程服务器获取SVG汉字数据 + * @returns Promise> + */ +async function fetchRemoteData(): Promise> { + console.log('从远程服务器获取SVG汉字数据...'); + + const svgWordsData = await getJson>( + CACHE_CONFIG.url, + { + timeout: 15000, // 15秒超时 + header: { + 'Accept': 'application/json', + 'Cache-Control': 'no-cache' + } + } + ); + + console.log('远程SVG汉字数据获取成功,共', Object.keys(svgWordsData).length, '个汉字'); + return svgWordsData; +} + +/** + * 获取SVG汉字数据(带缓存机制) + * 优先使用本地缓存,如果缓存不存在或已过期,则从远程获取 + * @returns Promise> + */ +export async function getWordsSvgData(): Promise> { + try { + // 1. 尝试从本地缓存获取数据 + const cacheData = getCacheData(); + if (cacheData) { + return cacheData.data; + } + + // 2. 缓存不存在或已过期,从远程获取 + const remoteData = await fetchRemoteData(); + + // 3. 保存到本地缓存 + saveCacheData(remoteData); + + return remoteData; + } catch (error) { + console.error('获取SVG汉字数据失败:', error); + throw error; + } +} + +/** + * 清除SVG汉字数据缓存 + */ +export function clearWordsSvgCache(): void { + try { + wx.removeStorageSync(CACHE_CONFIG.key); + console.log('SVG汉字数据缓存已清除'); + } catch (error) { + console.error('清除SVG汉字数据缓存失败:', error); + } +} + +/** + * 获取缓存信息 + * @returns 缓存信息对象 + */ +export function getCacheInfo(): { exists: boolean; timestamp?: number; isExpired?: boolean; daysLeft?: number } { + try { + const cacheStr = wx.getStorageSync(CACHE_CONFIG.key); + if (!cacheStr) { + return { exists: false }; + } + + const cacheData: CacheData = JSON.parse(cacheStr); + 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)); + + return { + exists: true, + timestamp: cacheData.timestamp, + isExpired, + daysLeft + }; + } catch (error) { + console.error('获取缓存信息失败:', error); + return { exists: false }; + } +} diff --git a/miniprogram/utils/http.ts b/miniprogram/utils/http.ts new file mode 100644 index 0000000..c12c84e --- /dev/null +++ b/miniprogram/utils/http.ts @@ -0,0 +1,123 @@ +/** + * HTTP请求工具模块 + */ + +interface HttpRequestOptions { + url: string; + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + data?: any; + header?: Record; + timeout?: number; +} + +interface HttpResponse { + data: T; + statusCode: number; + header: Record; +} + +/** + * 发起HTTP请求 + * @param options 请求配置 + * @returns Promise> + */ +export function request(options: HttpRequestOptions): Promise> { + return new Promise((resolve, reject) => { + wx.request({ + url: options.url, + method: options.method || 'GET', + data: options.data, + header: { + 'Content-Type': 'application/json', + ...options.header + }, + timeout: options.timeout || 10000, + success: (res) => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve({ + data: res.data as T, + statusCode: res.statusCode, + header: res.header as Record + }); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${res.data}`)); + } + }, + fail: (err) => { + reject(new Error(`请求失败: ${err.errMsg || '网络错误'}`)); + } + }); + }); +} + +/** + * GET请求 + * @param url 请求地址 + * @param options 额外配置 + * @returns Promise> + */ +export function get(url: string, options?: Partial): Promise> { + return request({ + url, + method: 'GET', + ...options + }); +} + +/** + * POST请求 + * @param url 请求地址 + * @param data 请求数据 + * @param options 额外配置 + * @returns Promise> + */ +export function post(url: string, data?: any, options?: Partial): Promise> { + return request({ + url, + method: 'POST', + data, + ...options + }); +} + +/** + * 下载文件 + * @param url 文件地址 + * @param options 额外配置 + * @returns Promise + */ +export function download(url: string, options?: { timeout?: number }): Promise { + return new Promise((resolve, reject) => { + wx.downloadFile({ + url, + timeout: options?.timeout || 10000, + success: (res) => { + if (res.statusCode === 200) { + // @ts-ignore + resolve(res.data); + } else { + reject(new Error(`下载失败: HTTP ${res.statusCode}`)); + } + }, + fail: (err) => { + reject(new Error(`下载失败: ${err.errMsg || '网络错误'}`)); + } + }); + }); +} + +/** + * 获取JSON数据 + * @param url JSON文件地址 + * @param options 额外配置 + * @returns Promise + */ +export async function getJson(url: string, options?: Partial): Promise { + try { + const response = await get(url, options); + return response.data; + } catch (error) { + console.error('获取JSON数据失败:', error); + throw error; + } +} diff --git a/miniprogram/utils/index.ts b/miniprogram/utils/index.ts index 37f4eb9..9a9abaa 100644 --- a/miniprogram/utils/index.ts +++ b/miniprogram/utils/index.ts @@ -1,3 +1,9 @@ +// 导出HTTP模块 +export * from './http'; + +// 导出SVG汉字数据获取模块 +export * from './getWordsSvgJson'; + export async function getMiniCodeImage(canvas: Canvas) { return getImage(canvas, '/assets/imgs/doodle-mini-code.jpg'); }