diff --git a/.gitattributes b/.gitattributes index 39f1d1e12b04589c6c4e3c00b70dfe2274137a3a..a74d77e6610c22b12664e552221bbae5fa312f78 100644 --- a/.gitattributes +++ b/.gitattributes @@ -52,3 +52,7 @@ img/memes.png filter=lfs diff=lfs merge=lfs -text 普通拼豆作品展示.MOV filter=lfs diff=lfs merge=lfs -text 烫好了,还没有揭开烫纸,用微信二维码扫一扫,成功扫码.MP4 filter=lfs diff=lfs merge=lfs -text 用烫豆机对拼好的作品烫.MOV filter=lfs diff=lfs merge=lfs -text +BeadQR/docs/promo/douwei-1080-cover.png filter=lfs diff=lfs merge=lfs -text +BeadQR/docs/promo/douwei-1080.mp4 filter=lfs diff=lfs merge=lfs -text +BeadQR/docs/promo/douwei-720-cover.png filter=lfs diff=lfs merge=lfs -text +BeadQR/docs/promo/douwei-720.mp4 filter=lfs diff=lfs merge=lfs -text diff --git a/BeadQR/README.md b/BeadQR/README.md new file mode 100644 index 0000000000000000000000000000000000000000..58e1c1be66ddca4b41d2a437f4899b49d3837beb --- /dev/null +++ b/BeadQR/README.md @@ -0,0 +1,50 @@ +# 豆维 · 可扫码的拼豆 + +输入一句话(可以再选一张照片),生成**用微信扫一扫就能读出内容**的 MARD 拼豆图纸,并支持导出 PDF 或图片。 + +## 本版重点 + +- **一眼看懂**:首屏直接说明用途,页面按“写一句话 → 选张照片 → 导出图纸”三步走,打开就能看到示例效果。 +- **更快**:配色查表加速,色格数据不再走页面数据通道;PDF 改为矢量直出(毫秒级,文件更小);导出时不再重复绘制。 +- **更好看、更好扫**: + - 感知更准的配色(OKLab) + - 照片按面积取平均色,不再有锯齿 + - 相邻色块自动平滑,零散颜色自动合并,所需豆子种类更少 + - 二维码定位图形强制高对比 + - 本机扫码自检:扫不出时一键优化 +- **打印友好**:PDF 为 A4 矢量图纸,大作品会自动分块并附总览页;每一格都印有色号,另附带用量的清单。 +- **精选色板**:除标准 221 色和完整 291 色外,还提供 144/96/72/48/24 色精选套装,方便少买豆。 + +## 内容安全链路(按微信平台要求) + +| 环节 | 措施 | +| --- | --- | +| 二维码文字、扫码导入的文字、水印 | 云函数调用 `security.msgSecCheck` v2(带 openid、按场景值检测),结果为 risky 或 review 均拦截 | +| 照片 | 选图后立即压缩到官方推荐的 750×1334 以内,由云函数调用 `security.imgSecCheck`;**检测通过前不能应用照片** | +| 检测异常 | 一律视为未通过,不允许生成与导出(fail-closed) | +| 导出前 | 再次校验全部文本(已通过的走缓存,不增加等待) | +| 云函数加固 | 必须有 OPENID;只接受 `sec-check/` 目录下的 jpg;通过文件头判断格式;限制 1MB;检测后立即删除云端临时文件 | +| 隐私 | 已开启 `__usePrivacyCheck__`,内置隐私授权弹窗;页脚设有“隐私保护指引”入口 | +| 投诉与反馈 | 页脚提供“内容规范”说明及 `open-type="feedback"` 的投诉反馈入口 | +| 分享 | 分享出去的只是小程序卡片,不带用户内容 | + +## 部署步骤 + +1. 用微信开发者工具打开本目录(appid 已配置)。 +2. 右键 `cloudfunctions/quickstartFunctions` →「上传并部署:云端安装依赖」。**本版云函数有改动,必须重新部署。** +3. 在云开发控制台 → 存储中确认可以写入 `sec-check/` 目录(默认权限即可)。 +4. 在小程序管理后台 →「设置 → 服务内容声明 → 用户隐私保护指引」中声明:选择图片/拍照(`chooseMedia`)、保存到相册(`saveImageToPhotosAlbum`)、扫码(`scanCode`)。 +5. 真机测试:正常/违规文字、正常/违规图片、断网时的检测异常,确认都会被正确拦截和提示。 + +## 说明 + +- 为保证扫码成功率,**二维码默认保留 1 格白边**。如需原来的“无白边”效果,可在「高级设置 → 白边」中选择“无”。 +- 圆豆、中空豆样式只影响图纸的显示效果,实际拼出来的位置与方豆完全一致。 +- 第三方代码:`utils/qrcore`(node-qrcode,MIT)、`utils/vendor/jsqr.js`(jsQR,Apache-2.0)。 + +## 首屏演示动画 & 宣传视频 + +- 首次打开(还没写内容)时,预览区播放 12 秒 1:1 演示动画:写一句话 → 编成二维码 → 融进照片 → 微信一扫 → 结尾卡片。 +- 动画由 `miniprogram/utils/intro.js` 纯代码实时绘制,**不含任何视频/图片素材,几乎不占包体积**(约 20KB)。演示作品由真实配色引擎生成,画面里的二维码真的能扫出 “Happy Birthday!”。 +- 滑出屏幕、切到后台、打开裁剪时自动暂停;点一下预览区重新播放;写下内容(或选了照片)后自动切回真实二维码。 +- `docs/promo/` 里是用同一套代码渲染的宣传视频(1080 / 720,含原创合成配乐)和封面图,可直接发公众号。该目录不在 `miniprogram/` 内,不计入小程序包体积。 diff --git a/BeadQR/cloudfunctions/quickstartFunctions/config.json b/BeadQR/cloudfunctions/quickstartFunctions/config.json new file mode 100644 index 0000000000000000000000000000000000000000..a1d2eac6ed64d91b41f32dd6a796a2fb91328985 --- /dev/null +++ b/BeadQR/cloudfunctions/quickstartFunctions/config.json @@ -0,0 +1,9 @@ +{ + "timeout": 20, + "permissions": { + "openapi": [ + "security.msgSecCheck", + "security.imgSecCheck" + ] + } +} diff --git a/BeadQR/cloudfunctions/quickstartFunctions/index.js b/BeadQR/cloudfunctions/quickstartFunctions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..24bdb9b2b2dba5b4e592e6b61c46ec3124794dd7 --- /dev/null +++ b/BeadQR/cloudfunctions/quickstartFunctions/index.js @@ -0,0 +1,92 @@ +// 豆维 · 内容安全云函数 +// type: checkMessage | checkMessages | checkImage +// 返回约定:{ risk: boolean, skipped?: boolean };skipped 表示检测未能完成,前端必须拦截 +const cloud = require("wx-server-sdk"); +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const MAX_TEXT = 2500; +const MAX_IMAGE_BYTES = 1024 * 1024; +const MAX_BATCH = 5; + +const extractErrCode = (error) => { + if (!error) return undefined; + if (typeof error.errCode === "number") return error.errCode; + if (typeof error.code === "number") return error.code; + const match = typeof error.errMsg === "string" ? error.errMsg.match(/-?\d{3,}/) : null; + return match ? Number(match[0]) : undefined; +}; + +// msgSecCheck v2:scene 1 资料 / 2 评论 / 3 论坛 / 4 社交日志 +async function checkOneMessage(content, scene, openid) { + const value = String(content || "").slice(0, MAX_TEXT); + if (!value.trim()) return { risk: false }; + const safeScene = [1, 2, 3, 4].includes(Number(scene)) ? Number(scene) : 2; + try { + const response = await cloud.openapi.security.msgSecCheck({ + version: 2, + scene: safeScene, + openid, + content: value, + }); + const suggest = response && response.result && response.result.suggest; + if (!suggest) return { risk: false, skipped: true }; + // review(疑似)按违规处理,宁严勿松 + return { risk: suggest === "risky" || suggest === "review", verdict: suggest }; + } catch (e) { + if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" }; + return { risk: false, skipped: true, errCode: extractErrCode(e) }; + } +} + +// 通过文件头判断真实格式,不信任客户端传入的类型 +function sniffType(buffer) { + if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg"; + if (buffer.length > 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) return "image/png"; + return ""; +} + +async function checkImage(fileID) { + // 只允许检测本小程序上传到 sec-check/ 目录的临时文件,防止借接口删除/读取其他文件 + if (typeof fileID !== "string" || !/^cloud:\/\/[^/]+\/sec-check\/[\w.-]+\.jpg$/.test(fileID)) { + return { risk: false, skipped: true, reason: "INVALID_FILE" }; + } + try { + const download = await cloud.downloadFile({ fileID }); + const buffer = download && download.fileContent; + const contentType = buffer ? sniffType(buffer) : ""; + if (!buffer || !buffer.length || buffer.length > MAX_IMAGE_BYTES || !contentType) { + return { risk: false, skipped: true, reason: "BAD_IMAGE" }; + } + await cloud.openapi.security.imgSecCheck({ + media: { contentType, value: buffer }, + }); + return { risk: false }; + } catch (e) { + if (extractErrCode(e) === 87014) return { risk: true, verdict: "risky" }; + return { risk: false, skipped: true, errCode: extractErrCode(e) }; + } finally { + try { + await cloud.deleteFile({ fileList: [fileID] }); + } catch (e) { + // 清理失败不影响检测结果 + } + } +} + +exports.main = async (event) => { + const { OPENID } = cloud.getWXContext(); + if (!OPENID) return { risk: false, skipped: true, reason: "NO_OPENID" }; + const type = event && event.type; + if (type === "checkMessage") { + return checkOneMessage(event.content, event.scene, OPENID); + } + if (type === "checkMessages") { + const items = Array.isArray(event.items) ? event.items.slice(0, MAX_BATCH) : []; + const results = await Promise.all(items.map((it) => checkOneMessage(it && it.value, it && it.scene, OPENID))); + return { results }; + } + if (type === "checkImage") { + return checkImage(event.fileID); + } + return { risk: false, skipped: true, reason: "UNKNOWN_TYPE" }; +}; diff --git a/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json b/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..26d0d9d6e69b043888fa78b3e1a003670970cf1c --- /dev/null +++ b/BeadQR/cloudfunctions/quickstartFunctions/package-lock.json @@ -0,0 +1,1157 @@ +{ + "name": "quickstartFunctions", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "quickstartFunctions", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "wx-server-sdk": "~4.0.2" + } + }, + "node_modules/@cloudbase/adapter-interface": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@cloudbase/adapter-interface/-/adapter-interface-0.7.1.tgz", + "integrity": "sha512-BjHTAvIMSLxbp26SeT6qLiSSBygz3kAVno/PMwwBwjgzVkVUvuZGVcvf1mhHxY/BjdRGLrd6X60OgvymV/aAUw==", + "license": "ISC" + }, + "node_modules/@cloudbase/adapter-wx_mp": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@cloudbase/adapter-wx_mp/-/adapter-wx_mp-1.3.1.tgz", + "integrity": "sha512-rS0N1aD1a0zc421ds17/HzM4RChQ6T2ixMz87bhPJ1yveqJt/KUqYxiXJDWhZ803wwTmJs8aqNkgugsNusNhbg==", + "license": "ISC", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.0", + "web-streams-polyfill": "^4.0.0" + } + }, + "node_modules/@cloudbase/ai": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@cloudbase/ai/-/ai-2.30.0.tgz", + "integrity": "sha512-RtBux47omXWcMhiiDDVJ96d2JlqnQ/bzKC6GG3ROgupYG3ReQyv1IssCpg5cnzmZ+l651IqdOpt27VTuO3kEGQ==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/types": "2.30.0", + "@cloudbase/utilities": "2.30.0", + "@mattiasbuelens/web-streams-adapter": "^0.1.0", + "text-encoding-shim": "^1.0.5", + "web-streams-polyfill": "^4.0.0" + } + }, + "node_modules/@cloudbase/ai/node_modules/@cloudbase/types": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz", + "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1" + } + }, + "node_modules/@cloudbase/app": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@cloudbase/app/-/app-2.32.0.tgz", + "integrity": "sha512-78+eSo8OLO4iUiifdynZo4dK4wNlyaz5x9423muSibyl4FLwt5/Xt0t9s7StFmGaBdvB75UYM9dGWxOdIFRzaw==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1", + "@cloudbase/adapter-wx_mp": "^1.3.1", + "@cloudbase/types": "2.32.0", + "@cloudbase/utilities": "2.32.0" + }, + "peerDependencies": { + "@cloudbase/signature-nodejs": ">=1.0.0", + "jsonwebtoken": ">=8.0.0", + "ws": "^8.18.0" + }, + "peerDependenciesMeta": { + "@cloudbase/signature-nodejs": { + "optional": true + }, + "jsonwebtoken": { + "optional": true + }, + "ws": { + "optional": true + } + } + }, + "node_modules/@cloudbase/app/node_modules/@cloudbase/utilities": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.32.0.tgz", + "integrity": "sha512-vYrr1aXOpU+/IrkHMSTxi/k2d4N8raNu+E3IrKtWt4w7Y0Ke3DLvCHeUJcuWECvQBVhzmg1ert4DcXVUymSKlg==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1", + "@cloudbase/types": "2.32.0", + "jwt-decode": "^3.1.2" + } + }, + "node_modules/@cloudbase/database": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@cloudbase/database/-/database-1.4.3.tgz", + "integrity": "sha512-JzmdsGjy9LwzSQQ0Fv4OlNHNK61BRXdJSembTpy4408AQA3q2Ip3N5eB3v9OpAlFMvP/6MGmOQ/ZuUPKfJddJA==", + "license": "ISC", + "dependencies": { + "bson": "^4.0.3", + "lodash.clonedeep": "4.5.0", + "lodash.set": "4.3.2", + "lodash.unset": "4.5.2" + } + }, + "node_modules/@cloudbase/node-sdk": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/@cloudbase/node-sdk/-/node-sdk-3.17.2.tgz", + "integrity": "sha512-74ekvRsdmeGch9hFHkQnjHPTEfk8k09iQFvI1aAo48ZsbLqK3czqj/3GoVpSkw5+eYe05tZV13Ct/kWiV5tuGg==", + "license": "MIT", + "dependencies": { + "@cloudbase/ai": "^2.23.0", + "@cloudbase/app": "^2.23.0", + "@cloudbase/database": "1.4.3", + "@cloudbase/signature-nodejs": "2.2.0", + "@cloudbase/types": "^2.23.0", + "@cloudbase/wx-cloud-client-sdk": "1.7.1", + "agentkeepalive": "^4.3.0", + "axios": "0.27.2", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "jsonwebtoken": "^9.0.2", + "retry": "^0.13.1", + "web-streams-polyfill": "^4.2.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cloudbase/signature-nodejs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cloudbase/signature-nodejs/-/signature-nodejs-2.2.0.tgz", + "integrity": "sha512-2y2rieOpJNnBA2MW7sLJKek3iFOjgr8TNicw5qjBkcdVczLR0cFzxMhip75NLCwarFTIAONE3oeNxoQ19eVHcA==", + "dependencies": { + "@types/clone": "^2.1.0", + "clone": "^2.1.2", + "is-stream": "^2.0.0", + "url": "^0.11.0" + } + }, + "node_modules/@cloudbase/types": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.32.0.tgz", + "integrity": "sha512-HPsMEHHt5SAyD3tpv/C/v2nYudSawE48ixjK5Y9w3R/82auJag0AUsZGfCExgdPa4Zud9OvGx5127ssEXLgadw==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1" + } + }, + "node_modules/@cloudbase/utilities": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@cloudbase/utilities/-/utilities-2.30.0.tgz", + "integrity": "sha512-52DfQH5qgfOtrwj26KCGw6cz0pUjuJludJ7fccUKCzBtn/QwPudwtXo7gOUTtKKDubW3+VUCjjBm7UiDwFu+ag==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1", + "@cloudbase/types": "2.30.0", + "jwt-decode": "^3.1.2" + } + }, + "node_modules/@cloudbase/utilities/node_modules/@cloudbase/types": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/@cloudbase/types/-/types-2.30.0.tgz", + "integrity": "sha512-tVapfLLDA1MlmG2OwC5eq5eSra9l3EgpeJnx1kNR4CGh8zQRrVvFRyaRLo4BrzY0p/5xqX3iJliRmcASF7Ip7g==", + "license": "Apache-2.0", + "dependencies": { + "@cloudbase/adapter-interface": "^0.7.1" + } + }, + "node_modules/@cloudbase/wx-cloud-client-sdk": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@cloudbase/wx-cloud-client-sdk/-/wx-cloud-client-sdk-1.7.1.tgz", + "integrity": "sha512-NzLOoYDAeoRh2lRjCtvr6yXu4KPVHqZtPxyv70e+3b2+dZIDE5S5gtU8daE7xS5u4TwPqKB/E2vmgRTYOsCS9w==", + "dependencies": { + "core-js-pure": "^3.45.0" + } + }, + "node_modules/@mattiasbuelens/web-streams-adapter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz", + "integrity": "sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@tootallnate/once": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", + "integrity": "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/clone": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-2.1.4.tgz", + "integrity": "sha512-NKRWaEGaVGVLnGLB2GazvDaZnyweW9FJLLFL5LhywGJB3aqGMT9R/EUoJoSRP4nzofYnZysuDmrEJtJdAqUOtQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.3.0.tgz", + "integrity": "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bson": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", + "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/core-js-pure": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.50.0.tgz", + "integrity": "sha512-6GP3Pxz4IKyWjAfa747vIu/jilB5z29JWROLqH/b+pXVcpgh6tM06ZIBwSuglgVqzDYURhOK6oEzTrG0bCHitA==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==", + "license": "MIT" + }, + "node_modules/lodash.unset": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.5.2.tgz", + "integrity": "sha512-bwKX88k2JhCV9D1vtE8+naDKlLiGrSmf8zi/Y9ivFHwbmRfA8RxS/aVJ+sIht2XOwqoNr4xUPUkGZpc1sHFEKg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-encoding-shim": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/text-encoding-shim/-/text-encoding-shim-1.0.5.tgz", + "integrity": "sha512-H7yYW+jRn4yhu60ygZ2f/eMhXPITRt4QSUTKzLm+eCaDsdX8avmgWpmtmHAzesjBVUTAypz9odu5RKUjX5HNYA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.3.0.tgz", + "integrity": "sha512-/Gnggvj9oSrEvJbDyyPtAnxBt5fGQM2iWOKQNu7ie1OxDgK40iZpyV3TKaRiEzVj1oA1UxKnEy9XPXh6PW3eVw==", + "license": "MIT", + "workspaces": [ + "test/benchmark-test", + "test/rollup-test", + "test/webpack-test" + ], + "engines": { + "node": ">= 8" + } + }, + "node_modules/wx-server-sdk": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/wx-server-sdk/-/wx-server-sdk-4.0.2.tgz", + "integrity": "sha512-dgpgWxYvOFWnQdGOb78sU+uQGELoEQSKzmJ7m8Y6eTCJKsfkdIHLMhRrdN5S5oDfAqr8Pc83jZKQeNRMZASIcg==", + "license": "MIT", + "dependencies": { + "@cloudbase/node-sdk": "3.17.2", + "json-bigint": "^1.0.0", + "protobufjs": "^7.2.4", + "tslib": "^1.9.3" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/BeadQR/cloudfunctions/quickstartFunctions/package.json b/BeadQR/cloudfunctions/quickstartFunctions/package.json new file mode 100644 index 0000000000000000000000000000000000000000..63727cda9c2e7d038ad78f36ae111268eadd9258 --- /dev/null +++ b/BeadQR/cloudfunctions/quickstartFunctions/package.json @@ -0,0 +1,14 @@ +{ + "name": "quickstartFunctions", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "wx-server-sdk": "~4.0.2" + } +} diff --git a/BeadQR/docs/promo/douwei-1080-cover.png b/BeadQR/docs/promo/douwei-1080-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..f08e99fd0f86d17f8467e6b059637e3b989fc1ec --- /dev/null +++ b/BeadQR/docs/promo/douwei-1080-cover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aeac46fce4028cbfc2d7bae0dad427ed5f67262582ad411a00ccb2db65f8f03b +size 319823 diff --git a/BeadQR/docs/promo/douwei-1080.mp4 b/BeadQR/docs/promo/douwei-1080.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..8ad3a7bd6f7099c1305b7b42b367c9c7db85cdbb --- /dev/null +++ b/BeadQR/docs/promo/douwei-1080.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ba1505c4ce4aa36316841c497dc96b8997bac534bb534426c23ca96ce87dcc +size 3693122 diff --git a/BeadQR/docs/promo/douwei-720-cover.png b/BeadQR/docs/promo/douwei-720-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..ccaf575a9b83c1b8b0eb3255f0e3a37673424a67 --- /dev/null +++ b/BeadQR/docs/promo/douwei-720-cover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdb68c450700c1e65492e708440a29d07c9288606ca0ad7b464133052855fb2a +size 195403 diff --git a/BeadQR/docs/promo/douwei-720.mp4 b/BeadQR/docs/promo/douwei-720.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..b95f5525b899e8a0144b1b7da6234149348de007 --- /dev/null +++ b/BeadQR/docs/promo/douwei-720.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:626ea04f449f33fe29c4ec6c4aa94a2db88600cababab4f5a11b74155b4eee4e +size 1650037 diff --git a/BeadQR/docs/video-script.md b/BeadQR/docs/video-script.md new file mode 100644 index 0000000000000000000000000000000000000000..bc42e2b8af7f963da3dc1c90067b2aa461061fcc --- /dev/null +++ b/BeadQR/docs/video-script.md @@ -0,0 +1,116 @@ +# 豆维 · 视频文案与分镜脚本 + +> 目标:用一个短视频介绍「豆维 · 可扫码的拼豆」小程序,让观众理解它是什么、为什么有趣、怎么做出来。 +> 结构:提出想法 → 实现想法 → 制作例子 → 完成例子。 + +--- + +## 一、视频定位与调性 + +| 维度 | 建议 | +|------|------| +| 时长 | 主版本 60–90 秒(B站 / 朋友圈 / 公众号),另附 15–30 秒短视频版 | +| 调性 | 温暖手作 × 科技惊喜,不是冷冰冰的产品演示 | +| 核心情绪 | 「拼豆不止是一幅画,它是一封能被读到的信」 | +| 目标观众 | 拼豆爱好者、手工创作者、喜欢送独特礼物的人 | +| 画面风格 | 暖色调,木桌面,自然光;手机屏幕录制与实物特写交替出现 | + +--- + +## 二、核心文案(Slogan / 一句话介绍) + +以下任选一条作为标题或结尾定版字: + +1. **豆维 · 可扫码的拼豆** +2. **每一颗珠子,都藏着一句话。** +3. **拼出来的二维码,扫出来的心意。** +4. **把想说的话,拼进一幅画里。** +5. **看得见,也读得到。** + +推荐主标题:**看得见,也读得到。——豆维,可扫码的拼豆** + +--- + +## 三、主版本分镜脚本(60–90 秒) + +> 旁白以第一人称「我」叙述,语气像朋友分享,不念广告稿。 +> 「画面」列标注了你需要准备的素材类型;如果你已有类似镜头,直接对应即可。 + +| 幕 | 时间 | 画面 | 旁白 / 字幕 | 剪辑提示 | +|----|------|------|-------------|----------| +| **第一幕:提出想法** | 0:00–0:08 | 桌面上摆着几幅拼豆作品,阳光照进来。手拿起其中一幅,翻到背面,又翻回来。 | **旁白:** 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画? | 慢推镜头,配温暖的钢琴或指弹吉他。最后一帧画面停在作品特写。 | +| | 0:08–0:14 | 手拿起手机对准拼豆作品「扫一扫」——屏幕弹出一条消息或链接。 | **旁白:** 能不能扫一下,就能看到我想说的话? | 这是全片第一处「惊喜时刻」,音效清脆,屏幕内容要清晰可见。 | +| **第二幕:实现想法** | 0:14–0:24 | 切到手机屏幕录屏:打开豆维小程序,输入一段文字。 | **旁白:** 所以我做了「豆维」——一个可扫码的拼豆图纸生成器。输入一段话,或者一个链接。 | 屏幕录制加速 2–3 倍,打字过程快放。 | +| | 0:24–0:34 | 录屏:导入一张照片,裁切、去背景。 | **旁白:** 再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。 | 展示裁切弹窗操作,去背景效果前后对比。 | +| | 0:34–0:44 | 录屏:拖动滑块调整二维码大小和位置,切换「丰富 / 平衡 / 稳定」模式。 | **旁白:** 二维码可以放大缩小、移到任何位置。颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。 | 滑块操作用慢动作展示,让观众看到图案实时变化。 | +| **第三幕:制作例子** | 0:44–0:52 | 导出的图纸打印出来,摆在桌上。开始一颗一颗摆珠子。 | **旁白:** 图纸保存下来,打印出来,就可以开始拼了。 | 延时摄影或快剪,展示摆珠过程,保留几秒真实速度的特写。 | +| | 0:52–0:58 | 熨斗烫过,拼豆板冷却,掀开,成品完成。 | **旁白:** 熨好,冷却,掀开—— | 掀开那一刻音效「叮」,画面干净利落。 | +| **第四幕:完成例子** | 0:58–1:10 | 手持成品对镜头展示,然后用手机扫描。屏幕上弹出一开始输入的那段话。 | **旁白:** 它不只是一幅画。你把手机对准它——就能读到里面藏着的话。 | 这是全片情绪最高点。扫描过程用真实速度,屏幕内容要清楚。 | +| | 1:10–1:20 | 多个成品排列展示(不同图案、不同颜色),最后画面淡出到品牌 Logo 和小程序码。 | **旁白:** 豆维,可扫码的拼豆。看得见,也读得到。 | 结尾定格 3 秒,放小程序码,方便观众扫码体验。 | + +--- + +## 四、15 秒短视频版(抖音 / 视频号快闪) + +| 时间 | 画面 | 字幕(无旁白,纯字幕流) | +|------|------|--------------------------| +| 0–3s | 手拿起拼豆作品 → 手机扫描 → 屏幕弹出消息 | 拼豆里,藏着一条消息 | +| 3–8s | 快剪:输入文字 → 导入照片 → 图纸生成 → 摆珠 | 豆维:输入一句话,选一张图,生成拼豆图纸 | +| 8–13s | 熨烫 → 掀开 → 成品再扫一次 | 拼好,烫平,扫一下—— | +| 13–15s | 品牌 Logo + 小程序码 | 消息就出来了。豆维 · 可扫码的拼豆 | + +> 快闪版不需要旁白,靠音乐节奏 + 字幕推进。音乐选节奏感强的电子或 Lo-Fi。 + +--- + +## 五、30 秒版本(朋友圈 / 公众号内嵌) + +节奏比 15 秒版稍慢,保留 4 个关键节点: + +1. **悬念**(5s):拿起作品 → 扫码 → 弹出内容 +2. **怎么做**(10s):小程序操作快剪(输入 → 导图 → 调整 → 导出) +3. **手作过程**(8s):摆珠延时 + 熨烫 + 掀开 +4. **结尾**(7s):成品特写 + 再扫一次 + 定版 + +配乐:轻快指弹吉他或 Lo-Fi,音量压低,让旁白清晰。 + +--- + +## 六、素材清单 + +拍摄前对照这份清单,确认已拍到以下镜头(√ 为已有,× 为待补): + +| 素材 | 用途 | 状态 | +|------|------|------| +| 多幅完成的拼豆作品摆拍(正面 / 斜角 / 背面) | 开头悬念、结尾展示 | ☐ | +| 手机扫描拼豆作品并弹出内容的实拍 | 全片核心「惊喜时刻」,需拍 2–3 次 | ☐ | +| 豆维小程序屏幕录制:输入文字、导入图片、裁切、调 QR 位置、切模式 | 第二幕 | ☐ | +| 图纸导出 / 打印的镜头 | 第三幕开头 | ☐ | +| 摆珠延时或快剪素材 | 第三幕 | ☐ | +| 熨斗烫拼豆 + 掀开的特写 | 第三幕高潮 | ☐ | +| 成品多角度特写(自然光 / 桌面) | 结尾 | ☐ | + +--- + +## 七、剪辑建议 + +- **转场**:幕与幕之间用干净的硬切或快速模糊,不要花哨转场。产品视频要显得克制。 +- **音乐**:主版本选温暖的原声吉他或钢琴;快闪版选节奏感强的 Lo-Fi / 电子。全片一首歌,不要中途换。 +- **节奏**:第三幕(摆珠)可以稍慢,让观众感受手作质感;第二幕(录屏)加速推进。 +- **字幕**:全程加简体中文字幕,关键词用品牌色 `#416d62`(深绿)高亮。 +- **结尾**:小程序码至少停留 3 秒,让观众来得及扫码。 + +--- + +## 八、旁白全文(主版本,可直接朗读) + +> 我做了很多拼豆作品,挂在墙上很好看。但我总在想——它能不能,不止是一幅画?能不能扫一下,就能看到我想说的话? +> +> 所以我做了「豆维」。输入一段话,或者一个链接,再选一张你喜欢的照片。它会把照片和二维码,合成一幅完整的拼豆图纸。二维码可以放大缩小、移到任何位置,颜色会自动匹配 221 色的拼豆色库,还能选择更注重美观、还是更保证能扫出来。 +> +> 图纸保存下来,打印出来,就可以开始拼了。熨好,冷却,掀开—— +> +> 它不只是一幅画。你把手机对准它,就能读到里面藏着的话。 +> +> 豆维,可扫码的拼豆。看得见,也读得到。 + diff --git "a/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md" "b/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md" new file mode 100644 index 0000000000000000000000000000000000000000..32154ecf4a6048b2035090856a2d3e244d5fcf13 --- /dev/null +++ "b/BeadQR/docs/\345\256\241\346\240\270\351\251\263\345\233\236\345\244\204\347\220\206-\346\226\207\346\241\210\344\270\216\346\255\245\351\252\244.md" @@ -0,0 +1,58 @@ +# 审核驳回处理:深度合成误判 — 文案与步骤 + +> 驳回原因(2026-09-30):小程序服务内容涉及深度合成技术(如 AI 问答、AI 换脸视频、AI 绘画等),属个人主体尚未开放服务类目,建议申请企业主体类型小程序。 + +## 结论 + +代码里没有任何 AI 功能:文字是标准二维码编码;"照片转图纸"是确定性本地图像处理(按网格取平均色 → 映射到 221 种固定色号),不用任何模型、不生成新图像。被判"深度合成"大概率是**后台简介 / 服务类目 / 服务内容声明的表述**或审核员看到"照片→生成图纸"产生的误判。 + +## 路径 A(推荐,最快):修正表述后重新提审 + +### 1. 检查后台三处(mp.weixin.qq.com) + +| 位置 | 检查项 | +| --- | --- | +| 服务类目 | 改为 **工具 - 图片处理**(个人主体可选);删除任何 AI / 深度合成相关类目 | +| 服务内容声明 / 提审问卷 | 凡问"是否提供 AI 生成内容、AI 问答、AI 绘画、深度合成"一律选 **否** | +| 小程序简介 | 换成下方"新简介",去掉"生成器 / 一键生成 / 智能"等字眼 | + +### 2. 新简介(约 90 字,上限 120) + +``` +手工拼豆图纸工具:输入一句话或选一张照片,按 MARD 标准色号排成拼豆图纸,拼好微信一扫就能读出内容。支持 A4 矢量 PDF 与图片导出、用量清单。照片仅在本地做取色处理,并经微信内容安全检测。 +``` + +### 3. 版本描述 / 审核备注(重新提审时填写) + +``` +本小程序为拼豆手工图纸工具,不使用任何人工智能或深度合成技术,无 AIGC 功能: +1. "照片转图纸"为确定性本地图像处理:将用户上传照片按网格取平均色,再映射到固定的 221 种拼豆标准色号(色彩量化 + 最近邻查表),不生成、不合成任何新图像内容,不涉及人脸或生物特征编辑; +2. 文字功能是将文本编码为标准二维码点阵,非内容生成; +3. 所有处理均在用户手机本地完成,且已接入微信内容安全接口(security.msgSecCheck / imgSecCheck),检测未通过时禁止生成与导出(fail-closed)。 +请按"工具-图片处理"类目审核。 +``` + +## 路径 B:申诉 + +若不想改简介,可直接对本次审核结果发起申诉(审核结果页 → 申诉),粘贴下文: + +``` +本小程序为拼豆手工图纸工具,不涉及深度合成技术: +(1) 不使用 AI 模型 / 神经网络,无 AI 问答、AI 绘画、AI 换脸等 AIGC 功能; +(2) 照片功能仅做确定性色彩量化:按网格取平均色并映射到 221 种固定标准色号,属于常规图像处理,不生成新图像、不做人脸或生物特征编辑; +(3) 文字功能为标准二维码编码; +(4) 已接入微信内容安全检测(msgSecCheck / imgSecCheck),检测未通过不能生成或导出。 +恳请复核,建议按"工具-图片处理"类目审核。 +``` + +申诉未通过时:修改简介与类目后按路径 A 重新提审(改了表述基本都能过)。 + +## 路径 C:仅当二次被拒或想保留"生成"叙事 + +1. **隐藏照片功能**(只保留文字 → 二维码图纸,纯编码无争议)——我可以直接改代码; +2. **升级主体**:注册个体工商户 / 企业 → 主体变更 → 申请深度合成相关类目 + 算法备案。仅在以后真要做 AI 功能时才值得,当前工具用不到。 + +## 顺手可做的降风险小改动(可选) + +- 首页徽标 `MARD 拼豆图纸生成器` → `MARD 拼豆图纸工具` +- 分享标题 `豆维 · 可扫码的拼豆图纸生成器` → `豆维 · 可扫码的拼豆图纸` diff --git a/BeadQR/miniprogram/app.js b/BeadQR/miniprogram/app.js new file mode 100644 index 0000000000000000000000000000000000000000..77c44453096b7460e86e268a5079a6b79c45d25d --- /dev/null +++ b/BeadQR/miniprogram/app.js @@ -0,0 +1,14 @@ +// 豆维 · 可扫码的拼豆 +App({ + onLaunch() { + if (!wx.cloud) { + console.error('当前基础库不支持云开发,内容安全检测不可用') + return + } + wx.cloud.init({ + env: 'cloud1-d6gthxado7f6c9d14', + traceUser: true, + }) + }, + globalData: {}, +}) diff --git a/BeadQR/miniprogram/app.json b/BeadQR/miniprogram/app.json new file mode 100644 index 0000000000000000000000000000000000000000..c65531905dd809351665a2e54b7406068e24fe14 --- /dev/null +++ b/BeadQR/miniprogram/app.json @@ -0,0 +1,19 @@ +{ + "pages": [ + "pages/index/index" + ], + "window": { + "backgroundColor": "#f6f5f1", + "backgroundTextStyle": "dark", + "navigationBarBackgroundColor": "#f6f5f1", + "navigationBarTitleText": "豆维 · 可扫码的拼豆", + "navigationBarTextStyle": "black" + }, + "usingComponents": { + "privacy-popup": "/components/privacy-popup/index" + }, + "__usePrivacyCheck__": true, + "sitemapLocation": "sitemap.json", + "style": "v2", + "lazyCodeLoading": "requiredComponents" +} diff --git a/BeadQR/miniprogram/app.wxss b/BeadQR/miniprogram/app.wxss new file mode 100644 index 0000000000000000000000000000000000000000..284c483d7c36686985f29a1abfdb8de7b83aead0 --- /dev/null +++ b/BeadQR/miniprogram/app.wxss @@ -0,0 +1,37 @@ +/* 豆维 · 设计变量 */ +page { + --ink: #16211f; + --ink-2: #3a4643; + --muted: #6f7a76; + --faint: #a4aca8; + --bg: #f6f5f1; + --card: #ffffff; + --line: #ebe8e1; + --brand: #0f766e; + --brand-2: #14b8a6; + --brand-soft: #e6f4f1; + --brand-deep: #0f3d3a; + --accent: #ff7a45; + --accent-soft: #fff1ea; + --danger: #e5484d; + --danger-soft: #fdecec; + --ok: #12a150; + --ok-soft: #e7f6ed; + --warn: #c77700; + --warn-soft: #fff5e0; + --r-s: 16rpx; + --r-m: 24rpx; + --r-l: 36rpx; + --shadow: 0 8rpx 32rpx rgba(22, 33, 31, 0.06); + + background: var(--bg); + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; + font-size: 28rpx; + line-height: 1.55; +} + +view, text, textarea, input, button, scroll-view, canvas, image { box-sizing: border-box; } + +button { padding: 0; margin: 0; background: none; border: 0; border-radius: 0; line-height: inherit; font-size: inherit; color: inherit; text-align: center; } +button::after { border: 0; } diff --git a/BeadQR/miniprogram/assets/icon.png b/BeadQR/miniprogram/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7d10be4c5227e54d35f75210df674fac3218ff05 Binary files /dev/null and b/BeadQR/miniprogram/assets/icon.png differ diff --git a/BeadQR/miniprogram/assets/miniprogram-code.png b/BeadQR/miniprogram/assets/miniprogram-code.png new file mode 100644 index 0000000000000000000000000000000000000000..5a295d47d3f983270485c2eefceb296ce2729810 Binary files /dev/null and b/BeadQR/miniprogram/assets/miniprogram-code.png differ diff --git a/BeadQR/miniprogram/assets/shield.svg b/BeadQR/miniprogram/assets/shield.svg new file mode 100644 index 0000000000000000000000000000000000000000..50456c42007effc88842ab4b474c62b6b64af781 --- /dev/null +++ b/BeadQR/miniprogram/assets/shield.svg @@ -0,0 +1 @@ + diff --git a/BeadQR/miniprogram/components/privacy-popup/index.js b/BeadQR/miniprogram/components/privacy-popup/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4e5d889e8cec37f4235f72742b66926a17825115 --- /dev/null +++ b/BeadQR/miniprogram/components/privacy-popup/index.js @@ -0,0 +1,40 @@ +// 隐私保护指引弹窗:用户首次调用相册/相机/保存到相册等隐私接口时由微信触发 +Component({ + data: { visible: false, contractName: '《豆维隐私保护指引》' }, + + lifetimes: { + attached() { + if (!wx.onNeedPrivacyAuthorization) return + this._listener = (resolve) => { + this._resolve = resolve + this.setData({ visible: true }) + } + wx.onNeedPrivacyAuthorization(this._listener) + if (wx.getPrivacySetting) { + wx.getPrivacySetting({ + success: (res) => { + if (res.privacyContractName) this.setData({ contractName: res.privacyContractName }) + }, + }) + } + }, + }, + + methods: { + openContract() { + if (wx.openPrivacyContract) wx.openPrivacyContract({}) + }, + handleAgree() { + if (this._resolve) this._resolve({ buttonId: 'privacy-agree-btn', event: 'agree' }) + this._resolve = null + this.setData({ visible: false }) + }, + handleDisagree() { + if (this._resolve) this._resolve({ event: 'disagree' }) + this._resolve = null + this.setData({ visible: false }) + wx.showToast({ title: '未同意将无法选择照片或保存图纸', icon: 'none' }) + }, + noop() {}, + }, +}) diff --git a/BeadQR/miniprogram/components/privacy-popup/index.json b/BeadQR/miniprogram/components/privacy-popup/index.json new file mode 100644 index 0000000000000000000000000000000000000000..a89ef4dbeefa01f5cd7971973aa4db6498d139f7 --- /dev/null +++ b/BeadQR/miniprogram/components/privacy-popup/index.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/BeadQR/miniprogram/components/privacy-popup/index.wxml b/BeadQR/miniprogram/components/privacy-popup/index.wxml new file mode 100644 index 0000000000000000000000000000000000000000..52eb9a5d528a586383b94527a8a6814bea849ea0 --- /dev/null +++ b/BeadQR/miniprogram/components/privacy-popup/index.wxml @@ -0,0 +1,13 @@ + + + 隐私保护提示 + + 为了帮你把照片做成拼豆图纸,豆维需要读取你选择的照片、保存图纸到相册。照片仅在本机处理,送检的压缩副本会在内容安全检测后立即删除。使用前请阅读 + {{contractName}}。 + + + + + + + diff --git a/BeadQR/miniprogram/components/privacy-popup/index.wxss b/BeadQR/miniprogram/components/privacy-popup/index.wxss new file mode 100644 index 0000000000000000000000000000000000000000..0310cb844f9df69a543c3477b2353f044a03cd4a --- /dev/null +++ b/BeadQR/miniprogram/components/privacy-popup/index.wxss @@ -0,0 +1,10 @@ +.mask { position: fixed; inset: 0; z-index: 999; background: rgba(15, 23, 22, 0.55); display: flex; align-items: flex-end; } +.sheet { width: 100%; background: #fff; border-radius: 40rpx 40rpx 0 0; padding: 48rpx 40rpx calc(40rpx + env(safe-area-inset-bottom)); box-sizing: border-box; } +.title { font-size: 36rpx; font-weight: 700; color: #1d2b2a; margin-bottom: 20rpx; } +.desc { font-size: 28rpx; line-height: 1.7; color: #55615c; } +.link { color: #0f766e; font-weight: 600; } +.actions { display: flex; gap: 20rpx; margin-top: 40rpx; } +.btn { flex: 1; height: 92rpx; line-height: 92rpx; border-radius: 999rpx; font-size: 30rpx; font-weight: 600; margin: 0 !important; padding: 0 !important; width: auto !important; } +.btn::after { border: 0; } +.ghost { background: #f1f3f2; color: #55615c; } +.primary { background: #0f766e; color: #fff; } diff --git a/BeadQR/miniprogram/pages/index/index.js b/BeadQR/miniprogram/pages/index/index.js new file mode 100644 index 0000000000000000000000000000000000000000..59dd120e7362c0a310d02be21431a930cfd65ba3 --- /dev/null +++ b/BeadQR/miniprogram/pages/index/index.js @@ -0,0 +1,1073 @@ +// 豆维主页面 +// 大数据(色格、图片、画布节点)一律挂在 this._ 上,不进 setData,保证滑动/输入流畅 +const { MATCH_MODES, PRESETS, preparePalette } = require('../../utils/palette') +const { makeQr, createPlacement, SAMPLE_TEXT } = require('../../utils/qr') +const { getImageDimensions, drawCropped, sampleImage, removeSimpleBackground } = require('../../utils/imaging') +const { computeArtwork } = require('../../utils/engine') +const { selfCheck } = require('../../utils/scancheck') +const { renderPreview, patternPageLayout, drawPatternPage, buildInfoPages, drawWatermark } = require('../../utils/pattern') +const { PdfDoc, planPatternPages, addPatternPages, addImagePage } = require('../../utils/pdf') +const security = require('../../utils/security') +const { createIntro } = require('../../utils/intro') + +const LEVELS = [ + { id: 'M', label: 'M', hint: '格子少' }, + { id: 'Q', label: 'Q', hint: '均衡' }, + { id: 'H', label: 'H', hint: '最耐磨' }, +] +const QUIET_OPTIONS = [ + { id: 0, label: '无' }, + { id: 1, label: '1 格' }, + { id: 2, label: '2 格' }, +] +const BEAD_STYLES = [ + { id: 'square', label: '方豆' }, + { id: 'round', label: '圆豆' }, + { id: 'hollow', label: '中空豆' }, +] +const GRID_COLORS = ['#0f766e', '#16211f', '#e5484d', '#3b82f6', '#a855f7'] +const POSITIONS = [0, 50, 100] +const MAX_CANVAS_SIDE = 8000 +const MAX_CANVAS_PIXELS = 16000000 +const CROP_SIZE = 640 +const FILE_PREFIX = 'douwei-' + +function fitScale(w, h) { + return Math.min(1, MAX_CANVAS_SIDE / w, MAX_CANVAS_SIDE / h, Math.sqrt(MAX_CANVAS_PIXELS / (w * h))) +} + +function formatNumber(n) { + return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',') +} + +Page({ + data: { + // 导航 + statusBarHeight: 20, + navHeight: 44, + navSolid: false, + // 1. 内容 + text: '', + textStatus: 'idle', // idle | checking | pass | risky | error + scanImportLoading: false, + // 2. 照片 + hasImage: false, + imageThumb: '', + imageMeta: '', + removeBackground: false, + bgTolerance: 22, + // 3. 效果 + qrScale: 100, + posX: 50, + posY: 50, + positions: POSITIONS, + modeId: 'balanced', + matchModes: MATCH_MODES, + advancedOpen: false, + fineTuneOpen: false, + presets: PRESETS.map((p) => ({ id: p.id, short: p.short, detail: p.detail })), + paletteId: '221', + coherence: 40, + cleanup: true, + levels: LEVELS, + level: 'H', + quietOptions: QUIET_OPTIONS, + quiet: 1, + // 预览与统计 + generating: false, + isSample: true, + showIntro: true, // 无内容时播放动态演示,写下内容后切回真实预览 + textFocus: false, + gridText: '—', + beadsText: '—', + colorsText: '—', + qrGridText: '', + scanState: 'idle', // idle | checking | ok | fail + usage: [], + usageMore: 0, + usageExpanded: false, + usageAll: [], + // 裁切 + cropVisible: false, + cropCheck: 'idle', // checking | pass | risky | error + draftFocusX: 50, + draftFocusY: 50, + draftZoom: 100, + draftFlipH: false, + draftFlipV: false, + draftRemoveBg: false, + draftBgTolerance: 22, + // 导出 + exportVisible: false, + exportFormat: 'pdf', + exportLayout: 'combined', + exportQuality: 'hd', + beadStyles: BEAD_STYLES, + beadStyle: 'square', + gridEnabled: true, + gridInterval: 10, + gridColors: GRID_COLORS, + gridColor: '#0f766e', + watermark: '', + watermarkStatus: 'idle', + exporting: false, + exportStep: '', + exportResult: null, // { kind, files: [{ name, path }] } + }, + + // —— 生命周期 —— + + onLoad() { + const win = wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() + let navHeight = 44 + try { + const capsule = wx.getMenuButtonBoundingClientRect() + if (capsule && capsule.top) navHeight = (capsule.top - win.statusBarHeight) * 2 + capsule.height + } catch (e) { /* 使用默认高度 */ } + this._dpr = Math.min(3, win.pixelRatio || 2) + this._timers = {} + this._nodes = {} + this._token = 0 + this.setData({ statusBarHeight: win.statusBarHeight || 20, navHeight }) + }, + + onReady() { + wx.createSelectorQuery().in(this) + .select('#preview').fields({ node: true, size: true }) + .select('#work').fields({ node: true }) + .select('#security').fields({ node: true }) + .exec((res) => { + this._nodes.preview = res[0] && res[0].node + this._previewCss = (res[0] && res[0].width) || 300 + this._nodes.work = res[1] && res[1].node + this._nodes.security = res[2] && res[2].node + this.loadMpCode() + this.schedule(0) + }) + }, + + onUnload() { + Object.keys(this._timers).forEach((k) => clearTimeout(this._timers[k])) + this.stopIntro() + }, + + onShow() { + this._hidden = false + if (this._nodes && this._nodes.preview) this.syncIntro() + }, + + onHide() { + this._hidden = true + this.pauseIntro() + }, + + onPageScroll(e) { + const solid = e.scrollTop > 40 + if (solid !== this.data.navSolid) this.setData({ navSolid: solid }) + // 演示区滚出屏幕后暂停动画,省电 + const away = e.scrollTop > 650 + if (away !== this._scrolledAway) { + this._scrolledAway = away + this.syncIntro() + } + }, + + // 分享的是小程序页面卡片,不携带用户内容 + onShareAppMessage() { + return { title: '把一句话藏进拼豆里,微信一扫就能读出来', path: '/pages/index/index' } + }, + onShareTimeline() { + return { title: '豆维 · 可扫码的拼豆图纸生成器' } + }, + + loadMpCode() { + if (!this._nodes.work) return + const image = this._nodes.work.createImage() + image.onload = () => { this._mpCode = image } + image.onerror = () => { this._mpCode = null } + image.src = '/assets/miniprogram-code.png' + }, + + debounce(key, fn, wait) { + clearTimeout(this._timers[key]) + this._timers[key] = setTimeout(fn, wait) + }, + + noop() {}, + + openPrivacy() { + if (wx.openPrivacyContract) { + wx.openPrivacyContract({ fail: () => wx.showToast({ title: '请在微信内查看隐私保护指引', icon: 'none' }) }) + } + }, + + showRules() { + wx.showModal({ + title: '内容规范', + content: '二维码内容、水印和照片都会经过微信内容安全检测。请勿输入或上传违法违规、色情低俗、暴力恐怖、侵犯他人权益等内容;检测未通过或检测异常时无法生成与导出。如发现不良内容,可通过页面底部“投诉与反馈”告诉我们。', + showCancel: false, + confirmText: '我知道了', + }) + }, + + // —— 生成管线 —— + + schedule(wait = 120) { + this.debounce('run', () => this.run(), wait) + }, + + run() { + if (!this._nodes.preview || !this._nodes.work) return + // 没写内容、也没选照片:示例二维码没有意义,改为播放动态演示 + if (!this.data.text.trim() && !this._source) { + this._token += 1 + this.setData({ + showIntro: true, generating: false, isSample: true, scanState: 'idle', usage: [], usageAll: [], usageMore: 0, + }, () => this.syncIntro()) + return + } + this.stopIntro() + if (this.data.showIntro) this.setData({ showIntro: false }) + const token = ++this._token + this.setData({ generating: true }, () => { + // 先让“生成中”状态渲染一帧,再做同步计算 + setTimeout(() => { + if (token !== this._token) return + try { + this.compute() + this.afterCompute(token) + } catch (error) { + console.error('生成失败', error) + this.setData({ generating: false }) + wx.showToast({ title: '内容太长或图片异常,请调整后重试', icon: 'none' }) + } + }, 16) + }) + }, + + compute() { + const d = this.data + const text = d.text.trim() + const qrKey = `${text}|${d.level}` + if (!this._qr || this._qrKey !== qrKey) { + this._qr = makeQr(text, d.level) + this._qrKey = qrKey + } + const src = this._source + const placement = createPlacement(this._qr.size, { + scale: d.qrScale, posX: d.posX, posY: d.posY, quiet: d.quiet, hasImage: Boolean(src), + }) + let base = null + if (src) { + const c = src.crop + const baseKey = `${placement.size}|${c.focusX}|${c.focusY}|${c.zoom}|${c.flipH}|${c.flipV}|${d.removeBackground}|${d.bgTolerance}` + if (this._baseKey !== baseKey) { + const sampled = sampleImage(this._nodes.work, src.image, placement.size, c) + if (d.removeBackground) sampled.mask = removeSimpleBackground(sampled.rgb, placement.size, d.bgTolerance) + this._base = sampled + this._baseKey = baseKey + } + base = this._base + } + const pal = preparePalette(d.paletteId) + this._artwork = computeArtwork(base, this._qr, placement, pal, { + modeId: d.modeId, coherence: d.coherence, cleanup: d.cleanup, + }) + }, + + afterCompute(token) { + const art = this._artwork + this.drawPreview() + const usageAll = art.usage.map((u) => ({ id: u.id, hex: u.hex, count: u.count })) + this.setData({ + generating: false, + isSample: this._qr.isSample, + gridText: `${art.size}×${art.size}`, + beadsText: formatNumber(art.beads), + colorsText: String(art.usage.length), + qrGridText: `${this._qr.size}×${this._qr.size}`, + usage: usageAll.slice(0, 12), + usageAll: this.data.usageExpanded ? usageAll : [], + usageMore: Math.max(0, usageAll.length - 12), + scanState: 'checking', + }) + this._usageAll = usageAll + // 扫码自检放到下一轮,避免阻塞预览出图 + this.debounce('scan', () => { + if (token !== this._token) return + const expected = this.data.text.trim() || SAMPLE_TEXT + const result = selfCheck(art, expected) + if (token === this._token) this.setData({ scanState: result.ok ? 'ok' : 'fail' }) + }, 60) + }, + + drawPreview() { + const node = this._nodes.preview + if (!node || !this._artwork) return + const px = Math.round(this._previewCss * this._dpr) + renderPreview(node, this._artwork, px, this.data.beadStyle) + }, + + // —— 动态演示 —— + + syncIntro() { + const should = this.data.showIntro && !this._hidden && !this._scrolledAway && !this.data.cropVisible + if (should) this.startIntro() + else this.pauseIntro() + }, + + startIntro() { + if (this._introPlayer) return + const node = this._nodes.preview + if (!node || !this._nodes.work) return + if (!this._intro) { + try { + this._intro = createIntro({ workCanvas: this._nodes.work }) + } catch (error) { + console.error('演示动画初始化失败', error) + return + } + } + const px = Math.round(this._previewCss * this._dpr) + node.width = px + node.height = px + const ctx = node.getContext('2d') + const raf = node.requestAnimationFrame ? (f) => node.requestAnimationFrame(f) : (f) => setTimeout(f, 16) + const caf = node.cancelAnimationFrame ? (id) => node.cancelAnimationFrame(id) : (id) => clearTimeout(id) + const player = { start: Date.now() - (this._introElapsed || 0), last: 0, id: 0, caf } + const tick = () => { + if (this._introPlayer !== player) return + const now = Date.now() + if (now - player.last >= 30) { // 约 30 帧/秒,流畅且省电 + player.last = now + this._introElapsed = now - player.start + this._intro.draw(ctx, this._introElapsed / 1000, px) + } + player.id = raf(tick) + } + this._introPlayer = player + tick() + }, + + pauseIntro() { + const player = this._introPlayer + if (!player) return + this._introPlayer = null + player.caf(player.id) + }, + + stopIntro() { + this.pauseIntro() + this._introElapsed = 0 + }, + + replayIntro() { + if (!this.data.showIntro) return + this.stopIntro() + this.syncIntro() + }, + + // 引导用户开始:滚到第 1 步并弹出键盘 + startCreate() { + wx.pageScrollTo({ + selector: '#step1', + offsetTop: -(this.data.statusBarHeight + this.data.navHeight + 12), + duration: 300, + }) + setTimeout(() => this.setData({ textFocus: true }), 320) + }, + + onTextBlur() { + if (this.data.textFocus) this.setData({ textFocus: false }) + }, + + // —— 1. 内容 —— + + onTextInput(e) { + const text = e.detail.value + this.setData({ text, textStatus: text.trim() ? 'checking' : 'idle' }) + this.schedule(220) + this.debounce('textCheck', () => this.verifyText(text), 650) + }, + + clearText() { + this.setData({ text: '', textStatus: 'idle' }) + this.schedule(0) + }, + + verifyText(raw) { + const value = String(raw || '').trim() + if (!value) return + security.checkText(value, 2).then(({ status }) => { + if (this.data.text.trim() !== value) return + this.setData({ textStatus: status }) + if (status === 'risky') wx.showToast({ title: security.MESSAGES.risky, icon: 'none' }) + }) + }, + + scanImport() { + wx.scanCode({ + onlyFromCamera: false, + scanType: ['qrCode'], + success: (res) => { + const decoded = String(res.result || '').trim().slice(0, 120) + if (!decoded) { wx.showToast({ title: '二维码里没有内容', icon: 'none' }); return } + this.setData({ scanImportLoading: true }) + // 扫码得到的内容同样先过内容安全检测,通过后才填入 + security.checkText(decoded, 2).then(({ status }) => { + this.setData({ scanImportLoading: false }) + if (status !== 'pass') { wx.showToast({ title: security.MESSAGES[status], icon: 'none' }); return } + this.setData({ text: decoded, textStatus: 'pass' }) + this.schedule(0) + wx.showToast({ title: '已导入二维码内容', icon: 'none' }) + }) + }, + fail: (err) => { + if (err && /cancel/i.test(err.errMsg || '')) return + wx.showToast({ title: '没有识别到二维码', icon: 'none' }) + }, + }) + }, + + // —— 2. 照片 —— + + loadImage(path, info) { + return new Promise((resolve, reject) => { + const image = this._nodes.work.createImage() + image.onload = () => { + if (info && info.width) { image._w = info.width; image._h = info.height } + resolve(image) + } + image.onerror = () => reject(new Error('IMAGE_LOAD_FAILED')) + image.src = path + }) + }, + + chooseImage() { + wx.chooseMedia({ + count: 1, + mediaType: ['image'], + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + const file = res.tempFiles && res.tempFiles[0] + if (file && file.tempFilePath) this.prepareImage(file.tempFilePath) + }, + fail: (err) => { + if (err && /cancel/i.test(err.errMsg || '')) return + wx.showToast({ title: '没有选到照片', icon: 'none' }) + }, + }) + }, + + prepareImage(path) { + wx.showLoading({ title: '读取照片…', mask: true }) + new Promise((resolve) => { + wx.getImageInfo({ src: path, success: resolve, fail: () => resolve(null) }) + }) + .then((info) => this.loadImage(path, info).then((image) => ({ image, info }))) + .then(({ image }) => { + wx.hideLoading() + this._pending = { image, path } + // 选图后立即在后台做内容安全检测;同时打开构图编辑,检测通过后才能使用 + this.startImageCheck(this._pending) + this.openCrop(true) + }) + .catch(() => { + wx.hideLoading() + wx.showToast({ title: '照片读取失败,请换一张 JPG / PNG', icon: 'none' }) + }) + }, + + startImageCheck(pending) { + pending.status = 'checking' + this.setData({ cropCheck: 'checking' }) + pending.check = security.checkImage(pending.path, pending.image, this._nodes.security).then(({ status }) => { + pending.status = status + if (this._pending === pending) { + this.setData({ cropCheck: status }) + if (pending.applyWhenPassed && status === 'pass') this.applyCrop() + pending.applyWhenPassed = false + } + return status + }) + }, + + retryImageCheck() { + if (this._pending) this.startImageCheck(this._pending) + }, + + editImage() { + if (!this._source) return + this._pending = { image: this._source.image, path: this._source.path, status: 'pass' } + this.setData({ cropCheck: 'pass' }) + this.openCrop(false) + }, + + removeImage() { + this._source = null + this._base = null + this._baseKey = '' + this.setData({ hasImage: false, imageThumb: '', imageMeta: '', removeBackground: false }) + this.schedule(0) + }, + + // —— 裁切编辑 —— + + openCrop(isNew) { + const crop = !isNew && this._source ? this._source.crop : null + this.pauseIntro() + this.setData({ + cropVisible: true, + draftFocusX: crop ? crop.focusX : 50, + draftFocusY: crop ? crop.focusY : 50, + draftZoom: crop ? Math.round(crop.zoom * 100) : 100, + draftFlipH: crop ? crop.flipH : false, + draftFlipV: crop ? crop.flipV : false, + draftRemoveBg: isNew ? false : this.data.removeBackground, + draftBgTolerance: isNew ? 22 : this.data.bgTolerance, + }, () => { + wx.createSelectorQuery().in(this) + .select('#crop').fields({ node: true, size: true }) + .select('#crop').boundingClientRect() + .exec((res) => { + this._nodes.crop = res[0] && res[0].node + this._cropCss = (res[0] && res[0].width) || 300 + this._cropRect = res[1] || { left: 0, top: 0 } + this.drawCrop() + }) + }) + }, + + drawCrop() { + const node = this._nodes.crop + const pending = this._pending + if (!node || !pending) return + node.width = CROP_SIZE + node.height = CROP_SIZE + const ctx = node.getContext('2d') + const d = this.data + ctx.imageSmoothingEnabled = true + drawCropped(ctx, pending.image, CROP_SIZE, CROP_SIZE, { + focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV, + }) + // 三分线,仅辅助构图 + ctx.strokeStyle = 'rgba(255,255,255,0.5)' + ctx.lineWidth = 1.5 + ctx.beginPath() + for (let i = 1; i < 3; i += 1) { + const p = Math.round(CROP_SIZE * i / 3) + 0.5 + ctx.moveTo(p, 0); ctx.lineTo(p, CROP_SIZE) + ctx.moveTo(0, p); ctx.lineTo(CROP_SIZE, p) + } + ctx.stroke() + }, + + touchPoint(t) { + const rect = this._cropRect || { left: 0, top: 0 } + const x = t.clientX == null ? (t.pageX == null ? t.x : t.pageX) : t.clientX + const y = t.clientY == null ? (t.pageY == null ? t.y : t.pageY) : t.clientY + return { x: x - rect.left, y: y - rect.top } + }, + + touchDistance(touches) { + const a = this.touchPoint(touches[0]) + const b = this.touchPoint(touches[1]) + return Math.hypot(b.x - a.x, b.y - a.y) + }, + + onCropStart(e) { + const touches = e.touches || [] + if (!touches[0]) return + if (touches.length >= 2) { + this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom } + return + } + this._gesture = { type: 'drag', ...this.touchPoint(touches[0]), focusX: this.data.draftFocusX, focusY: this.data.draftFocusY } + }, + + onCropMove(e) { + const touches = e.touches || [] + const g = this._gesture + const pending = this._pending + if (!touches[0] || !g || !pending) return + if (touches.length >= 2) { + if (g.type !== 'pinch') { + this._gesture = { type: 'pinch', distance: this.touchDistance(touches), zoom: this.data.draftZoom } + return + } + const zoom = Math.max(100, Math.min(400, Math.round(g.zoom * this.touchDistance(touches) / g.distance))) + this.setData({ draftZoom: zoom }) + this.debounce('crop', () => this.drawCrop(), 16) + return + } + if (g.type !== 'drag') return + const p = this.touchPoint(touches[0]) + const cssScale = CROP_SIZE / (this._cropCss || CROP_SIZE) + const src = getImageDimensions(pending.image) + const scale = Math.max(CROP_SIZE / src.width, CROP_SIZE / src.height) * (this.data.draftZoom / 100) + const w = src.width * scale + const h = src.height * scale + const clamp = (v) => Math.max(0, Math.min(100, v)) + const dirX = this.data.draftFlipH ? -1 : 1 + const dirY = this.data.draftFlipV ? -1 : 1 + const fx = Math.abs(w - CROP_SIZE) < 0.01 ? g.focusX : clamp(g.focusX + dirX * ((p.x - g.x) * cssScale / (CROP_SIZE - w)) * 100) + const fy = Math.abs(h - CROP_SIZE) < 0.01 ? g.focusY : clamp(g.focusY + dirY * ((p.y - g.y) * cssScale / (CROP_SIZE - h)) * 100) + this.setData({ draftFocusX: fx, draftFocusY: fy }) + this.debounce('crop', () => this.drawCrop(), 16) + }, + + onCropEnd() { + this._gesture = null + clearTimeout(this._timers.crop) + this.drawCrop() + }, + + onDraftZoom(e) { + this.setData({ draftZoom: Number(e.detail.value) }) + this.debounce('crop', () => this.drawCrop(), 16) + }, + resetCrop() { this.setData({ draftFocusX: 50, draftFocusY: 50, draftZoom: 100 }, () => this.drawCrop()) }, + flipH() { this.setData({ draftFlipH: !this.data.draftFlipH }, () => this.drawCrop()) }, + flipV() { this.setData({ draftFlipV: !this.data.draftFlipV }, () => this.drawCrop()) }, + onDraftRemoveBg(e) { this.setData({ draftRemoveBg: e.detail.value }) }, + onDraftBgTolerance(e) { this.setData({ draftBgTolerance: Number(e.detail.value) }) }, + + cancelCrop() { + if (this._pending) this._pending.applyWhenPassed = false + this.setData({ cropVisible: false }, () => this.syncIntro()) + }, + + applyCrop() { + const pending = this._pending + if (!pending) return + if (pending.status === 'checking') { + // 检测未完成:记住用户意图,通过后自动应用 + pending.applyWhenPassed = true + wx.showToast({ title: '正在做内容安全检测,通过后自动应用', icon: 'none' }) + return + } + if (pending.status !== 'pass') { + wx.showToast({ title: security.MESSAGES[pending.status] || security.MESSAGES.error, icon: 'none' }) + return + } + const d = this.data + const src = getImageDimensions(pending.image) + this._source = { + image: pending.image, + path: pending.path, + crop: { focusX: d.draftFocusX, focusY: d.draftFocusY, zoom: d.draftZoom / 100, flipH: d.draftFlipH, flipV: d.draftFlipV }, + } + this._baseKey = '' + this.setData({ + cropVisible: false, + hasImage: true, + imageThumb: pending.path, + imageMeta: `${src.width}×${src.height} · 已通过安全检测`, + removeBackground: d.draftRemoveBg, + bgTolerance: d.draftBgTolerance, + // 首次导入照片时,二维码默认铺满全图;用户可再缩小 + }) + this.schedule(0) + }, + + // —— 3. 效果 —— + + onScaleChanging(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(160) }, + onScaleChange(e) { this.setData({ qrScale: Number(e.detail.value) }); this.schedule(0) }, + pickPosition(e) { + const { x, y } = e.currentTarget.dataset + this.setData({ posX: Number(x), posY: Number(y) }) + this.schedule(0) + }, + toggleFineTune() { this.setData({ fineTuneOpen: !this.data.fineTuneOpen }) }, + onPosX(e) { this.setData({ posX: Number(e.detail.value) }); this.schedule(100) }, + onPosY(e) { this.setData({ posY: Number(e.detail.value) }); this.schedule(100) }, + pickMode(e) { this.setData({ modeId: e.currentTarget.dataset.id }); this.schedule(0) }, + toggleAdvanced() { this.setData({ advancedOpen: !this.data.advancedOpen }) }, + pickPalette(e) { this.setData({ paletteId: e.currentTarget.dataset.id }); this.schedule(0) }, + onCoherence(e) { this.setData({ coherence: Number(e.detail.value) }); this.schedule(200) }, + onCleanup(e) { this.setData({ cleanup: e.detail.value }); this.schedule(0) }, + pickLevel(e) { this.setData({ level: e.currentTarget.dataset.id }); this.schedule(0) }, + pickQuiet(e) { this.setData({ quiet: Number(e.currentTarget.dataset.id) }); this.schedule(0) }, + onBgToggle(e) { this.setData({ removeBackground: e.detail.value }); this.schedule(0) }, + + applyScanFix() { + // 自检未通过时的一键修复:扫码优先 + 1 格白边 + H 级纠错 + this.setData({ modeId: 'strict', quiet: Math.max(1, this.data.quiet), level: 'H' }) + this.schedule(0) + wx.showToast({ title: '已切换为扫码优先', icon: 'none' }) + }, + + toggleUsage() { + const expanded = !this.data.usageExpanded + this.setData({ usageExpanded: expanded, usageAll: expanded ? (this._usageAll || []) : [] }) + }, + + // 生成当前预览图,用微信“识别图中二维码”实测 + verifyScan() { + const node = this._nodes.preview + if (!node) return + wx.canvasToTempFilePath({ + canvas: node, + fileType: 'png', + success: (res) => { + wx.previewImage({ urls: [res.tempFilePath], showmenu: true }) + wx.showToast({ title: '长按图片,选择“识别图中二维码”', icon: 'none', duration: 2500 }) + }, + fail: () => wx.showToast({ title: '预览生成失败,请重试', icon: 'none' }), + }) + }, + + // —— 导出 —— + + openExport() { + if (!this.data.text.trim()) { + this.startCreate() + return + } + if (!this._artwork) return + if (this.data.textStatus === 'risky') { + wx.showToast({ title: security.MESSAGES.risky, icon: 'none' }) + return + } + this.setData({ exportVisible: true, exportResult: null }) + }, + closeExport() { + if (this.data.exporting) return + this.setData({ exportVisible: false }) + }, + pickFormat(e) { this.setData({ exportFormat: e.currentTarget.dataset.id, exportResult: null }) }, + pickLayout(e) { this.setData({ exportLayout: e.currentTarget.dataset.id, exportResult: null }) }, + pickQuality(e) { this.setData({ exportQuality: e.currentTarget.dataset.id, exportResult: null }) }, + pickBeadStyle(e) { + this.setData({ beadStyle: e.currentTarget.dataset.id, exportResult: null }, () => this.drawPreview()) + }, + onGridToggle(e) { this.setData({ gridEnabled: e.detail.value, exportResult: null }) }, + pickInterval(e) { this.setData({ gridInterval: Number(e.currentTarget.dataset.id), exportResult: null }) }, + pickGridColor(e) { this.setData({ gridColor: e.currentTarget.dataset.color, exportResult: null }) }, + onWatermark(e) { + const watermark = e.detail.value + this.setData({ watermark, watermarkStatus: watermark.trim() ? 'checking' : 'idle', exportResult: null }) + this.debounce('wmCheck', () => { + const value = watermark.trim() + if (!value) return + security.checkText(value, 1).then(({ status }) => { + if (this.data.watermark.trim() === value) this.setData({ watermarkStatus: status }) + }) + }, 650) + }, + + gridSettings() { + return { enabled: this.data.gridEnabled, interval: this.data.gridInterval, color: this.data.gridColor, opacity: 60 } + }, + + capture(fileType, quality) { + return new Promise((resolve, reject) => { + wx.canvasToTempFilePath({ + canvas: this._nodes.work, + fileType, + quality, + success: (res) => resolve(res.tempFilePath), + fail: reject, + }) + }) + }, + + step(label) { + return new Promise((resolve) => this.setData({ exportStep: label }, () => setTimeout(resolve, 16))) + }, + + async startExport() { + if (this.data.exporting || !this._artwork) return + this.setData({ exporting: true, exportResult: null }) + try { + // 平台合规:导出前再次确认所有用户文本均已通过内容安全检测(已通过的走缓存,无额外等待) + await this.step('内容安全校验…') + const check = await security.checkTexts([ + { value: this.data.text, scene: 2 }, + { value: this.data.watermark, scene: 1 }, + ]) + if (check.status !== 'pass') { + wx.showToast({ title: security.MESSAGES[check.status], icon: 'none' }) + if (check.status === 'risky') this.setData({ textStatus: security.isTextPassed(this.data.text) ? 'pass' : 'risky' }) + return + } + if (this._source && !this.data.hasImage) throw new Error('IMAGE_STATE') + this.cleanOldFiles() + if (this.data.exportFormat === 'png') await this.exportPng() + else await this.exportPdf() + } catch (error) { + console.error('导出失败', error) + wx.showToast({ title: '导出失败,请重试(可尝试“标准”清晰度)', icon: 'none' }) + } finally { + this.setData({ exporting: false, exportStep: '' }) + } + }, + + // 按清晰度求每格像素:高清长边约 4000px,标准约 2400px + pngCellSize() { + const size = this._artwork.size + const maxSide = this.data.exportQuality === 'hd' ? 4000 : 2400 + let cell = Math.max(12, Math.min(72, Math.floor(maxSide / (size + 5)))) + while (cell > 12) { + const L = patternPageLayout(size, cell) + if (Math.max(L.width, L.height) <= maxSide) break + cell -= 1 + } + return cell + }, + + patternMeta() { + const art = this._artwork + return `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${art.pal.label} · 扫码内容:${this.data.text.trim()}` + }, + + infoParams(width) { + return { + artwork: this._artwork, + width, + text: this.data.text.trim(), + paletteLabel: `${this._artwork.pal.label}(本作品用 ${this._artwork.usage.length} 色)`, + mpCode: this._mpCode || null, + thumbnail: true, + } + }, + + async exportPng() { + const node = this._nodes.work + const art = this._artwork + const cell = this.pngCellSize() + const L = patternPageLayout(art.size, cell) + const wm = this.data.watermark.trim() + const drawPattern = (ctx) => drawPatternPage(ctx, art, { cell, style: this.data.beadStyle, grid: this.gridSettings(), meta: this.patternMeta() }) + const files = [] + if (this.data.exportLayout === 'separate') { + await this.step('绘制图纸…') + node.width = L.width + node.height = L.height + const ctx = node.getContext('2d') + drawPattern(ctx) + drawWatermark(ctx, L.width, L.height, wm) + files.push({ name: '拼豆图纸', path: await this.capture('png') }) + const info = buildInfoPages(this.infoParams(this.data.exportQuality === 'hd' ? 2000 : 1400)) + for (let p = 0; p < info.pageCount; p += 1) { + await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`) + node.width = info.width + node.height = info.height + const c = node.getContext('2d') + info.draw(c, p) + drawWatermark(c, info.width, info.height, wm) + files.push({ name: info.pageCount > 1 ? `用豆清单 ${p + 1}` : '用豆清单', path: await this.capture('png') }) + } + } else { + await this.step('绘制图纸与清单…') + const info = buildInfoPages(this.infoParams(L.width)) + const gap = Math.round(L.width * 0.02) + const totalH = L.height + info.pageCount * (info.height + gap) + const scale = fitScale(L.width, totalH) + node.width = Math.round(L.width * scale) + node.height = Math.round(totalH * scale) + const ctx = node.getContext('2d') + ctx.fillStyle = '#e8e5de' + ctx.fillRect(0, 0, node.width, node.height) + ctx.scale(scale, scale) + drawPattern(ctx) + for (let p = 0; p < info.pageCount; p += 1) { + ctx.save() + ctx.translate(0, L.height + gap + p * (info.height + gap)) + info.draw(ctx, p) + ctx.restore() + } + ctx.setTransform(1, 0, 0, 1, 0, 0) + drawWatermark(ctx, node.width, node.height, wm) + files.push({ name: '图纸 + 用豆清单', path: await this.capture('png') }) + } + await this.step('保存到相册…') + const saved = await this.saveImages(files.map((f) => f.path)) + this.setData({ exportResult: { kind: 'png', saved, files } }) + if (saved) wx.showToast({ title: `${files.length} 张图片已保存`, icon: 'success' }) + }, + + saveImages(paths) { + return paths.reduce((chain, path) => chain.then((ok) => (ok ? new Promise((resolve) => { + wx.saveImageToPhotosAlbum({ + filePath: path, + success: () => resolve(true), + fail: (err) => { + if (err && /auth|deny/i.test(err.errMsg || '')) { + wx.showModal({ + title: '需要相册权限', + content: '请允许保存到相册,才能把图纸存到手机里', + confirmText: '去设置', + success: (r) => { if (r.confirm) wx.openSetting() }, + }) + } + resolve(false) + }, + }) + }) : false)), Promise.resolve(true)) + }, + + previewResult() { + const result = this.data.exportResult + if (!result) return + if (result.kind === 'png') { + wx.previewImage({ urls: result.files.map((f) => f.path), showmenu: true }) + } else { + this.openPdf(result.files[0].path) + } + }, + + openResultFile(e) { + const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)] + if (!file) return + if (this.data.exportResult.kind === 'png') wx.previewImage({ urls: [file.path], showmenu: true }) + else this.openPdf(file.path) + }, + + shareResultFile(e) { + const file = this.data.exportResult.files[Number(e.currentTarget.dataset.index)] + if (!file || !wx.shareFileMessage) { + wx.showToast({ title: '当前微信版本不支持,请先打开再转发', icon: 'none' }) + return + } + wx.shareFileMessage({ filePath: file.path, fileName: file.fileName || `${file.name}.pdf` }) + }, + + openPdf(path) { + wx.openDocument({ filePath: path, fileType: 'pdf', showMenu: true }) + }, + + cleanOldFiles() { + try { + const fs = wx.getFileSystemManager() + fs.readdirSync(wx.env.USER_DATA_PATH) + .filter((name) => name.indexOf(FILE_PREFIX) === 0) + .forEach((name) => { try { fs.unlinkSync(`${wx.env.USER_DATA_PATH}/${name}`) } catch (e) { /* 忽略 */ } }) + } catch (e) { /* 忽略 */ } + }, + + async canvasJpeg(width, height, draw, quality = 0.9) { + const node = this._nodes.work + node.width = width + node.height = height + const ctx = node.getContext('2d') + draw(ctx) + const path = await this.capture('jpg', quality) + return wx.getFileSystemManager().readFileSync(path) + }, + + // 页眉(中文)渲染为小图,矢量页面上方使用 + async renderHeader(title, sub) { + const w = 1540 + const h = 80 + const data = await this.canvasJpeg(w, h, (ctx) => { + ctx.fillStyle = '#ffffff' + ctx.fillRect(0, 0, w, h) + ctx.fillStyle = '#16211f' + ctx.font = 'bold 34px sans-serif' + ctx.textBaseline = 'middle' + ctx.fillText(title, 0, 30) + ctx.fillStyle = '#6f7a76' + ctx.font = '22px sans-serif' + ctx.fillText(sub, 0, 64) + ctx.fillStyle = '#0f766e' + ctx.font = 'bold 24px sans-serif' + ctx.textAlign = 'right' + ctx.fillText('豆维 · 可扫码的拼豆', w, 30) + }, 0.92) + return { data, width: w, height: h } + }, + + // 水印转为 1-bit 蒙版,矢量页上以半透明平铺 + watermarkMask(doc, text) { + const value = String(text || '').trim() + if (!value) return null + const node = this._nodes.work + const fontSize = 64 + node.width = 16 + node.height = 16 + let ctx = node.getContext('2d') + ctx.font = `bold ${fontSize}px sans-serif` + const width = Math.min(2000, Math.ceil(ctx.measureText(value).width) + 16) + const height = Math.round(fontSize * 1.4) + node.width = width + node.height = height + ctx = node.getContext('2d') + ctx.clearRect(0, 0, width, height) + ctx.fillStyle = '#000000' + ctx.font = `bold ${fontSize}px sans-serif` + ctx.textBaseline = 'middle' + ctx.fillText(value, 8, height / 2) + const pixels = ctx.getImageData(0, 0, width, height).data + const rowBytes = Math.ceil(width / 8) + const bits = new Uint8Array(rowBytes * height).fill(0xff) // 1 = 不绘制 + for (let y = 0; y < height; y += 1) { + for (let x = 0; x < width; x += 1) { + if (pixels[(y * width + x) * 4 + 3] > 110) bits[y * rowBytes + (x >> 3)] &= ~(0x80 >> (x & 7)) + } + } + return { id: doc.addImageMask(bits, width, height), width, height } + }, + + async buildPatternPdf(doc) { + const art = this._artwork + const plan = planPatternPages(art.size) + const headers = [] + for (let i = 0; i < plan.pages.length; i += 1) { + const page = plan.pages[i] + await this.step(`绘制图纸页 ${i + 1}/${plan.pages.length}…`) + const title = page.kind === 'overview' + ? (plan.tiled ? `图纸总览 · 共分 ${plan.pages.length - 1} 块` : '拼豆图纸') + : `第 ${page.tile} 块 · 列 ${page.region.col + 1}–${page.region.col + page.region.cols} · 行 ${page.region.row + 1}–${page.region.row + page.region.rows}` + const sub = page.kind === 'overview' + ? `${art.size}×${art.size} 格 · ${formatNumber(art.beads)} 颗 · ${art.usage.length} 色 · ${plan.tiled ? '红框编号对应后续分块页,按块拼更轻松' : '格内为 MARD 色号'}` + : `第 ${i + 1} / ${plan.pages.length} 页 · 格内为 MARD 色号 · 坐标为整张作品的行列号` + const header = await this.renderHeader(title, sub) + headers.push({ id: doc.addJpeg(header.data, header.width, header.height), width: header.width, height: header.height }) + } + const watermark = this.watermarkMask(doc, this.data.watermark) + addPatternPages(doc, art, { plan, headers, style: this.data.beadStyle, grid: this.gridSettings(), watermark }) + }, + + async buildInfoPdf(doc) { + const info = buildInfoPages(this.infoParams(1654)) + for (let p = 0; p < info.pageCount; p += 1) { + await this.step(`绘制用豆清单 ${p + 1}/${info.pageCount}…`) + const data = await this.canvasJpeg(info.width, info.height, (ctx) => { + info.draw(ctx, p) + drawWatermark(ctx, info.width, info.height, this.data.watermark) + }, 0.9) + addImagePage(doc, doc.addJpeg(data, info.width, info.height), info.width, info.height) + } + }, + + async exportPdf() { + const fs = wx.getFileSystemManager() + const stamp = Date.now().toString(36) + const art = this._artwork + const base = `${FILE_PREFIX}${art.size}x${art.size}-${stamp}` + const files = [] + const write = (doc, suffix, name) => { + const fileName = `豆维-${name}.pdf` + const path = `${wx.env.USER_DATA_PATH}/${base}-${suffix}.pdf` + fs.writeFileSync(path, doc.build(), 'binary') + files.push({ name, fileName, path }) + } + if (this.data.exportLayout === 'separate') { + const patternDoc = new PdfDoc() + await this.buildPatternPdf(patternDoc) + write(patternDoc, 'pattern', '拼豆图纸') + const infoDoc = new PdfDoc() + await this.buildInfoPdf(infoDoc) + write(infoDoc, 'info', '用豆清单') + } else { + const doc = new PdfDoc() + await this.buildPatternPdf(doc) + await this.buildInfoPdf(doc) + write(doc, 'all', '图纸与用豆清单') + } + this.setData({ exportResult: { kind: 'pdf', saved: true, files } }) + }, +}) diff --git a/BeadQR/miniprogram/pages/index/index.json b/BeadQR/miniprogram/pages/index/index.json new file mode 100644 index 0000000000000000000000000000000000000000..3a164a5de25f9e3973d37a973c87fb807ef30625 --- /dev/null +++ b/BeadQR/miniprogram/pages/index/index.json @@ -0,0 +1,6 @@ +{ + "navigationStyle": "custom", + "navigationBarTextStyle": "white", + "backgroundColorTop": "#0f3d3a", + "usingComponents": {} +} diff --git a/BeadQR/miniprogram/pages/index/index.wxml b/BeadQR/miniprogram/pages/index/index.wxml new file mode 100644 index 0000000000000000000000000000000000000000..7234757764c44cf005156ba6875b418f5495f838 --- /dev/null +++ b/BeadQR/miniprogram/pages/index/index.wxml @@ -0,0 +1,403 @@ + + + + + 豆维 · 可扫码的拼豆 + + + + + + + MARD 拼豆图纸生成器 + 把一句话藏进拼豆里 + 拼好的作品用微信一扫,就能读出你写的文字或链接 + + 1写一句话 + › + 2选张照片 + › + 3导出图纸 + + + + + + + + 示例二维码 · 写下内容后换成你的 + + + + + + + 上面是演示:写下一句话,这里就会变成你专属的可扫码图纸 + + + 221 色MARD 标准色号 + A4打印图纸 + 扫一扫微信直接读出 + + + 开始制作 · 写下第一句话 + + + + + + + 写下内容后,自动检查能否扫出 + 正在检查能否扫出… + ✓ 自检通过,可以扫出内容 + ! 自检未扫出,建议优化 + 生成后自动检查能否扫码 + + 一键优化 + + + + {{gridText}}格子 + {{beadsText}}颗豆子 + {{colorsText}}种颜色 + + + + 写下内容,生成你的二维码 + 用微信扫一扫验证 + + + + + + + + 1 + + 扫码后要看到什么? + 一句祝福、一个网址,或者你的名字 + + + +