reborn-ui 0.1.76 → 0.1.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +182 -240
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
- package/registry/components/reborn-affix.json +8 -3
- package/registry/components/reborn-back-top.json +9 -4
- package/registry/components/reborn-badge.json +11 -5
- package/registry/components/reborn-button.json +5 -5
- package/registry/components/reborn-card.json +18 -0
- package/registry/components/reborn-cascader.json +18 -0
- package/registry/components/reborn-checkbox.json +4 -4
- package/registry/components/reborn-chip.json +11 -5
- package/registry/components/reborn-collapse.json +11 -5
- package/registry/components/reborn-color-picker.json +50 -0
- package/registry/components/reborn-draggable.json +32 -0
- package/registry/components/reborn-drawer.json +17 -0
- package/registry/components/reborn-dropdown-select.json +18 -0
- package/registry/components/reborn-footer.json +40 -0
- package/registry/components/reborn-form.json +11 -6
- package/registry/components/reborn-image.json +10 -5
- package/registry/components/reborn-input-number.json +12 -6
- package/registry/components/reborn-input-otp.json +40 -0
- package/registry/components/reborn-input.json +4 -4
- package/registry/components/reborn-loading.json +23 -0
- package/registry/components/reborn-loadmore.json +23 -0
- package/registry/components/reborn-overlay.json +38 -0
- package/registry/components/reborn-page.json +18 -0
- package/registry/components/reborn-picker-view.json +26 -0
- package/registry/components/reborn-popover.json +58 -0
- package/registry/components/reborn-popup.json +23 -0
- package/registry/components/reborn-qrcode.json +45 -0
- package/registry/components/reborn-radio.json +45 -0
- package/registry/components/reborn-rate.json +40 -0
- package/registry/components/reborn-root-portal.json +26 -0
- package/registry/components/reborn-select-date.json +40 -0
- package/registry/components/reborn-select-trigger.json +25 -0
- package/registry/components/reborn-select.json +41 -0
- package/registry/components/reborn-slider.json +40 -0
- package/registry/components/reborn-sticky.json +12 -6
- package/registry/components/reborn-switch.json +13 -7
- package/registry/components/reborn-tabbar.json +38 -0
- package/registry/components/reborn-tabs copy.json +46 -0
- package/registry/components/reborn-tabs-test.json +46 -0
- package/registry/components/reborn-tabs.json +12 -6
- package/registry/components/reborn-text.json +34 -0
- package/registry/components/reborn-textarea.json +5 -5
- package/registry/components/reborn-toast.json +38 -0
- package/registry/components/reborn-transition.json +38 -0
- package/registry/components/reborn-waterfall.json +18 -0
- package/registry/components/scroll-island.json +2 -2
- package/registry/registry.json +1101 -97
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-qrcode",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "export { qrcodeMode, default as rebornQrcodeConfig } from './reborn-qrcode.config'\r\nexport { default as RebornQrcode } from './RebornQrcode.vue'\r\nexport { type ClQrcodeMode, eccLevel } from './types'\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "qrcode.ts",
|
|
11
|
+
"content": "export type GenerateFrameResult = {\r\n\tframeBuffer: Uint8Array;\r\n\twidth: number;\r\n};\r\n\r\n/**\r\n * 二维码生成器\r\n * @description 纯 UTS 实现的二维码生成算法,支持多平台,兼容 uni-app x。核心算法参考 QR Code 标准,支持自定义纠错级别、自动适配内容长度。\r\n * @version 1.0.0\r\n * @平台兼容性 App、H5、微信小程序、UTS\r\n * @注意事项\r\n * - 仅支持 8bit 字符串内容,不支持数字/字母/汉字等模式优化\r\n * - 生成结果为二维码点阵数据和宽度,需配合 canvas 绘制\r\n * - 纠错级别支持 'L'/'M'/'Q'/'H',默认 'L'\r\n */\r\n\r\n// 对齐块间距表 - 不同版本二维码的对齐块分布位置\r\nconst ALIGNMENT_DELTA = [\r\n\t0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,\r\n\t26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28\r\n] as number[];\r\n\r\n// 纠错块参数表 - 每个版本包含4个参数:块数、数据宽度、纠错宽度\r\nconst ECC_BLOCKS = [\r\n\t1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22,\r\n\t1, 0, 16, 28, 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32,\r\n\t18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, 2, 0,\r\n\t68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,\r\n\t1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22,\r\n\t4, 4, 16, 20, 4, 4, 12, 24, 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81,\r\n\t20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4,\r\n\t14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,\r\n\t11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98,\r\n\t24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2,\r\n\t17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, 3, 4, 113, 28, 3, 11, 44,\r\n\t26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,\r\n\t4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16,\r\n\t24, 30, 34, 0, 13, 24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117,\r\n\t30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30,\r\n\t22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8, 4, 122, 30, 22,\r\n\t3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31,\r\n\t15, 30, 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47,\r\n\t28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15,\r\n\t30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1, 115, 30, 14, 21, 46,\r\n\t28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16,\r\n\t30, 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47,\r\n\t28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15,\r\n\t30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20, 4, 117, 30, 40, 7, 47,\r\n\t28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15,\r\n\t30\r\n] as number[];\r\n\r\n// 纠错级别映射表 - 将人类可读的纠错级别映射为内部数值\r\nconst ECC_LEVELS = new Map<string, number>([\r\n\t[\"L\", 1],\r\n\t[\"M\", 2],\r\n\t[\"Q\", 3],\r\n\t[\"H\", 4]\r\n]);\r\n\r\n// 最终格式信息掩码表 - 用于格式信息区域的掩码计算(level << 3 | mask)\r\nconst FINAL_FORMAT = [\r\n\t0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976 /* L */, 0x5412, 0x5125, 0x5e7c,\r\n\t0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0 /* M */, 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183,\r\n\t0x2eda, 0x2bed /* Q */, 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b /* H */\r\n];\r\n\r\n// Galois域指数表 - 用于纠错码计算的查找表\r\nconst GALOIS_EXPONENT = [\r\n\t0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,\r\n\t0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,\r\n\t0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,\r\n\t0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,\r\n\t0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,\r\n\t0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,\r\n\t0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,\r\n\t0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,\r\n\t0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,\r\n\t0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,\r\n\t0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,\r\n\t0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,\r\n\t0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,\r\n\t0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,\r\n\t0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,\r\n\t0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00\r\n];\r\n\r\n// Galois域对数表 - 用于纠错码计算的反向查找表\r\nconst GALOIS_LOG = [\r\n\t0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,\r\n\t0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,\r\n\t0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,\r\n\t0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,\r\n\t0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,\r\n\t0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,\r\n\t0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,\r\n\t0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,\r\n\t0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,\r\n\t0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,\r\n\t0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,\r\n\t0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,\r\n\t0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,\r\n\t0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,\r\n\t0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,\r\n\t0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf\r\n];\r\n\r\n// 二维码质量评估系数 - 用于计算最佳掩码模式\r\n// N1: 连续5个及以上同色模块的惩罚分数\r\nconst N1 = 3;\r\n// N2: 2x2同色模块区域的惩罚分数\r\nconst N2 = 3;\r\n// N3: 类似定位图形的图案(1:1:3:1:1)的惩罚分数\r\nconst N3 = 40;\r\n// N4: 黑白模块比例不均衡的惩罚分数\r\nconst N4 = 10;\r\n\r\n// 版本信息掩码表 - 用于在二维码中嵌入版本信息\r\nconst VERSION_BLOCK = [\r\n\t0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532,\r\n\t0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5,\r\n\t0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69\r\n];\r\n\r\n/**\r\n * 生成二维码点阵\r\n * @param _str 输入字符串,支持任意文本内容,默认 null 表示空字符串\r\n * @param ecc 纠错级别,可选 'L' | 'M' | 'Q' | 'H',默认 'L'\r\n * @returns {GenerateFrameResult} 返回二维码点阵数据和宽度\r\n */\r\nexport function generateFrame(\r\n\t_str: string | null = null,\r\n\tecc: string | null = null\r\n): GenerateFrameResult {\r\n\t// 变量声明区,所有临时变量、缓冲区\r\n\tlet i: number;\r\n\tlet t: number;\r\n\tlet j: number;\r\n\tlet k: number;\r\n\tlet m: number;\r\n\tlet v: number;\r\n\tlet x: number;\r\n\tlet y: number;\r\n\tlet version: number;\r\n\tlet str = _str == null ? \"\" : _str;\r\n\tlet width = 0;\r\n\t// 获取纠错级别数值\r\n\tlet eccLevel = ECC_LEVELS.get(ecc == null ? \"L\" : ecc)!;\r\n\r\n\t// Data block\r\n\t// 数据块、纠错块、块数\r\n\tlet dataBlock: number;\r\n\tlet eccBlock: number;\r\n\tlet neccBlock1: number;\r\n\tlet neccBlock2: number;\r\n\r\n\t// ECC buffer.\r\n\t// 纠错码缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet eccBuffer: Uint8Array;\r\n\r\n\t// Image buffer.\r\n\t// 二维码点阵缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet frameBuffer = new Uint8Array(0);\r\n\r\n\t// Fixed part of the image.\r\n\t// 点阵掩码缓冲区(标记不可变区域) - 先初始化为空数组,后面会重新赋值\r\n\tlet frameMask = new Uint8Array(0);\r\n\r\n\t// Generator polynomial.\r\n\t// 生成多项式缓冲区(纠错码计算用) - 先初始化为空数组,后面会重新赋值\r\n\tlet polynomial = new Uint8Array(0);\r\n\r\n\t// Data input buffer.\r\n\t// 数据输入缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet stringBuffer = new Uint8Array(0);\r\n\r\n\t/**\r\n\t * 设置掩码位,表示该点为不可变区域(对称处理)\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction setMask(_x: number, _y: number) {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit *= y;\r\n\t\tbit += y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\r\n\t\tframeMask[bit] = 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 添加对齐块,设置对应点阵和掩码\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction addAlignment(_x: number, _y: number) {\r\n\t\tlet i: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tframeBuffer[x + width * y] = 1;\r\n\r\n\t\tfor (i = -2; i < 2; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i + 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i)] = 1;\r\n\t\t\tframeBuffer[x + i + 1 + width * (y + 2)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < 2; i++) {\r\n\t\t\tsetMask(x - 1, y + i);\r\n\t\t\tsetMask(x + 1, y - i);\r\n\t\t\tsetMask(x - i, y - 1);\r\n\t\t\tsetMask(x + i, y + 1);\r\n\t\t}\r\n\r\n\t\tfor (i = 2; i < 4; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i - 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i - 2)] = 1;\r\n\t\t\tframeBuffer[x - 1 + width * (y + i - 2)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Galois 域取模运算\r\n\t * @param _x 输入数值\r\n\t * @returns {number} 取模结果\r\n\t */\r\n\tfunction modN(_x: number): number {\r\n\t\tvar x = _x;\r\n\t\twhile (x >= 255) {\r\n\t\t\tx -= 255;\r\n\t\t\tx = (x >> 8) + (x & 255);\r\n\t\t}\r\n\r\n\t\treturn x;\r\n\t}\r\n\r\n\t/**\r\n\t * 计算并追加纠错码到数据块\r\n\t * @param _data 数据起始索引\r\n\t * @param _dataLength 数据长度\r\n\t * @param _ecc 纠错码起始索引\r\n\t * @param _eccLength 纠错码长度\r\n\t */\r\n\tfunction appendData(_data: number, _dataLength: number, _ecc: number, _eccLength: number) {\r\n\t\tlet bit: number;\r\n\t\tlet i: number;\r\n\t\tlet j: number;\r\n\t\tlet data = _data;\r\n\t\tlet dataLength = _dataLength;\r\n\t\tlet ecc = _ecc;\r\n\t\tlet eccLength = _eccLength;\r\n\r\n\t\tfor (i = 0; i < eccLength; i++) {\r\n\t\t\tstringBuffer[ecc + i] = 0;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < dataLength; i++) {\r\n\t\t\tbit = GALOIS_LOG[stringBuffer[data + i] ^ stringBuffer[ecc]];\r\n\r\n\t\t\tif (bit != 255) {\r\n\t\t\t\tfor (j = 1; j < eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[ecc + j - 1] =\r\n\t\t\t\t\t\tstringBuffer[ecc + j] ^\r\n\t\t\t\t\t\tGALOIS_EXPONENT[modN(bit + polynomial[eccLength - j])];\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tfor (j = ecc; j < ecc + eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[j] = stringBuffer[j + 1];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstringBuffer[ecc + eccLength - 1] =\r\n\t\t\t\tbit == 255 ? 0 : GALOIS_EXPONENT[modN(bit + polynomial[0])];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 判断某点是否为掩码区域\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t * @returns {boolean} 是否为掩码\r\n\t */\r\n\tfunction isMasked(_x: number, _y: number): boolean {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit += y * y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\t\treturn frameMask[bit] == 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 根据 QR Code 标准,应用指定的掩码 pattern\r\n\t * @param mask 掩码编号 (0-7)\r\n\t */\r\n\tfunction applyMask(mask: number) {\r\n\t\tfor (let y = 0; y < width; y++) {\r\n\t\t\tfor (let x = 0; x < width; x++) {\r\n\t\t\t\tif (!isMasked(x, y)) {\r\n\t\t\t\t\tlet shouldInvert = false;\r\n\t\t\t\t\tswitch (mask) {\r\n\t\t\t\t\t\tcase 0:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 1:\r\n\t\t\t\t\t\t\tshouldInvert = y % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 2:\r\n\t\t\t\t\t\t\tshouldInvert = x % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 3:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 4:\r\n\t\t\t\t\t\t\tshouldInvert = (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 5:\r\n\t\t\t\t\t\t\tshouldInvert = ((x * y) % 2) + ((x * y) % 3) == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 6:\r\n\t\t\t\t\t\t\tshouldInvert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 7:\r\n\t\t\t\t\t\t\tshouldInvert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (shouldInvert) {\r\n\t\t\t\t\t\tframeBuffer[x + y * width] ^= 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 计算连续同色块的\"坏度\"分数\r\n\t * @param runLengths\r\n\t * @param length 块长度\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction getBadRuns(runLengths: number[], length: number): number {\r\n\t\tlet badRuns = 0;\r\n\t\tlet i: number;\r\n\r\n\t\tfor (i = 0; i <= length; i++) {\r\n\t\t\tif (i < runLengths.length && runLengths[i] >= 5) {\r\n\t\t\t\tbadRuns += N1 + runLengths[i] - 5;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// FBFFFBF as in finder.\r\n\t\tfor (i = 3; i < length - 1; i += 2) {\r\n\t\t\t// 检查数组索引是否越界\r\n\t\t\tif (i + 2 >= runLengths.length || i - 3 < 0) {\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (\r\n\t\t\t\trunLengths[i - 2] == runLengths[i + 2] &&\r\n\t\t\t\trunLengths[i + 2] == runLengths[i - 1] &&\r\n\t\t\t\trunLengths[i - 1] == runLengths[i + 1] &&\r\n\t\t\t\trunLengths[i - 1] * 3 == runLengths[i] &&\r\n\t\t\t\t// Background around the foreground pattern? Not part of the specs.\r\n\t\t\t\t(runLengths[i - 3] == 0 ||\r\n\t\t\t\t\ti + 3 > length ||\r\n\t\t\t\t\trunLengths[i - 3] * 3 >= runLengths[i] * 4 ||\r\n\t\t\t\t\trunLengths[i + 3] * 3 >= runLengths[i] * 4)\r\n\t\t\t) {\r\n\t\t\t\tbadRuns += N3;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn badRuns;\r\n\t}\r\n\r\n\t/**\r\n\t * 评估当前二维码点阵的整体\"坏度\"\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction checkBadness(): number {\r\n\t\tlet b: number;\r\n\t\tlet b1: number;\r\n\t\tlet bad = 0;\r\n\t\tlet big: number;\r\n\t\tlet bw = 0;\r\n\t\tlet count = 0;\r\n\t\tlet h: number;\r\n\t\tlet x: number;\r\n\t\tlet y: number;\r\n\t\t// 优化:在函数内创建badBuffer,避免外部变量的内存泄漏风险\r\n\t\tlet badBuffer = new Array<number>(width);\r\n\r\n\t\t// Blocks of same colour.\r\n\t\tfor (y = 0; y < width - 1; y++) {\r\n\t\t\tfor (x = 0; x < width - 1; x++) {\r\n\t\t\t\t// All foreground colour.\r\n\t\t\t\tif (\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 1) ||\r\n\t\t\t\t\t// All background colour.\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 0)\r\n\t\t\t\t) {\r\n\t\t\t\t\tbad += N2;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// X runs\r\n\t\tfor (y = 0; y < width; y++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (x = 0; x < width; x++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t\tbw += b > 0 ? 1 : -1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\tif (bw < 0) bw = -bw;\r\n\r\n\t\tbig = bw;\r\n\t\tbig += big << 2;\r\n\t\tbig <<= 1;\r\n\r\n\t\twhile (big > width * width) {\r\n\t\t\tbig -= width * width;\r\n\t\t\tcount++;\r\n\t\t}\r\n\r\n\t\tbad += count * N4;\r\n\r\n\t\t// Y runs.\r\n\t\tfor (x = 0; x < width; x++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (y = 0; y < width; y++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\treturn bad;\r\n\t}\r\n\r\n\t/**\r\n\t * 将字符串转为 UTF-8 编码,兼容多平台\r\n\t * @param str 输入字符串\r\n\t * @returns {string} UTF-8 编码字符串\r\n\t */\r\n\tfunction toUtf8(str: string): string {\r\n\t\tlet out = \"\";\r\n\t\tlet i: number;\r\n\t\tlet len: number;\r\n\t\tlet c: number;\r\n\t\tlen = str.length;\r\n\t\tfor (i = 0; i < len; i++) {\r\n\t\t\tc = str.charCodeAt(i)!;\r\n\t\t\tif (c >= 0x0001 && c <= 0x007f) {\r\n\t\t\t\tout += str.charAt(i);\r\n\t\t\t} else if (c > 0x07ff) {\r\n\t\t\t\tout += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t} else {\r\n\t\t\t\tout += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\t//end functions\r\n\r\n\t// Find the smallest version that fits the string.\r\n\t// 1. 字符串转 UTF-8,计算长度\r\n\tstr = toUtf8(str);\r\n\tt = str.length;\r\n\r\n\t// 2. 自动选择最小可用版本\r\n\tversion = 0;\r\n\tdo {\r\n\t\tversion++;\r\n\t\tk = (eccLevel - 1) * 4 + (version - 1) * 16;\r\n\t\tneccBlock1 = ECC_BLOCKS[k++];\r\n\t\tneccBlock2 = ECC_BLOCKS[k++];\r\n\t\tdataBlock = ECC_BLOCKS[k++];\r\n\t\teccBlock = ECC_BLOCKS[k];\r\n\r\n\t\tk = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2 - 3 + (version <= 9 ? 1 : 0);\r\n\r\n\t\tif (t <= k) break;\r\n\t} while (version < 40);\r\n\r\n\t// FIXME: Ensure that it fits insted of being truncated.\r\n\t// 3. 计算二维码宽度\r\n\twidth = 17 + 4 * version;\r\n\r\n\t// Allocate, clear and setup data structures.\r\n\t// 4. 分配缓冲区, 使用定长的 Uint8Array 优化内存\r\n\tv = dataBlock + (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\teccBuffer = new Uint8Array(v);\r\n\tstringBuffer = new Uint8Array(v);\r\n\r\n\t// 5. 预分配点阵、掩码缓冲区\r\n\tframeBuffer = new Uint8Array(width * width);\r\n\tframeMask = new Uint8Array(Math.floor((width * (width + 1) + 1) / 2));\r\n\r\n\t// Insert finders: Foreground colour to frame and background to mask.\r\n\t// 插入定位点: 前景色为二维码,背景色为掩码\r\n\tfor (t = 0; t < 3; t++) {\r\n\t\tk = 0;\r\n\t\ty = 0;\r\n\t\tif (t == 1) k = width - 7;\r\n\t\tif (t == 2) y = width - 7;\r\n\r\n\t\tframeBuffer[y + 3 + width * (k + 3)] = 1;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tframeBuffer[y + x + width * k] = 1;\r\n\t\t\tframeBuffer[y + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 6 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 6)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (x = 1; x < 5; x++) {\r\n\t\t\tsetMask(y + x, k + 1);\r\n\t\t\tsetMask(y + 1, k + x + 1);\r\n\t\t\tsetMask(y + 5, k + x);\r\n\t\t\tsetMask(y + x + 1, k + 5);\r\n\t\t}\r\n\r\n\t\tfor (x = 2; x < 4; x++) {\r\n\t\t\tframeBuffer[y + x + width * (k + 2)] = 1;\r\n\t\t\tframeBuffer[y + 2 + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 4 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 4)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Alignment blocks.\r\n\t// 插入对齐点: 前景色为二维码,背景色为掩码\r\n\tif (version > 1) {\r\n\t\tt = ALIGNMENT_DELTA[version];\r\n\t\ty = width - 7;\r\n\r\n\t\tfor (; ;) {\r\n\t\t\tx = width - 7;\r\n\r\n\t\t\twhile (x > t - 3) {\r\n\t\t\t\taddAlignment(x, y);\r\n\r\n\t\t\t\tif (x < t) break;\r\n\r\n\t\t\t\tx -= t;\r\n\t\t\t}\r\n\r\n\t\t\tif (y <= t + 9) break;\r\n\r\n\t\t\ty -= t;\r\n\r\n\t\t\taddAlignment(6, y);\r\n\t\t\taddAlignment(y, 6);\r\n\t\t}\r\n\t}\r\n\r\n\t// Single foreground cell.\r\n\t// 插入单个前景色单元格: 前景色为二维码,背景色为掩码\r\n\tframeBuffer[8 + width * (width - 8)] = 1;\r\n\r\n\t// Timing gap (mask only).\r\n\t// 插入时间间隔: 掩码\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(7, y);\r\n\t\tsetMask(width - 8, y);\r\n\t\tsetMask(7, y + width - 7);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x, 7);\r\n\t\tsetMask(x + width - 8, 7);\r\n\t\tsetMask(x, width - 8);\r\n\t}\r\n\r\n\t// Reserve mask, format area.\r\n\t// 保留掩码,格式化区域\r\n\tfor (x = 0; x < 9; x++) {\r\n\t\tsetMask(x, 8);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x + width - 8, 8);\r\n\t\tsetMask(8, x);\r\n\t}\r\n\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(8, y + width - 7);\r\n\t}\r\n\r\n\t// Timing row/column.\r\n\t// 插入时间间隔行/列: 掩码\r\n\tfor (x = 0; x < width - 14; x++) {\r\n\t\tif ((x & 1) > 0) {\r\n\t\t\tsetMask(8 + x, 6);\r\n\t\t\tsetMask(6, 8 + x);\r\n\t\t} else {\r\n\t\t\tframeBuffer[8 + x + width * 6] = 1;\r\n\t\t\tframeBuffer[6 + width * (8 + x)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Version block.\r\n\tif (version > 6) {\r\n\t\tt = VERSION_BLOCK[version - 7];\r\n\t\tk = 17;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tfor (y = 0; y < 3; y++) {\r\n\t\t\t\tif ((1 & (k > 11 ? version >> (k - 12) : t >> k)) > 0) {\r\n\t\t\t\t\tframeBuffer[5 - x + width * (2 - y + width - 11)] = 1;\r\n\t\t\t\t\tframeBuffer[2 - y + width - 11 + width * (5 - x)] = 1;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsetMask(5 - x, 2 - y + width - 11);\r\n\t\t\t\t\tsetMask(2 - y + width - 11, 5 - x);\r\n\t\t\t\t}\r\n\t\t\t\tk--;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Sync mask bits. Only set above for background cells, so now add the foreground.\r\n\t// 同步掩码位。只有上方的背景单元格需要设置,现在添加前景色。\r\n\tfor (y = 0; y < width; y++) {\r\n\t\tfor (x = 0; x <= y; x++) {\r\n\t\t\tif (frameBuffer[x + width * y] > 0) {\r\n\t\t\t\tsetMask(x, y);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Convert string to bit stream. 8-bit data to QR-coded 8-bit data (numeric, alphanum, or kanji\r\n\t// not supported).\r\n\t// 将字符串转换为位流。8位数据转换为QR编码的8位数据(不支持数字、字母或汉字)。\r\n\tv = str.length;\r\n\r\n\t// String to array.\r\n\tfor (i = 0; i < v; i++) {\r\n\t\teccBuffer[i] = str.charCodeAt(i)!;\r\n\t}\r\n\r\n\t//++++++++++++++++++++==============\r\n\tstringBuffer.set(eccBuffer.subarray(0, v));\r\n\r\n\t// Calculate max string length.\r\n\tx = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tif (v >= x - 2) {\r\n\t\tv = x - 2;\r\n\r\n\t\tif (version > 9) v--;\r\n\t}\r\n\r\n\t// Shift and re-pack to insert length prefix.\r\n\t// 移位并重新打包以插入长度前缀。\r\n\ti = v;\r\n\r\n\tif (version > 9) {\r\n\t\tstringBuffer[i + 2] = 0;\r\n\t\tstringBuffer[i + 3] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 3] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 2] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[2] |= 255 & (v << 4);\r\n\t\tstringBuffer[1] = v >> 4;\r\n\t\tstringBuffer[0] = 0x40 | (v >> 12);\r\n\t} else {\r\n\t\tstringBuffer[i + 1] = 0;\r\n\t\tstringBuffer[i + 2] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 2] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 1] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[1] |= 255 & (v << 4);\r\n\t\tstringBuffer[0] = 0x40 | (v >> 4);\r\n\t}\r\n\r\n\t// Fill to end with pad pattern.\r\n\t// 用填充模式填充到结束。\r\n\ti = v + 3 - (version < 10 ? 1 : 0);\r\n\r\n\twhile (i < x) {\r\n\t\tstringBuffer[i++] = 0xec;\r\n\t\tstringBuffer[i++] = 0x11;\r\n\t}\r\n\r\n\t// Calculate generator polynomial.\r\n\t// 计算生成多项式。\r\n\tpolynomial = new Uint8Array(eccBlock + 1);\r\n\tpolynomial[0] = 1;\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i + 1] = 1;\r\n\r\n\t\tfor (j = i; j > 0; j--) {\r\n\t\t\tpolynomial[j] =\r\n\t\t\t\tpolynomial[j] > 0\r\n\t\t\t\t\t? polynomial[j - 1] ^ GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[j]] + i)]\r\n\t\t\t\t\t: polynomial[j - 1];\r\n\t\t}\r\n\r\n\t\tpolynomial[0] = GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[0]] + i)];\r\n\t}\r\n\r\n\t// Use logs for generator polynomial to save calculation step.\r\n\t// 使用对数计算生成多项式以节省计算步骤。\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i] = GALOIS_LOG[polynomial[i]];\r\n\t}\r\n\r\n\t// Append ECC to data buffer.\r\n\t// 将ECC附加到数据缓冲区。\r\n\tk = x;\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < neccBlock1; i++) {\r\n\t\tappendData(y, dataBlock, k, eccBlock);\r\n\r\n\t\ty += dataBlock;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\tfor (i = 0; i < neccBlock2; i++) {\r\n\t\tappendData(y, dataBlock + 1, k, eccBlock);\r\n\r\n\t\ty += dataBlock + 1;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\t// Interleave blocks.\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < dataBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[i + j * dataBlock];\r\n\t\t}\r\n\r\n\t\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t\t}\r\n\t}\r\n\r\n\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t}\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1 + neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[x + i + j * eccBlock];\r\n\t\t}\r\n\t}\r\n\r\n\tstringBuffer.set(eccBuffer);\r\n\r\n\t// Pack bits into frame avoiding masked area.\r\n\t// 将位流打包到帧中,避免掩码区域。\r\n\tx = width - 1;\r\n\ty = width - 1;\r\n\tk = 1;\r\n\tv = 1;\r\n\r\n\t// inteleaved data and ECC codes.\r\n\t// 交错数据和ECC代码。\r\n\tm = (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tfor (i = 0; i < m; i++) {\r\n\t\tt = stringBuffer[i];\r\n\r\n\t\tfor (j = 0; j < 8; j++) {\r\n\t\t\tif ((0x80 & t) > 0) {\r\n\t\t\t\tframeBuffer[x + width * y] = 1;\r\n\t\t\t}\r\n\r\n\t\t\t// Find next fill position.\r\n\t\t\t// 找到下一个填充位置。\r\n\t\t\tdo {\r\n\t\t\t\tif (v > 0) {\r\n\t\t\t\t\tx--;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tx++;\r\n\r\n\t\t\t\t\tif (k > 0) {\r\n\t\t\t\t\t\tif (y != 0) {\r\n\t\t\t\t\t\t\ty--;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty = 9;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (y != width - 1) {\r\n\t\t\t\t\t\t\ty++;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty -= 8;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tv = v == 0 ? 1 : 0;\r\n\t\t\t} while (isMasked(x, y));\r\n\t\t\tt <<= 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Save pre-mask copy of frame.\r\n\tconst frameBufferCopy = frameBuffer.slice(0);\r\n\r\n\tt = 0;\r\n\ty = 30000;\r\n\r\n\t// Using `for` instead of `while` since in original Arduino code if an early mask was *good\r\n\t// enough* it wouldn't try for a better one since they get more complex and take longer.\r\n\t// 使用`for`而不是`while`,因为在原始Arduino代码中,如果早期掩码足够好,它不会尝试更好的掩码,因为它们变得更复杂并需要更长的时间。\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\t// Returns foreground-background imbalance.\r\n\t\t// 返回前景色和背景色的不平衡。\r\n\t\tapplyMask(k);\r\n\r\n\t\tx = checkBadness();\r\n\r\n\t\t// Is current mask better than previous best?\r\n\t\t// 当前掩码是否比之前的最佳掩码更好?\r\n\t\tif (x < y) {\r\n\t\t\ty = x;\r\n\t\t\tt = k;\r\n\t\t}\r\n\r\n\t\t// Don't increment `i` to a void redoing mask.\r\n\t\t// 不要增加`i`以避免重新做掩码。\r\n\t\tif (t == 7) break;\r\n\r\n\t\t// Reset for next pass.\r\n\t\t// 重置下一个循环。\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t}\r\n\r\n\t// Redo best mask as none were *good enough* (i.e. last wasn't `t`).\r\n\t// 重做最佳掩码,因为没有一个掩码足够好(即最后一个不是`t`)。\r\n\tif (t != k) {\r\n\t\t// Reset buffer to pre-mask state before applying the best one\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t\tapplyMask(t);\r\n\t}\r\n\r\n\t// Add in final mask/ECC level bytes.\r\n\t// 添加最终的掩码/ECC级别字节。\r\n\ty = FINAL_FORMAT[t + ((eccLevel - 1) << 3)];\r\n\r\n\t// Low byte.\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[width - 1 - k + width * 8] = 1;\r\n\r\n\t\t\tif (k < 6) {\r\n\t\t\t\tframeBuffer[8 + width * k] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[8 + width * (k + 1)] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// High byte.\r\n\tfor (k = 0; k < 7; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[8 + width * (width - 7 + k)] = 1;\r\n\r\n\t\t\tif (k > 0) {\r\n\t\t\t\tframeBuffer[6 - k + width * 8] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[7 + width * 8] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// Finally, return the image data.\r\n\treturn {\r\n\t\tframeBuffer: frameBuffer,\r\n\t\twidth: width\r\n\t} as GenerateFrameResult;\r\n}\r\n",
|
|
12
|
+
"target": "web"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "reborn-qrcode.config.ts",
|
|
16
|
+
"content": "export default {\r\n slots: {\r\n root: 'relative',\r\n },\r\n}\r\n\r\nexport const qrcodeMode = ['rect', 'circular', 'line', 'rectSmall']\r\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "RebornQrcode.vue",
|
|
20
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClQrcodeMode } from './types'\r\nimport { computed } from 'vue'\r\nimport { generateFrame } from './qrcode'\r\nimport { eccLevel } from './types'\r\n\r\ninterface RebornQrcodeOptions {\r\n size?: number\r\n foreground?: string\r\n background?: string\r\n pdColor?: string | null\r\n pdRadius?: number\r\n text?: string\r\n logo?: string\r\n logoSize?: number\r\n padding?: number\r\n logoMargin?: number\r\n logoHideBackgroundDots?: boolean\r\n logoShadow?: boolean\r\n mode?: ClQrcodeMode\r\n ecc?: eccLevel\r\n pdOuterRadius?: number\r\n pdInnerRadius?: number\r\n dotsGradient?: any\r\n dotsImage?: string | null\r\n backgroundGradient?: any\r\n backgroundTransparent?: boolean\r\n logoOptions?: any\r\n cornersSquareGradient?: any\r\n cornersDotGradient?: any\r\n cornersSquareOptions?: any\r\n cornersDotOptions?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornQrcodeOptions>(), {\r\n size: 200,\r\n foreground: '#131313',\r\n background: '#FFFFFF',\r\n pdColor: null,\r\n pdRadius: 10,\r\n text: 'https://cool-js.com/',\r\n logo: '',\r\n logoSize: 40,\r\n padding: 5,\r\n logoMargin: 4,\r\n logoHideBackgroundDots: false,\r\n logoShadow: false,\r\n mode: 'circular',\r\n ecc: eccLevel.H,\r\n pdOuterRadius: undefined,\r\n pdInnerRadius: undefined,\r\n})\r\n\r\nconst frameData = computed(() => generateFrame(props.text, props.ecc))\r\nconst width = computed(() => frameData.value.width)\r\nconst cellSize = computed(() => (props.size - props.padding * 2) / width.value)\r\n\r\ninterface EyeRect {\r\n x: number\r\n y: number\r\n size: number\r\n}\r\n\r\nconst eyeRects = computed((): EyeRect[] => {\r\n const w = width.value\r\n const s = 7\r\n return [\r\n { x: 0, y: 0, size: s },\r\n { x: w - s, y: 0, size: s },\r\n { x: 0, y: w - s, size: s },\r\n ]\r\n})\r\n\r\nfunction isInEye(x: number, y: number) {\r\n return eyeRects.value.some((eye: EyeRect) => x >= eye.x && x < eye.x + eye.size && y >= eye.y && y < eye.y + eye.size)\r\n}\r\n\r\nfunction isInLogo(x: number, y: number) {\r\n if (!props.logo || !props.logoHideBackgroundDots) return false\r\n\r\n const logoPx = {\r\n x1: (props.size - props.logoSize) / 2 - props.logoMargin,\r\n y1: (props.size - props.logoSize) / 2 - props.logoMargin,\r\n x2: (props.size + props.logoSize) / 2 + props.logoMargin,\r\n y2: (props.size + props.logoSize) / 2 + props.logoMargin,\r\n }\r\n\r\n const dotPx = {\r\n x1: props.padding + x * cellSize.value,\r\n y1: props.padding + y * cellSize.value,\r\n x2: props.padding + (x + 1) * cellSize.value,\r\n y2: props.padding + (y + 1) * cellSize.value,\r\n }\r\n\r\n return (\r\n dotPx.x1 < logoPx.x2 &&\r\n dotPx.x2 > logoPx.x1 &&\r\n dotPx.y1 < logoPx.y2 &&\r\n dotPx.y2 > logoPx.y1\r\n )\r\n}\r\n\r\nconst dots = computed(() => {\r\n const arr: Array<{ x: number, y: number }> = []\r\n const frame = frameData.value.frameBuffer\r\n const w = width.value\r\n for (let y = 0; y < w; y++) {\r\n for (let x = 0; x < w; x++) {\r\n const idx = y * w + x\r\n if (frame[idx] && !isInEye(x, y) && !isInLogo(x, y))\r\n arr.push({ x, y })\r\n }\r\n }\r\n return arr\r\n})\r\n\r\nconst eyeCenter = (eye: EyeRect) => {\r\n return { x: eye.x + 2, y: eye.y + 2 }\r\n}\r\n\r\nconst qrcodeId = computed(() => `qr-${Math.random().toString(36).slice(2, 9)}`)\r\n\r\nconst getGradientStops = (gradient: any) => {\r\n if (!gradient || !gradient.colorStops) return []\r\n return gradient.colorStops\r\n}\r\n\r\nconst getGradientCoords = (gradient: any) => {\r\n if (!gradient || !gradient.direction) return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n switch (gradient.direction) {\r\n case 'horizontal': return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n case 'vertical': return { x1: \"0%\", y1: \"0%\", x2: \"0%\", y2: \"100%\" }\r\n case 'diagonal': return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"100%\" }\r\n default: return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n }\r\n}\r\n\r\nconst dotsFill = computed(() => {\r\n if (props.dotsImage) return `url(#${qrcodeId.value}-dots-img)`\r\n if (props.dotsGradient) return `url(#${qrcodeId.value}-dots-grad)`\r\n return props.foreground\r\n})\r\n\r\nconst bgFill = computed(() => {\r\n if (props.backgroundTransparent) return 'transparent'\r\n if (props.backgroundGradient) return `url(#${qrcodeId.value}-bg-grad)`\r\n return props.background\r\n})\r\n\r\nconst cornerSquareFill = computed(() => {\r\n if (props.cornersSquareGradient) return `url(#${qrcodeId.value}-cs-grad)`\r\n if (props.cornersSquareOptions?.color) return props.cornersSquareOptions.color\r\n if (props.pdColor) return props.pdColor\r\n return props.foreground\r\n})\r\n\r\nconst cornerDotFill = computed(() => {\r\n if (props.cornersDotGradient) return `url(#${qrcodeId.value}-cd-grad)`\r\n if (props.cornersDotOptions?.color) return props.cornersDotOptions.color\r\n if (props.pdColor) return props.pdColor\r\n return props.foreground\r\n})\r\n\r\nconst getRoundRectPath = (x: number, y: number, w: number, h: number, r: number) => {\r\n if (r <= 0) return `M ${x} ${y} h ${w} v ${h} h ${-w} Z`\r\n const radius = Math.min(r, w / 2, h / 2)\r\n return `M ${x + radius} ${y} \r\n h ${w - 2 * radius} \r\n a ${radius} ${radius} 0 0 1 ${radius} ${radius} \r\n v ${h - 2 * radius} \r\n a ${radius} ${radius} 0 0 1 ${-radius} ${radius} \r\n h ${-(w - 2 * radius)} \r\n a ${radius} ${radius} 0 0 1 ${-radius} ${-radius} \r\n v ${-(h - 2 * radius)} \r\n a ${radius} ${radius} 0 0 1 ${radius} ${-radius} Z`\r\n}\r\n</script>\r\n\r\n<template>\r\n <div :style=\"{ width: `${size}px`, height: `${size}px` }\" class=\"relative\">\r\n <svg :width=\"size\" :height=\"size\" :viewBox=\"`0 0 ${size} ${size}`\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <defs>\r\n <!-- Dots Gradient -->\r\n <linearGradient v-if=\"dotsGradient\" :id=\"`${qrcodeId}-dots-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(dotsGradient)\">\r\n <stop v-for=\"stop in getGradientStops(dotsGradient)\" :key=\"stop.offset\" :offset=\"`${stop.offset * 100}%`\"\r\n :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Background Gradient -->\r\n <linearGradient v-if=\"backgroundGradient\" :id=\"`${qrcodeId}-bg-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(backgroundGradient)\">\r\n <stop v-for=\"stop in getGradientStops(backgroundGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Corner Square Gradient -->\r\n <linearGradient v-if=\"cornersSquareGradient\" :id=\"`${qrcodeId}-cs-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(cornersSquareGradient)\">\r\n <stop v-for=\"stop in getGradientStops(cornersSquareGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Corner Dot Gradient -->\r\n <linearGradient v-if=\"cornersDotGradient\" :id=\"`${qrcodeId}-cd-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(cornersDotGradient)\">\r\n <stop v-for=\"stop in getGradientStops(cornersDotGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Dots Image Pattern -->\r\n <pattern v-if=\"dotsImage\" :id=\"`${qrcodeId}-dots-img`\" patternUnits=\"userSpaceOnUse\" width=\"100%\" height=\"100%\">\r\n <image :href=\"dotsImage\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"xMidYMid slice\" />\r\n </pattern>\r\n </defs>\r\n\r\n <rect :width=\"size\" :height=\"size\" :fill=\"bgFill\" />\r\n\r\n <template v-for=\"dot in dots\" :key=\"`${dot.x}-${dot.y}`\">\r\n <circle v-if=\"mode === 'circular'\" :cx=\"padding + (dot.x + 0.5) * cellSize\"\r\n :cy=\"padding + (dot.y + 0.5) * cellSize\" :r=\"cellSize * 0.42\" :fill=\"dotsFill\" />\r\n <rect v-else-if=\"mode === 'rectSmall'\" :x=\"padding + dot.x * cellSize + cellSize * 0.2\"\r\n :y=\"padding + dot.y * cellSize + cellSize * 0.2\" :width=\"cellSize * 0.6\" :height=\"cellSize * 0.6\" rx=\"1\"\r\n :fill=\"dotsFill\" />\r\n <rect v-else-if=\"mode === 'line'\" :x=\"padding + dot.x * cellSize\"\r\n :y=\"padding + dot.y * cellSize + cellSize * 0.15\" :width=\"cellSize\" :height=\"cellSize * 0.7\"\r\n :fill=\"dotsFill\" />\r\n <rect v-else :x=\"padding + dot.x * cellSize\" :y=\"padding + dot.y * cellSize\" :width=\"cellSize\"\r\n :height=\"cellSize\" :fill=\"dotsFill\" />\r\n </template>\r\n\r\n <template v-for=\"eye in eyeRects\" :key=\"`${eye.x}-${eye.y}`\">\r\n <!-- Position Ring (Outer 7x7 - Inner 5x5) -->\r\n <path :d=\"`\r\n ${getRoundRectPath(padding + eye.x * cellSize, padding + eye.y * cellSize, eye.size * cellSize, eye.size * cellSize, pdOuterRadius ?? pdRadius)}\r\n ${getRoundRectPath(padding + (eye.x + 1) * cellSize, padding + (eye.y + 1) * cellSize, 5 * cellSize, 5 * cellSize, Math.max(0, (pdOuterRadius ?? pdRadius) - cellSize))}\r\n `\" fill-rule=\"evenodd\" :fill=\"cornerSquareFill\" />\r\n <!-- Inner Center Dot (3x3) -->\r\n <path\r\n :d=\"getRoundRectPath(padding + eyeCenter(eye).x * cellSize, padding + eyeCenter(eye).y * cellSize, 3 * cellSize, 3 * cellSize, pdInnerRadius ?? pdRadius / 2)\"\r\n :fill=\"cornerDotFill\" />\r\n </template>\r\n </svg>\r\n\r\n <img v-if=\"logo\" :src=\"logo\" alt=\"logo\" class=\"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded\"\r\n :style=\"{\r\n width: `${logoSize}px`,\r\n height: `${logoSize}px`,\r\n filter: logoShadow ? 'drop-shadow(0 2px 4px rgba(0,0,0,0.15))' : 'none'\r\n }\">\r\n </div>\r\n</template>\r\n",
|
|
21
|
+
"target": "web"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "types.ts",
|
|
25
|
+
"content": "export type ClQrcodeMode = 'rect' | 'circular' | 'line' | 'rectSmall'\r\n\r\nexport enum eccLevel {\r\n L = 'L',\r\n M = 'M',\r\n Q = 'Q',\r\n H = 'H',\r\n}\r\n"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "draw.ts",
|
|
29
|
+
"content": "/**\r\n * 导入所需的工具函数和依赖\r\n */\r\nimport { generateFrame } from \"./qrcode\";\r\n\r\nexport type ClQrcodeMode = \"rect\" | \"circular\" | \"line\" | \"rectSmall\";\r\n\r\nexport enum eccLevel {\r\n\tL = \"L\",\r\n\tM = \"M\",\r\n\tQ = \"Q\",\r\n\tH = \"H\"\r\n}\r\n\r\nexport type QrcodeOptions = {\r\n\tecc: eccLevel; // 纠错级别\r\n\ttext: string; // 二维码内容\r\n\tsize: number; // 二维码尺寸,单位px\r\n\tforeground: string; // 前景色\r\n\tbackground: string; // 背景色\r\n\tpadding: number; // 内边距\r\n\tlogo: string; // logo图片地址\r\n\tlogoSize: number; // logo尺寸\r\n\tmode: ClQrcodeMode; // 二维码样式模式\r\n\tpdColor: string | null; // 定位点颜色\r\n\tpdRadius: number; // 定位图案圆角半径(兼容旧版)\r\n\tpdOuterRadius?: number; // 码眼外框圆角半径\r\n\tpdInnerRadius?: number; // 码眼内点圆角半径\r\n\tdotsGradient?: any;\r\n\tdotsImage?: string | null;\r\n\tbackgroundGradient?: any;\r\n\tbackgroundTransparent?: boolean;\r\n\tlogoOptions?: any;\r\n\tcornersSquareGradient?: any;\r\n\tcornersDotGradient?: any;\r\n\tcornersSquareOptions?: any;\r\n\tcornersDotOptions?: any;\r\n};\r\n\r\n/**\r\n * 绘制圆角矩形\r\n */\r\nfunction drawRoundedRect(\r\n\tctx: any,\r\n\tx: number,\r\n\ty: number,\r\n\twidth: number,\r\n\theight: number,\r\n\tradius: number\r\n) {\r\n\tif (radius <= 0) {\r\n\t\tctx.fillRect(x, y, width, height);\r\n\t\treturn;\r\n\t}\r\n\r\n\tconst maxRadius = Math.min(width, height) / 2;\r\n\tconst r = Math.min(radius, maxRadius);\r\n\r\n\tctx.beginPath();\r\n\tctx.moveTo(x + r, y);\r\n\tctx.lineTo(x + width - r, y);\r\n\tctx.arcTo(x + width, y, x + width, y + r, r);\r\n\tctx.lineTo(x + width, y + height - r);\r\n\tctx.arcTo(x + width, y + height, x + width - r, y + height, r);\r\n\tctx.lineTo(x + r, y + height);\r\n\tctx.arcTo(x, y + height, x, y + height - r, r);\r\n\tctx.lineTo(x, y + r);\r\n\tctx.arcTo(x, y, x + r, y, r);\r\n\tctx.closePath();\r\n\tctx.fill();\r\n}\r\n\r\nfunction createFillStyle(ctx: any, styleConfig: any, size: number) {\r\n\tif (!styleConfig) return null;\r\n\ttry {\r\n\t\tif (styleConfig.type === 'linear') {\r\n\t\t\tlet x1 = 0, y1 = 0, x2 = 0, y2 = 0;\r\n\t\t\tswitch (styleConfig.direction) {\r\n\t\t\t\tcase 'horizontal': x2 = size; break;\r\n\t\t\t\tcase 'vertical': y2 = size; break;\r\n\t\t\t\tcase 'diagonal': x2 = size; y2 = size; break;\r\n\t\t\t\tcase 'center': x1 = size / 2; y1 = size / 2; x2 = size; y2 = size; break;\r\n\t\t\t\tdefault: x2 = size; break;\r\n\t\t\t}\r\n\t\t\tconst grd = ctx.createLinearGradient(x1, y1, x2, y2);\r\n\t\t\tif (styleConfig.colorStops) {\r\n\t\t\t\tstyleConfig.colorStops.forEach((stop: any) => {\r\n\t\t\t\t\tgrd.addColorStop(stop.offset, stop.color);\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t\treturn grd;\r\n\t\t} else if (styleConfig.type === 'radial') {\r\n\t\t\tconst grd = ctx.createCircularGradient ? ctx.createCircularGradient(size / 2, size / 2, size / 2) : ctx.createLinearGradient(0, 0, size, size);\r\n\t\t\tif (styleConfig.colorStops) {\r\n\t\t\t\tstyleConfig.colorStops.forEach((stop: any) => {\r\n\t\t\t\t\tgrd.addColorStop(stop.offset, stop.color);\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t\treturn grd;\r\n\t\t}\r\n\t} catch (e) {\r\n\t\tconsole.warn('创建渐变失败', e);\r\n\t}\r\n\treturn null;\r\n}\r\n\r\nfunction setFillStyle(ctx: any, fillStyle: any) {\r\n\tif (typeof ctx.setFillStyle === 'function') {\r\n\t\tctx.setFillStyle(fillStyle);\r\n\t\treturn;\r\n\t}\r\n\tctx.fillStyle = fillStyle;\r\n}\r\n\r\nfunction flushDraw(ctx: any) {\r\n\tif (typeof ctx.draw === 'function') {\r\n\t\tctx.draw(false);\r\n\t}\r\n}\r\n\r\nasync function loadImageSource(src: string, canvasNode?: any) {\r\n\tif (canvasNode?.createImage) {\r\n\t\tconst image = canvasNode.createImage();\r\n\t\tawait new Promise<void>((resolve, reject) => {\r\n\t\t\timage.onload = () => resolve();\r\n\t\t\timage.onerror = (err: any) => reject(err);\r\n\t\t\timage.src = src;\r\n\t\t});\r\n\t\treturn image;\r\n\t}\r\n\r\n\tconst imageInfo = await new Promise<any>((resolve, reject) => {\r\n\t\tuni.getImageInfo({\r\n\t\t\tsrc,\r\n\t\t\tsuccess: (res) => {\r\n\t\t\t\tif (\r\n\t\t\t\t\t!res.path.startsWith('http://') &&\r\n\t\t\t\t\t!res.path.startsWith('https://') &&\r\n\t\t\t\t\t!res.path.startsWith('/') &&\r\n\t\t\t\t\t!res.path.startsWith('data:') &&\r\n\t\t\t\t\t!res.path.startsWith('wxfile://')\r\n\t\t\t\t) {\r\n\t\t\t\t\tres.path = '/' + res.path;\r\n\t\t\t\t}\r\n\t\t\t\tresolve(res);\r\n\t\t\t},\r\n\t\t\tfail: reject\r\n\t\t});\r\n\t});\r\n\r\n\treturn imageInfo.path;\r\n}\r\n\r\n/**\r\n * 绘制定位图案(码眼)\r\n */\r\nfunction drawPositionPattern(\r\n\tctx: any,\r\n\tstartX: number,\r\n\tstartY: number,\r\n\tpx: number,\r\n\tpdSquareFillStyle: any,\r\n\tpdDotFillStyle: any,\r\n\tbackground: string,\r\n\touterRadius: number,\r\n\tcenterRadius: number\r\n) {\r\n\tconst patternSize = px * 7;\r\n\r\n\tconst safeOuterRadius = Math.max(0, outerRadius);\r\n\tconst safeCenterRadius = Math.max(0, centerRadius);\r\n\r\n\tsetFillStyle(ctx, pdSquareFillStyle);\r\n\tdrawRoundedRect(ctx, startX, startY, patternSize, patternSize, safeOuterRadius);\r\n\r\n\tif (background !== 'transparent') {\r\n\t\tsetFillStyle(ctx, background);\r\n\t\tconst innerStartX = startX + px;\r\n\t\tconst innerStartY = startY + px;\r\n\t\tconst innerSize = px * 5;\r\n\t\tconst innerRadius = Math.max(0, safeOuterRadius - px);\r\n\t\tdrawRoundedRect(ctx, innerStartX, innerStartY, innerSize, innerSize, innerRadius);\r\n\t} else {\r\n\t\t// 在透明背景下,如果只用上面的方法,内圈会实心。所以我们改用 clip+填充 的方式,或者绘制两层来避免遮挡:\r\n\t\t// 实际上透明背景应该清空内圈,由于 Canvas 是叠加的,这里比较难直接在图案上抠洞,\r\n\t\t// 通常做法是用 clip 或 composite 模式。为了简单处理,透明背景不填充中间层\r\n\t\t// 由于第一步已经填充了实心外框,如果背景透明,中间会是一整块。\r\n\t\t// 这里我们用 clearRect 加上 clear 的圆角实现不太容易。\r\n\t\t// 采用最保险的方式:用 path 绘制一个带有内孔的圆角矩形环\r\n\t}\r\n\r\n\t// 我们修改 drawPositionPattern 的实现,让它支持带洞的圆环和中心的点\r\n\tctx.beginPath();\r\n\r\n\t// 绘制外环(顺时针)\r\n\tconst r = Math.min(safeOuterRadius, patternSize / 2);\r\n\tif (r <= 0) {\r\n\t\tctx.moveTo(startX, startY);\r\n\t\tctx.lineTo(startX + patternSize, startY);\r\n\t\tctx.lineTo(startX + patternSize, startY + patternSize);\r\n\t\tctx.lineTo(startX, startY + patternSize);\r\n\t\tctx.lineTo(startX, startY);\r\n\t} else {\r\n\t\tctx.moveTo(startX + r, startY);\r\n\t\tctx.lineTo(startX + patternSize - r, startY);\r\n\t\tctx.arcTo(startX + patternSize, startY, startX + patternSize, startY + r, r);\r\n\t\tctx.lineTo(startX + patternSize, startY + patternSize - r);\r\n\t\tctx.arcTo(startX + patternSize, startY + patternSize, startX + patternSize - r, startY + patternSize, r);\r\n\t\tctx.lineTo(startX + r, startY + patternSize);\r\n\t\tctx.arcTo(startX, startY + patternSize, startX, startY + patternSize - r, r);\r\n\t\tctx.lineTo(startX, startY + r);\r\n\t\tctx.arcTo(startX, startY, startX + r, startY, r);\r\n\t}\r\n\r\n\t// 绘制内环(逆时针)以便留空\r\n\tconst innerStartX = startX + px;\r\n\tconst innerStartY = startY + px;\r\n\tconst innerSize = px * 5;\r\n\tconst innerR = Math.max(0, r - px);\r\n\r\n\tif (innerR <= 0) {\r\n\t\tctx.moveTo(innerStartX, innerStartY);\r\n\t\tctx.lineTo(innerStartX, innerStartY + innerSize);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY + innerSize);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY);\r\n\t\tctx.lineTo(innerStartX, innerStartY);\r\n\t} else {\r\n\t\tctx.moveTo(innerStartX + innerR, innerStartY);\r\n\t\tctx.arcTo(innerStartX, innerStartY, innerStartX, innerStartY + innerR, innerR);\r\n\t\tctx.lineTo(innerStartX, innerStartY + innerSize - innerR);\r\n\t\tctx.arcTo(innerStartX, innerStartY + innerSize, innerStartX + innerR, innerStartY + innerSize, innerR);\r\n\t\tctx.lineTo(innerStartX + innerSize - innerR, innerStartY + innerSize);\r\n\t\tctx.arcTo(innerStartX + innerSize, innerStartY + innerSize, innerStartX + innerSize, innerStartY + innerSize - innerR, innerR);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY + innerR);\r\n\t\tctx.arcTo(innerStartX + innerSize, innerStartY, innerStartX + innerSize - innerR, innerStartY, innerR);\r\n\t}\r\n\tctx.closePath();\r\n\r\n\tsetFillStyle(ctx, pdSquareFillStyle);\r\n\tctx.fill('evenodd'); // 用 evenodd 填充带洞的多边形\r\n\r\n\t// 绘制中心点\r\n\tsetFillStyle(ctx, pdDotFillStyle);\r\n\tconst centerStartX = startX + px * 2;\r\n\tconst centerStartY = startY + px * 2;\r\n\tconst centerSize = px * 3;\r\n\tdrawRoundedRect(ctx, centerStartX, centerStartY, centerSize, centerSize, safeCenterRadius);\r\n}\r\n\r\n/**\r\n * 在二维码中心绘制Logo\r\n */\r\nfunction drawLogo(ctx: any, options: QrcodeOptions, imageSource: any) {\r\n\tctx.save();\r\n\r\n\tconst contentSize = options.size - options.padding * 2;\r\n\tconst contentCenterX = options.padding + contentSize / 2;\r\n\tconst contentCenterY = options.padding + contentSize / 2;\r\n\r\n\tlet logoSize = options.logoSize;\r\n\tif (options.logoOptions && options.logoOptions.size) {\r\n\t\tswitch (options.logoOptions.size) {\r\n\t\t\tcase 'small': logoSize = 30; break;\r\n\t\t\tcase 'large': logoSize = 60; break;\r\n\t\t\tdefault: logoSize = 45; break; // medium\r\n\t\t}\r\n\t}\r\n\r\n\tlet backgroundPadding = 3;\r\n\tif (options.logoOptions && options.logoOptions.margin) {\r\n\t\tswitch (options.logoOptions.margin) {\r\n\t\t\tcase 'none': backgroundPadding = 0; break;\r\n\t\t\tcase 'small': backgroundPadding = 3; break;\r\n\t\t\tcase 'medium': backgroundPadding = 6; break;\r\n\t\t\tcase 'large': backgroundPadding = 10; break;\r\n\t\t}\r\n\t}\r\n\r\n\tconst backgroundSize = logoSize + backgroundPadding * 2;\r\n\tconst backgroundX = contentCenterX - backgroundSize / 2;\r\n\tconst backgroundY = contentCenterY - backgroundSize / 2;\r\n\r\n\tlet cornerRadius = Math.min(backgroundSize * 0.1, 6);\r\n\tif (options.logoOptions && options.logoOptions.shape) {\r\n\t\tswitch (options.logoOptions.shape) {\r\n\t\t\tcase 'rectangle': cornerRadius = 0; break;\r\n\t\t\tcase 'circle': cornerRadius = backgroundSize / 2; break;\r\n\t\t\tcase 'rounded-rectangle': cornerRadius = Math.min(backgroundSize * 0.2, 12); break;\r\n\t\t}\r\n\t}\r\n\r\n\tif (options.logoOptions && options.logoOptions.shadow) {\r\n\t\tctx.shadowColor = 'rgba(0, 0, 0, 0.2)';\r\n\t\tctx.shadowBlur = 10;\r\n\t\tctx.shadowOffsetX = 0;\r\n\t\tctx.shadowOffsetY = 4;\r\n\t}\r\n\r\n\t// 绘制Logo背景\r\n\tif (!options.logoOptions || options.logoOptions.hideBackgroundDots !== false) {\r\n\t\tsetFillStyle(ctx, options.backgroundTransparent ? '#ffffff' : options.background);\r\n\t\tdrawRoundedRect(ctx, backgroundX, backgroundY, backgroundSize, backgroundSize, cornerRadius);\r\n\t}\r\n\r\n\t// 重置阴影,避免影响图片\r\n\tctx.shadowColor = 'transparent';\r\n\tctx.shadowBlur = 0;\r\n\tctx.shadowOffsetX = 0;\r\n\tctx.shadowOffsetY = 0;\r\n\r\n\t// 绘制图片(如果需要裁剪的话可以加clip)\r\n\tconst logoX = contentCenterX - logoSize / 2;\r\n\tconst logoY = contentCenterY - logoSize / 2;\r\n\r\n\tif (cornerRadius > 0) {\r\n\t\tctx.save();\r\n\t\tctx.beginPath();\r\n\t\tconst innerRadius = Math.max(0, cornerRadius - backgroundPadding);\r\n\t\tctx.moveTo(logoX + innerRadius, logoY);\r\n\t\tctx.lineTo(logoX + logoSize - innerRadius, logoY);\r\n\t\tctx.arcTo(logoX + logoSize, logoY, logoX + logoSize, logoY + innerRadius, innerRadius);\r\n\t\tctx.lineTo(logoX + logoSize, logoY + logoSize - innerRadius);\r\n\t\tctx.arcTo(logoX + logoSize, logoY + logoSize, logoX + logoSize - innerRadius, logoY + logoSize, innerRadius);\r\n\t\tctx.lineTo(logoX + innerRadius, logoY + logoSize);\r\n\t\tctx.arcTo(logoX, logoY + logoSize, logoX, logoY + logoSize - innerRadius, innerRadius);\r\n\t\tctx.lineTo(logoX, logoY + innerRadius);\r\n\t\tctx.arcTo(logoX, logoY, logoX + innerRadius, logoY, innerRadius);\r\n\t\tctx.closePath();\r\n\t\tctx.clip();\r\n\t}\r\n\r\n\tctx.drawImage(imageSource, logoX, logoY, logoSize, logoSize);\r\n\r\n\tif (cornerRadius > 0) {\r\n\t\tctx.restore();\r\n\t}\r\n\r\n\tctx.restore();\r\n}\r\n\r\n/**\r\n * 绘制二维码到Canvas上下文\r\n */\r\nexport async function drawQrcode(ctx: any, options: QrcodeOptions, canvasNode?: any) {\r\n\tif (!ctx) return;\r\n\r\n\t// 生成二维码数据矩阵\r\n\tconst frame = generateFrame(options.text, options.ecc);\r\n\tconst points = frame.frameBuffer; // 点阵数据\r\n\tconst width = frame.width; // 矩阵宽度\r\n\r\n\t// 计算二维码内容区域大小\r\n\tconst contentSize = options.size - options.padding * 2;\r\n\tconst px = contentSize / width;\r\n\tconst offsetX = options.padding;\r\n\tconst offsetY = options.padding;\r\n\r\n\t// 绘制整个画布背景\r\n\tif (options.backgroundTransparent) {\r\n\t\tctx.clearRect(0, 0, options.size, options.size);\r\n\t} else {\r\n\t\tconst bgGradient = createFillStyle(ctx, options.backgroundGradient, options.size);\r\n\t\tsetFillStyle(ctx, bgGradient || options.background);\r\n\t\tctx.fillRect(0, 0, options.size, options.size);\r\n\t}\r\n\r\n\tfunction isPositionDetectionPattern(i: number, j: number, width: number): boolean {\r\n\t\tif (i < 7 && j < 7) return true; // 左上角\r\n\t\tif (i > width - 8 && j < 7) return true; // 右上角\r\n\t\tif (i < 7 && j > width - 8) return true; // 左下角\r\n\t\treturn false;\r\n\t}\r\n\r\n\tfunction isInLogoArea(\r\n\t\ti: number,\r\n\t\tj: number,\r\n\t\twidth: number,\r\n\t\tbaseLogoSize: number,\r\n\t\tpx: number\r\n\t): boolean {\r\n\t\tlet logoSize = baseLogoSize;\r\n\t\tif (options.logoOptions && options.logoOptions.size) {\r\n\t\t\tswitch (options.logoOptions.size) {\r\n\t\t\t\tcase 'small': logoSize = 30; break;\r\n\t\t\t\tcase 'large': logoSize = 60; break;\r\n\t\t\t\tdefault: logoSize = 45; break; // medium\r\n\t\t\t}\r\n\t\t}\r\n\t\tlet backgroundPadding = 3;\r\n\t\tif (options.logoOptions && options.logoOptions.margin) {\r\n\t\t\tswitch (options.logoOptions.margin) {\r\n\t\t\t\tcase 'none': backgroundPadding = 0; break;\r\n\t\t\t\tcase 'small': backgroundPadding = 3; break;\r\n\t\t\t\tcase 'medium': backgroundPadding = 6; break;\r\n\t\t\t\tcase 'large': backgroundPadding = 10; break;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (logoSize <= 0) return false;\r\n\r\n\t\tconst maxLogoRatio = 0.3;\r\n\t\tconst maxLogoPoints = Math.floor(width * maxLogoRatio);\r\n\t\tconst logoPoints = Math.min(Math.ceil((logoSize + backgroundPadding * 2) / px), maxLogoPoints);\r\n\r\n\t\tconst buffer = logoPoints > width * 0.1 ? 1 : 0;\r\n\t\tconst totalLogoPoints = logoPoints + buffer * 2;\r\n\r\n\t\tconst centerI = Math.floor(width / 2);\r\n\t\tconst centerJ = Math.floor(width / 2);\r\n\r\n\t\tconst halfSize = Math.floor(totalLogoPoints / 2);\r\n\t\tconst minI = centerI - halfSize;\r\n\t\tconst maxI = centerI + halfSize;\r\n\t\tconst minJ = centerJ - halfSize;\r\n\t\tconst maxJ = centerJ + halfSize;\r\n\r\n\t\treturn i >= minI && i <= maxI && j >= minJ && j <= maxJ;\r\n\t}\r\n\r\n\tconst pdColor = options.pdColor ?? options.foreground;\r\n\r\n\tconst baseRadius = options.pdRadius;\r\n\tconst outerRadius = options.pdOuterRadius !== undefined ? options.pdOuterRadius : baseRadius;\r\n\tconst centerRadius = options.pdInnerRadius !== undefined ? options.pdInnerRadius : Math.max(0, outerRadius - px * 2);\r\n\r\n\tconst fgGradient = createFillStyle(ctx, options.dotsGradient, options.size);\r\n\tconst fgStyle = fgGradient || options.foreground;\r\n\r\n\tconst pdSquareGradient = options.cornersSquareGradient || options.dotsGradient;\r\n\tconst pdDotGradient = options.cornersDotGradient || options.dotsGradient;\r\n\r\n\tconst pdSquareFillStyle = createFillStyle(ctx, pdSquareGradient, options.size) || (options.cornersSquareOptions && options.cornersSquareOptions.color) || ((options.pdColor && options.pdColor !== options.foreground) ? options.pdColor : fgStyle);\r\n\tconst pdDotFillStyle = createFillStyle(ctx, pdDotGradient, options.size) || (options.cornersDotOptions && options.cornersDotOptions.color) || ((options.pdColor && options.pdColor !== options.foreground) ? options.pdColor : fgStyle);\r\n\r\n\tdrawPositionPattern(ctx, offsetX, offsetY, px, pdSquareFillStyle, pdDotFillStyle, options.backgroundTransparent ? 'transparent' : options.background, outerRadius, centerRadius);\r\n\tdrawPositionPattern(\r\n\t\tctx,\r\n\t\toffsetX + (width - 7) * px,\r\n\t\toffsetY,\r\n\t\tpx,\r\n\t\tpdSquareFillStyle,\r\n\t\tpdDotFillStyle,\r\n\t\toptions.backgroundTransparent ? 'transparent' : options.background,\r\n\t\touterRadius,\r\n\t\tcenterRadius\r\n\t);\r\n\tdrawPositionPattern(\r\n\t\tctx,\r\n\t\toffsetX,\r\n\t\toffsetY + (width - 7) * px,\r\n\t\tpx,\r\n\t\tpdSquareFillStyle,\r\n\t\tpdDotFillStyle,\r\n\t\toptions.backgroundTransparent ? 'transparent' : options.background,\r\n\t\touterRadius,\r\n\t\tcenterRadius\r\n\t);\r\n\r\n\tconst dot = px * 0.1;\r\n\r\n\t// 加载前景图\r\n\tlet fgImageSource: any = null;\r\n\tif (options.dotsImage) {\r\n\t\ttry {\r\n\t\t\tlet src = options.dotsImage;\r\n\t\t\t// #ifdef MP-WEIXIN\r\n\t\t\t// 对于 http/https 或以 /、data: 开头的路径,直接使用原路径\r\n\t\t\tif (\r\n\t\t\t\t!src.startsWith('http://') &&\r\n\t\t\t\t!src.startsWith('https://') &&\r\n\t\t\t\t!src.startsWith('/') &&\r\n\t\t\t\t!src.startsWith('data:')\r\n\t\t\t) {\r\n\t\t\t\t// 微信小程序中 uni.getImageInfo 需要的是绝对路径,如果是 static/xx 则补上 /\r\n\t\t\t\tsrc = '/' + src;\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\t\t\tfgImageSource = await loadImageSource(src, canvasNode);\r\n\t\t} catch (e) {\r\n\t\t\tconsole.error(\"加载前景图失败\", e);\r\n\t\t}\r\n\t}\r\n\r\n\tif (fgImageSource) {\r\n\t\tctx.save();\r\n\t\tctx.beginPath();\r\n\t}\r\n\r\n\tfor (let i = 0; i < width; i++) {\r\n\t\tfor (let j = 0; j < width; j++) {\r\n\t\t\tif (points[j * width + i] > 0) {\r\n\t\t\t\tif (isPositionDetectionPattern(i, j, width)) {\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (options.logo != \"\" && isInLogoArea(i, j, width, options.logoSize, px)) {\r\n\t\t\t\t\tif (!options.logoOptions || options.logoOptions.hideBackgroundDots !== false) {\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (!fgImageSource) {\r\n\t\t\t\t\tsetFillStyle(ctx, fgStyle);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconst x = offsetX + px * i;\r\n\t\t\t\tconst y = offsetY + px * j;\r\n\r\n\t\t\t\tswitch (options.mode) {\r\n\t\t\t\t\tcase \"line\":\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x, y, px, px / 2);\r\n\t\t\t\t\t\telse ctx.fillRect(x, y, px, px / 2);\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase \"circular\":\r\n\t\t\t\t\t\tif (!fgImageSource) ctx.beginPath();\r\n\t\t\t\t\t\tconst rx = x + px / 2 - dot;\r\n\t\t\t\t\t\tconst ry = y + px / 2 - dot;\r\n\t\t\t\t\t\tif (fgImageSource) ctx.moveTo(rx + px / 2 - dot, ry);\r\n\t\t\t\t\t\tctx.arc(rx, ry, px / 2 - dot, 0, 2 * Math.PI);\r\n\t\t\t\t\t\tif (!fgImageSource) {\r\n\t\t\t\t\t\t\tctx.fill();\r\n\t\t\t\t\t\t\tctx.closePath();\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase \"rectSmall\":\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x + dot, y + dot, px - dot * 2, px - dot * 2);\r\n\t\t\t\t\t\telse ctx.fillRect(x + dot, y + dot, px - dot * 2, px - dot * 2);\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tdefault:\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x, y, px, px);\r\n\t\t\t\t\t\telse ctx.fillRect(x, y, px, px);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tif (fgImageSource) {\r\n\t\tctx.clip();\r\n\t\tctx.drawImage(fgImageSource, 0, 0, options.size, options.size);\r\n\t\tctx.restore();\r\n\t}\r\n\r\n\tif (options.logo != \"\") {\r\n\t\ttry {\r\n\t\t\tlet logoSrc = options.logo;\r\n\r\n\t\t\t// #ifdef H5\r\n\t\t\tif (logoSrc.startsWith(\"http\")) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tlogoSrc = await new Promise<string>((resolve, reject) => {\r\n\t\t\t\t\t\tconst img = new Image();\r\n\t\t\t\t\t\timg.crossOrigin = \"Anonymous\";\r\n\t\t\t\t\t\timg.onload = () => {\r\n\t\t\t\t\t\t\tconst canvas = document.createElement(\"canvas\");\r\n\t\t\t\t\t\t\tcanvas.width = img.width;\r\n\t\t\t\t\t\t\tcanvas.height = img.height;\r\n\t\t\t\t\t\t\tconst context = canvas.getContext(\"2d\");\r\n\t\t\t\t\t\t\tcontext?.drawImage(img, 0, 0);\r\n\t\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\t\tconst dataUrl = canvas.toDataURL(\"image/png\");\r\n\t\t\t\t\t\t\t\tresolve(dataUrl);\r\n\t\t\t\t\t\t\t} catch (err) {\r\n\t\t\t\t\t\t\t\treject(err);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t\timg.onerror = reject;\r\n\t\t\t\t\t\timg.src = logoSrc;\r\n\t\t\t\t\t});\r\n\t\t\t\t} catch (e) {\r\n\t\t\t\t\tconsole.warn(\"Logo CORS load failed, using original URL. Canvas export may fail.\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\r\n\t\t\t// #ifdef MP-WEIXIN\r\n\t\t\t// 对于 http/https 或以 /、data: 开头的路径,直接使用原路径\r\n\t\t\tif (\r\n\t\t\t\t!logoSrc.startsWith('http://') &&\r\n\t\t\t\t!logoSrc.startsWith('https://') &&\r\n\t\t\t\t!logoSrc.startsWith('/') &&\r\n\t\t\t\t!logoSrc.startsWith('data:')\r\n\t\t\t) {\r\n\t\t\t\t// 微信小程序中 uni.getImageInfo 需要的是绝对路径,如果是 static/xx 则补上 /\r\n\t\t\t\tlogoSrc = '/' + logoSrc;\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\r\n\t\t\tconst logoImageSource = await loadImageSource(logoSrc, canvasNode);\r\n\t\t\tdrawLogo(ctx, options, logoImageSource);\r\n\t\t} catch (err) {\r\n\t\t\tconsole.error(\"二维码 Logo 加载失败\", err);\r\n\t\t}\r\n\t}\r\n\r\n\t// 执行绘制\r\n\tflushDraw(ctx);\r\n}\r\n",
|
|
30
|
+
"target": "uniapp"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "qrcode.ts",
|
|
34
|
+
"content": "export type GenerateFrameResult = {\r\n\tframeBuffer: Uint8Array;\r\n\twidth: number;\r\n};\r\n\r\n/**\r\n * 二维码生成器\r\n * @description 纯 UTS 实现的二维码生成算法,支持多平台,兼容 uni-app x。核心算法参考 QR Code 标准,支持自定义纠错级别、自动适配内容长度。\r\n * @version 1.0.0\r\n * @平台兼容性 App、H5、微信小程序、UTS\r\n * @注意事项\r\n * - 仅支持 8bit 字符串内容,不支持数字/字母/汉字等模式优化\r\n * - 生成结果为二维码点阵数据和宽度,需配合 canvas 绘制\r\n * - 纠错级别支持 'L'/'M'/'Q'/'H',默认 'L'\r\n */\r\n\r\n// 对齐块间距表 - 不同版本二维码的对齐块分布位置\r\nconst ALIGNMENT_DELTA = [\r\n\t0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,\r\n\t26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28\r\n] as number[];\r\n\r\n// 纠错块参数表 - 每个版本包含4个参数:块数、数据宽度、纠错宽度\r\nconst ECC_BLOCKS = [\r\n\t1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22,\r\n\t1, 0, 16, 28, 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32,\r\n\t18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, 2, 0,\r\n\t68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,\r\n\t1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22,\r\n\t4, 4, 16, 20, 4, 4, 12, 24, 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81,\r\n\t20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4,\r\n\t14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,\r\n\t11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98,\r\n\t24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2,\r\n\t17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, 3, 4, 113, 28, 3, 11, 44,\r\n\t26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,\r\n\t4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16,\r\n\t24, 30, 34, 0, 13, 24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117,\r\n\t30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30,\r\n\t22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8, 4, 122, 30, 22,\r\n\t3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31,\r\n\t15, 30, 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47,\r\n\t28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15,\r\n\t30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1, 115, 30, 14, 21, 46,\r\n\t28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16,\r\n\t30, 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47,\r\n\t28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15,\r\n\t30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20, 4, 117, 30, 40, 7, 47,\r\n\t28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15,\r\n\t30\r\n] as number[];\r\n\r\n// 纠错级别映射表 - 将人类可读的纠错级别映射为内部数值\r\nconst ECC_LEVELS = new Map<string, number>([\r\n\t[\"L\", 1],\r\n\t[\"M\", 2],\r\n\t[\"Q\", 3],\r\n\t[\"H\", 4]\r\n]);\r\n\r\n// 最终格式信息掩码表 - 用于格式信息区域的掩码计算(level << 3 | mask)\r\nconst FINAL_FORMAT = [\r\n\t0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976 /* L */, 0x5412, 0x5125, 0x5e7c,\r\n\t0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0 /* M */, 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183,\r\n\t0x2eda, 0x2bed /* Q */, 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b /* H */\r\n];\r\n\r\n// Galois域指数表 - 用于纠错码计算的查找表\r\nconst GALOIS_EXPONENT = [\r\n\t0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,\r\n\t0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,\r\n\t0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,\r\n\t0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,\r\n\t0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,\r\n\t0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,\r\n\t0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,\r\n\t0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,\r\n\t0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,\r\n\t0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,\r\n\t0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,\r\n\t0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,\r\n\t0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,\r\n\t0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,\r\n\t0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,\r\n\t0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00\r\n];\r\n\r\n// Galois域对数表 - 用于纠错码计算的反向查找表\r\nconst GALOIS_LOG = [\r\n\t0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,\r\n\t0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,\r\n\t0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,\r\n\t0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,\r\n\t0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,\r\n\t0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,\r\n\t0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,\r\n\t0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,\r\n\t0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,\r\n\t0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,\r\n\t0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,\r\n\t0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,\r\n\t0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,\r\n\t0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,\r\n\t0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,\r\n\t0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf\r\n];\r\n\r\n// 二维码质量评估系数 - 用于计算最佳掩码模式\r\n// N1: 连续5个及以上同色模块的惩罚分数\r\nconst N1 = 3;\r\n// N2: 2x2同色模块区域的惩罚分数\r\nconst N2 = 3;\r\n// N3: 类似定位图形的图案(1:1:3:1:1)的惩罚分数\r\nconst N3 = 40;\r\n// N4: 黑白模块比例不均衡的惩罚分数\r\nconst N4 = 10;\r\n\r\n// 版本信息掩码表 - 用于在二维码中嵌入版本信息\r\nconst VERSION_BLOCK = [\r\n\t0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532,\r\n\t0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5,\r\n\t0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69\r\n];\r\n\r\n/**\r\n * 生成二维码点阵\r\n * @param _str 输入字符串,支持任意文本内容,默认 null 表示空字符串\r\n * @param ecc 纠错级别,可选 'L' | 'M' | 'Q' | 'H',默认 'L'\r\n * @returns {GenerateFrameResult} 返回二维码点阵数据和宽度\r\n */\r\nexport function generateFrame(\r\n\t_str: string | null = null,\r\n\tecc: string | null = null\r\n): GenerateFrameResult {\r\n\t// 变量声明区,所有临时变量、缓冲区\r\n\tlet i: number;\r\n\tlet t: number;\r\n\tlet j: number;\r\n\tlet k: number;\r\n\tlet m: number;\r\n\tlet v: number;\r\n\tlet x: number;\r\n\tlet y: number;\r\n\tlet version: number;\r\n\tlet str = _str == null ? \"\" : _str;\r\n\tlet width = 0;\r\n\t// 获取纠错级别数值\r\n\tlet eccLevel = ECC_LEVELS.get(ecc == null ? \"L\" : ecc)!;\r\n\r\n\t// Data block\r\n\t// 数据块、纠错块、块数\r\n\tlet dataBlock: number;\r\n\tlet eccBlock: number;\r\n\tlet neccBlock1: number;\r\n\tlet neccBlock2: number;\r\n\r\n\t// ECC buffer.\r\n\t// 纠错码缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet eccBuffer: Uint8Array;\r\n\r\n\t// Image buffer.\r\n\t// 二维码点阵缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet frameBuffer = new Uint8Array(0);\r\n\r\n\t// Fixed part of the image.\r\n\t// 点阵掩码缓冲区(标记不可变区域) - 先初始化为空数组,后面会重新赋值\r\n\tlet frameMask = new Uint8Array(0);\r\n\r\n\t// Generator polynomial.\r\n\t// 生成多项式缓冲区(纠错码计算用) - 先初始化为空数组,后面会重新赋值\r\n\tlet polynomial = new Uint8Array(0);\r\n\r\n\t// Data input buffer.\r\n\t// 数据输入缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet stringBuffer = new Uint8Array(0);\r\n\r\n\t/**\r\n\t * 设置掩码位,表示该点为不可变区域(对称处理)\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction setMask(_x: number, _y: number) {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit *= y;\r\n\t\tbit += y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\r\n\t\tframeMask[bit] = 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 添加对齐块,设置对应点阵和掩码\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction addAlignment(_x: number, _y: number) {\r\n\t\tlet i: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tframeBuffer[x + width * y] = 1;\r\n\r\n\t\tfor (i = -2; i < 2; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i + 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i)] = 1;\r\n\t\t\tframeBuffer[x + i + 1 + width * (y + 2)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < 2; i++) {\r\n\t\t\tsetMask(x - 1, y + i);\r\n\t\t\tsetMask(x + 1, y - i);\r\n\t\t\tsetMask(x - i, y - 1);\r\n\t\t\tsetMask(x + i, y + 1);\r\n\t\t}\r\n\r\n\t\tfor (i = 2; i < 4; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i - 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i - 2)] = 1;\r\n\t\t\tframeBuffer[x - 1 + width * (y + i - 2)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Galois 域取模运算\r\n\t * @param _x 输入数值\r\n\t * @returns {number} 取模结果\r\n\t */\r\n\tfunction modN(_x: number): number {\r\n\t\tvar x = _x;\r\n\t\twhile (x >= 255) {\r\n\t\t\tx -= 255;\r\n\t\t\tx = (x >> 8) + (x & 255);\r\n\t\t}\r\n\r\n\t\treturn x;\r\n\t}\r\n\r\n\t/**\r\n\t * 计算并追加纠错码到数据块\r\n\t * @param _data 数据起始索引\r\n\t * @param _dataLength 数据长度\r\n\t * @param _ecc 纠错码起始索引\r\n\t * @param _eccLength 纠错码长度\r\n\t */\r\n\tfunction appendData(_data: number, _dataLength: number, _ecc: number, _eccLength: number) {\r\n\t\tlet bit: number;\r\n\t\tlet i: number;\r\n\t\tlet j: number;\r\n\t\tlet data = _data;\r\n\t\tlet dataLength = _dataLength;\r\n\t\tlet ecc = _ecc;\r\n\t\tlet eccLength = _eccLength;\r\n\r\n\t\tfor (i = 0; i < eccLength; i++) {\r\n\t\t\tstringBuffer[ecc + i] = 0;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < dataLength; i++) {\r\n\t\t\tbit = GALOIS_LOG[stringBuffer[data + i] ^ stringBuffer[ecc]];\r\n\r\n\t\t\tif (bit != 255) {\r\n\t\t\t\tfor (j = 1; j < eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[ecc + j - 1] =\r\n\t\t\t\t\t\tstringBuffer[ecc + j] ^\r\n\t\t\t\t\t\tGALOIS_EXPONENT[modN(bit + polynomial[eccLength - j])];\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tfor (j = ecc; j < ecc + eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[j] = stringBuffer[j + 1];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstringBuffer[ecc + eccLength - 1] =\r\n\t\t\t\tbit == 255 ? 0 : GALOIS_EXPONENT[modN(bit + polynomial[0])];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 判断某点是否为掩码区域\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t * @returns {boolean} 是否为掩码\r\n\t */\r\n\tfunction isMasked(_x: number, _y: number): boolean {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit += y * y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\t\treturn frameMask[bit] == 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 根据 QR Code 标准,应用指定的掩码 pattern\r\n\t * @param mask 掩码编号 (0-7)\r\n\t */\r\n\tfunction applyMask(mask: number) {\r\n\t\tfor (let y = 0; y < width; y++) {\r\n\t\t\tfor (let x = 0; x < width; x++) {\r\n\t\t\t\tif (!isMasked(x, y)) {\r\n\t\t\t\t\tlet shouldInvert = false;\r\n\t\t\t\t\tswitch (mask) {\r\n\t\t\t\t\t\tcase 0:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 1:\r\n\t\t\t\t\t\t\tshouldInvert = y % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 2:\r\n\t\t\t\t\t\t\tshouldInvert = x % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 3:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 4:\r\n\t\t\t\t\t\t\tshouldInvert = (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 5:\r\n\t\t\t\t\t\t\tshouldInvert = ((x * y) % 2) + ((x * y) % 3) == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 6:\r\n\t\t\t\t\t\t\tshouldInvert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 7:\r\n\t\t\t\t\t\t\tshouldInvert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (shouldInvert) {\r\n\t\t\t\t\t\tframeBuffer[x + y * width] ^= 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 计算连续同色块的\"坏度\"分数\r\n\t * @param runLengths\r\n\t * @param length 块长度\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction getBadRuns(runLengths: number[], length: number): number {\r\n\t\tlet badRuns = 0;\r\n\t\tlet i: number;\r\n\r\n\t\tfor (i = 0; i <= length; i++) {\r\n\t\t\tif (i < runLengths.length && runLengths[i] >= 5) {\r\n\t\t\t\tbadRuns += N1 + runLengths[i] - 5;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// FBFFFBF as in finder.\r\n\t\tfor (i = 3; i < length - 1; i += 2) {\r\n\t\t\t// 检查数组索引是否越界\r\n\t\t\tif (i + 2 >= runLengths.length || i - 3 < 0) {\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (\r\n\t\t\t\trunLengths[i - 2] == runLengths[i + 2] &&\r\n\t\t\t\trunLengths[i + 2] == runLengths[i - 1] &&\r\n\t\t\t\trunLengths[i - 1] == runLengths[i + 1] &&\r\n\t\t\t\trunLengths[i - 1] * 3 == runLengths[i] &&\r\n\t\t\t\t// Background around the foreground pattern? Not part of the specs.\r\n\t\t\t\t(runLengths[i - 3] == 0 ||\r\n\t\t\t\t\ti + 3 > length ||\r\n\t\t\t\t\trunLengths[i - 3] * 3 >= runLengths[i] * 4 ||\r\n\t\t\t\t\trunLengths[i + 3] * 3 >= runLengths[i] * 4)\r\n\t\t\t) {\r\n\t\t\t\tbadRuns += N3;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn badRuns;\r\n\t}\r\n\r\n\t/**\r\n\t * 评估当前二维码点阵的整体\"坏度\"\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction checkBadness(): number {\r\n\t\tlet b: number;\r\n\t\tlet b1: number;\r\n\t\tlet bad = 0;\r\n\t\tlet big: number;\r\n\t\tlet bw = 0;\r\n\t\tlet count = 0;\r\n\t\tlet h: number;\r\n\t\tlet x: number;\r\n\t\tlet y: number;\r\n\t\t// 优化:在函数内创建badBuffer,避免外部变量的内存泄漏风险\r\n\t\tlet badBuffer = new Array<number>(width);\r\n\r\n\t\t// Blocks of same colour.\r\n\t\tfor (y = 0; y < width - 1; y++) {\r\n\t\t\tfor (x = 0; x < width - 1; x++) {\r\n\t\t\t\t// All foreground colour.\r\n\t\t\t\tif (\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 1) ||\r\n\t\t\t\t\t// All background colour.\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 0)\r\n\t\t\t\t) {\r\n\t\t\t\t\tbad += N2;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// X runs\r\n\t\tfor (y = 0; y < width; y++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (x = 0; x < width; x++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t\tbw += b > 0 ? 1 : -1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\tif (bw < 0) bw = -bw;\r\n\r\n\t\tbig = bw;\r\n\t\tbig += big << 2;\r\n\t\tbig <<= 1;\r\n\r\n\t\twhile (big > width * width) {\r\n\t\t\tbig -= width * width;\r\n\t\t\tcount++;\r\n\t\t}\r\n\r\n\t\tbad += count * N4;\r\n\r\n\t\t// Y runs.\r\n\t\tfor (x = 0; x < width; x++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (y = 0; y < width; y++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\treturn bad;\r\n\t}\r\n\r\n\t/**\r\n\t * 将字符串转为 UTF-8 编码,兼容多平台\r\n\t * @param str 输入字符串\r\n\t * @returns {string} UTF-8 编码字符串\r\n\t */\r\n\tfunction toUtf8(str: string): string {\r\n\t\tlet out = \"\";\r\n\t\tlet i: number;\r\n\t\tlet len: number;\r\n\t\tlet c: number;\r\n\t\tlen = str.length;\r\n\t\tfor (i = 0; i < len; i++) {\r\n\t\t\tc = str.charCodeAt(i)!;\r\n\t\t\tif (c >= 0x0001 && c <= 0x007f) {\r\n\t\t\t\tout += str.charAt(i);\r\n\t\t\t} else if (c > 0x07ff) {\r\n\t\t\t\tout += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t} else {\r\n\t\t\t\tout += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\t//end functions\r\n\r\n\t// Find the smallest version that fits the string.\r\n\t// 1. 字符串转 UTF-8,计算长度\r\n\tstr = toUtf8(str);\r\n\tt = str.length;\r\n\r\n\t// 2. 自动选择最小可用版本\r\n\tversion = 0;\r\n\tdo {\r\n\t\tversion++;\r\n\t\tk = (eccLevel - 1) * 4 + (version - 1) * 16;\r\n\t\tneccBlock1 = ECC_BLOCKS[k++];\r\n\t\tneccBlock2 = ECC_BLOCKS[k++];\r\n\t\tdataBlock = ECC_BLOCKS[k++];\r\n\t\teccBlock = ECC_BLOCKS[k];\r\n\r\n\t\tk = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2 - 3 + (version <= 9 ? 1 : 0);\r\n\r\n\t\tif (t <= k) break;\r\n\t} while (version < 40);\r\n\r\n\t// FIXME: Ensure that it fits insted of being truncated.\r\n\t// 3. 计算二维码宽度\r\n\twidth = 17 + 4 * version;\r\n\r\n\t// Allocate, clear and setup data structures.\r\n\t// 4. 分配缓冲区, 使用定长的 Uint8Array 优化内存\r\n\tv = dataBlock + (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\teccBuffer = new Uint8Array(v);\r\n\tstringBuffer = new Uint8Array(v);\r\n\r\n\t// 5. 预分配点阵、掩码缓冲区\r\n\tframeBuffer = new Uint8Array(width * width);\r\n\tframeMask = new Uint8Array(Math.floor((width * (width + 1) + 1) / 2));\r\n\r\n\t// Insert finders: Foreground colour to frame and background to mask.\r\n\t// 插入定位点: 前景色为二维码,背景色为掩码\r\n\tfor (t = 0; t < 3; t++) {\r\n\t\tk = 0;\r\n\t\ty = 0;\r\n\t\tif (t == 1) k = width - 7;\r\n\t\tif (t == 2) y = width - 7;\r\n\r\n\t\tframeBuffer[y + 3 + width * (k + 3)] = 1;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tframeBuffer[y + x + width * k] = 1;\r\n\t\t\tframeBuffer[y + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 6 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 6)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (x = 1; x < 5; x++) {\r\n\t\t\tsetMask(y + x, k + 1);\r\n\t\t\tsetMask(y + 1, k + x + 1);\r\n\t\t\tsetMask(y + 5, k + x);\r\n\t\t\tsetMask(y + x + 1, k + 5);\r\n\t\t}\r\n\r\n\t\tfor (x = 2; x < 4; x++) {\r\n\t\t\tframeBuffer[y + x + width * (k + 2)] = 1;\r\n\t\t\tframeBuffer[y + 2 + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 4 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 4)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Alignment blocks.\r\n\t// 插入对齐点: 前景色为二维码,背景色为掩码\r\n\tif (version > 1) {\r\n\t\tt = ALIGNMENT_DELTA[version];\r\n\t\ty = width - 7;\r\n\r\n\t\tfor (;;) {\r\n\t\t\tx = width - 7;\r\n\r\n\t\t\twhile (x > t - 3) {\r\n\t\t\t\taddAlignment(x, y);\r\n\r\n\t\t\t\tif (x < t) break;\r\n\r\n\t\t\t\tx -= t;\r\n\t\t\t}\r\n\r\n\t\t\tif (y <= t + 9) break;\r\n\r\n\t\t\ty -= t;\r\n\r\n\t\t\taddAlignment(6, y);\r\n\t\t\taddAlignment(y, 6);\r\n\t\t}\r\n\t}\r\n\r\n\t// Single foreground cell.\r\n\t// 插入单个前景色单元格: 前景色为二维码,背景色为掩码\r\n\tframeBuffer[8 + width * (width - 8)] = 1;\r\n\r\n\t// Timing gap (mask only).\r\n\t// 插入时间间隔: 掩码\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(7, y);\r\n\t\tsetMask(width - 8, y);\r\n\t\tsetMask(7, y + width - 7);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x, 7);\r\n\t\tsetMask(x + width - 8, 7);\r\n\t\tsetMask(x, width - 8);\r\n\t}\r\n\r\n\t// Reserve mask, format area.\r\n\t// 保留掩码,格式化区域\r\n\tfor (x = 0; x < 9; x++) {\r\n\t\tsetMask(x, 8);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x + width - 8, 8);\r\n\t\tsetMask(8, x);\r\n\t}\r\n\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(8, y + width - 7);\r\n\t}\r\n\r\n\t// Timing row/column.\r\n\t// 插入时间间隔行/列: 掩码\r\n\tfor (x = 0; x < width - 14; x++) {\r\n\t\tif ((x & 1) > 0) {\r\n\t\t\tsetMask(8 + x, 6);\r\n\t\t\tsetMask(6, 8 + x);\r\n\t\t} else {\r\n\t\t\tframeBuffer[8 + x + width * 6] = 1;\r\n\t\t\tframeBuffer[6 + width * (8 + x)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Version block.\r\n\tif (version > 6) {\r\n\t\tt = VERSION_BLOCK[version - 7];\r\n\t\tk = 17;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tfor (y = 0; y < 3; y++) {\r\n\t\t\t\tif ((1 & (k > 11 ? version >> (k - 12) : t >> k)) > 0) {\r\n\t\t\t\t\tframeBuffer[5 - x + width * (2 - y + width - 11)] = 1;\r\n\t\t\t\t\tframeBuffer[2 - y + width - 11 + width * (5 - x)] = 1;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsetMask(5 - x, 2 - y + width - 11);\r\n\t\t\t\t\tsetMask(2 - y + width - 11, 5 - x);\r\n\t\t\t\t}\r\n\t\t\t\tk--;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Sync mask bits. Only set above for background cells, so now add the foreground.\r\n\t// 同步掩码位。只有上方的背景单元格需要设置,现在添加前景色。\r\n\tfor (y = 0; y < width; y++) {\r\n\t\tfor (x = 0; x <= y; x++) {\r\n\t\t\tif (frameBuffer[x + width * y] > 0) {\r\n\t\t\t\tsetMask(x, y);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Convert string to bit stream. 8-bit data to QR-coded 8-bit data (numeric, alphanum, or kanji\r\n\t// not supported).\r\n\t// 将字符串转换为位流。8位数据转换为QR编码的8位数据(不支持数字、字母或汉字)。\r\n\tv = str.length;\r\n\r\n\t// String to array.\r\n\tfor (i = 0; i < v; i++) {\r\n\t\t// #ifdef APP-ANDROID\r\n\t\t// @ts-ignore\r\n\t\teccBuffer[i.toInt()] = str.charCodeAt(i)!;\r\n\t\t// #endif\r\n\t\t// #ifndef APP-ANDROID\r\n\t\teccBuffer[i] = str.charCodeAt(i)!;\r\n\t\t// #endif\r\n\t}\r\n\r\n\t//++++++++++++++++++++==============\r\n\tstringBuffer.set(eccBuffer.subarray(0, v));\r\n\r\n\t// Calculate max string length.\r\n\tx = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tif (v >= x - 2) {\r\n\t\tv = x - 2;\r\n\r\n\t\tif (version > 9) v--;\r\n\t}\r\n\r\n\t// Shift and re-pack to insert length prefix.\r\n\t// 移位并重新打包以插入长度前缀。\r\n\ti = v;\r\n\r\n\tif (version > 9) {\r\n\t\tstringBuffer[i + 2] = 0;\r\n\t\tstringBuffer[i + 3] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 3] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 2] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[2] |= 255 & (v << 4);\r\n\t\tstringBuffer[1] = v >> 4;\r\n\t\tstringBuffer[0] = 0x40 | (v >> 12);\r\n\t} else {\r\n\t\tstringBuffer[i + 1] = 0;\r\n\t\tstringBuffer[i + 2] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 2] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 1] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[1] |= 255 & (v << 4);\r\n\t\tstringBuffer[0] = 0x40 | (v >> 4);\r\n\t}\r\n\r\n\t// Fill to end with pad pattern.\r\n\t// 用填充模式填充到结束。\r\n\ti = v + 3 - (version < 10 ? 1 : 0);\r\n\r\n\twhile (i < x) {\r\n\t\tstringBuffer[i++] = 0xec;\r\n\t\tstringBuffer[i++] = 0x11;\r\n\t}\r\n\r\n\t// Calculate generator polynomial.\r\n\t// 计算生成多项式。\r\n\tpolynomial = new Uint8Array(eccBlock + 1);\r\n\tpolynomial[0] = 1;\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i + 1] = 1;\r\n\r\n\t\tfor (j = i; j > 0; j--) {\r\n\t\t\tpolynomial[j] =\r\n\t\t\t\tpolynomial[j] > 0\r\n\t\t\t\t\t? polynomial[j - 1] ^ GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[j]] + i)]\r\n\t\t\t\t\t: polynomial[j - 1];\r\n\t\t}\r\n\r\n\t\tpolynomial[0] = GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[0]] + i)];\r\n\t}\r\n\r\n\t// Use logs for generator polynomial to save calculation step.\r\n\t// 使用对数计算生成多项式以节省计算步骤。\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i] = GALOIS_LOG[polynomial[i]];\r\n\t}\r\n\r\n\t// Append ECC to data buffer.\r\n\t// 将ECC附加到数据缓冲区。\r\n\tk = x;\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < neccBlock1; i++) {\r\n\t\tappendData(y, dataBlock, k, eccBlock);\r\n\r\n\t\ty += dataBlock;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\tfor (i = 0; i < neccBlock2; i++) {\r\n\t\tappendData(y, dataBlock + 1, k, eccBlock);\r\n\r\n\t\ty += dataBlock + 1;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\t// Interleave blocks.\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < dataBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[i + j * dataBlock];\r\n\t\t}\r\n\r\n\t\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t\t}\r\n\t}\r\n\r\n\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t}\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1 + neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[x + i + j * eccBlock];\r\n\t\t}\r\n\t}\r\n\r\n\tstringBuffer.set(eccBuffer);\r\n\r\n\t// Pack bits into frame avoiding masked area.\r\n\t// 将位流打包到帧中,避免掩码区域。\r\n\tx = width - 1;\r\n\ty = width - 1;\r\n\tk = 1;\r\n\tv = 1;\r\n\r\n\t// inteleaved data and ECC codes.\r\n\t// 交错数据和ECC代码。\r\n\tm = (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tfor (i = 0; i < m; i++) {\r\n\t\tt = stringBuffer[i];\r\n\r\n\t\tfor (j = 0; j < 8; j++) {\r\n\t\t\tif ((0x80 & t) > 0) {\r\n\t\t\t\tframeBuffer[x + width * y] = 1;\r\n\t\t\t}\r\n\r\n\t\t\t// Find next fill position.\r\n\t\t\t// 找到下一个填充位置。\r\n\t\t\tdo {\r\n\t\t\t\tif (v > 0) {\r\n\t\t\t\t\tx--;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tx++;\r\n\r\n\t\t\t\t\tif (k > 0) {\r\n\t\t\t\t\t\tif (y != 0) {\r\n\t\t\t\t\t\t\ty--;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty = 9;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (y != width - 1) {\r\n\t\t\t\t\t\t\ty++;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty -= 8;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tv = v == 0 ? 1 : 0;\r\n\t\t\t} while (isMasked(x, y));\r\n\t\t\tt <<= 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Save pre-mask copy of frame.\r\n\tconst frameBufferCopy = frameBuffer.slice(0);\r\n\r\n\tt = 0;\r\n\ty = 30000;\r\n\r\n\t// Using `for` instead of `while` since in original Arduino code if an early mask was *good\r\n\t// enough* it wouldn't try for a better one since they get more complex and take longer.\r\n\t// 使用`for`而不是`while`,因为在原始Arduino代码中,如果早期掩码足够好,它不会尝试更好的掩码,因为它们变得更复杂并需要更长的时间。\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\t// Returns foreground-background imbalance.\r\n\t\t// 返回前景色和背景色的不平衡。\r\n\t\tapplyMask(k);\r\n\r\n\t\tx = checkBadness();\r\n\r\n\t\t// Is current mask better than previous best?\r\n\t\t// 当前掩码是否比之前的最佳掩码更好?\r\n\t\tif (x < y) {\r\n\t\t\ty = x;\r\n\t\t\tt = k;\r\n\t\t}\r\n\r\n\t\t// Don't increment `i` to a void redoing mask.\r\n\t\t// 不要增加`i`以避免重新做掩码。\r\n\t\tif (t == 7) break;\r\n\r\n\t\t// Reset for next pass.\r\n\t\t// 重置下一个循环。\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t}\r\n\r\n\t// Redo best mask as none were *good enough* (i.e. last wasn't `t`).\r\n\t// 重做最佳掩码,因为没有一个掩码足够好(即最后一个不是`t`)。\r\n\tif (t != k) {\r\n\t\t// Reset buffer to pre-mask state before applying the best one\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t\tapplyMask(t);\r\n\t}\r\n\r\n\t// Add in final mask/ECC level bytes.\r\n\t// 添加最终的掩码/ECC级别字节。\r\n\ty = FINAL_FORMAT[t + ((eccLevel - 1) << 3)];\r\n\r\n\t// Low byte.\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[width - 1 - k + width * 8] = 1;\r\n\r\n\t\t\tif (k < 6) {\r\n\t\t\t\tframeBuffer[8 + width * k] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[8 + width * (k + 1)] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// High byte.\r\n\tfor (k = 0; k < 7; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[8 + width * (width - 7 + k)] = 1;\r\n\r\n\t\t\tif (k > 0) {\r\n\t\t\t\tframeBuffer[6 - k + width * 8] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[7 + width * 8] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// Finally, return the image data.\r\n\treturn {\r\n\t\tframeBuffer: frameBuffer,\r\n\t\twidth: width\r\n\t} as GenerateFrameResult;\r\n}\r\n",
|
|
35
|
+
"target": "uniapp"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "RebornQrcode.vue",
|
|
39
|
+
"content": "<template>\r\n <view :style=\"{ width: size + 'px', height: size + 'px' }\">\r\n <!-- #ifdef MP-WEIXIN -->\r\n <canvas class=\"relative z-1\" type=\"2d\" :id=\"qrcodeId\"\r\n :style=\"{ width: size + 'px', height: size + 'px' }\"></canvas>\r\n <!-- #endif -->\r\n <!-- #ifndef MP-WEIXIN -->\r\n <canvas :canvas-id=\"qrcodeId\" :id=\"qrcodeId\" :style=\"{ width: size + 'px', height: size + 'px' }\"></canvas>\r\n <!-- #endif -->\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport {\r\n ref,\r\n watch,\r\n onMounted,\r\n getCurrentInstance,\r\n nextTick,\r\n computed,\r\n onUnmounted,\r\n} from \"vue\";\r\n\r\nimport { drawQrcode, eccLevel, type ClQrcodeMode } from \"./draw\";\r\nimport { canvasToPng, uuid } from \"../../lib/file\";\r\nimport { isAppIOS, isHarmony } from \"../../lib/device\";\r\n\r\ndefineOptions({\r\n name: \"reborn-qrcode\",\r\n inheritAttrs: false\r\n});\r\n\r\ninterface RebornQrcodeOptions {\r\n size?: number;\r\n foreground?: string;\r\n background?: string;\r\n pdColor?: string | null;\r\n pdRadius?: number;\r\n text?: string;\r\n logo?: string;\r\n logoSize?: number;\r\n padding?: number;\r\n mode?: ClQrcodeMode;\r\n ecc?: eccLevel;\r\n pdOuterRadius?: number;\r\n pdInnerRadius?: number;\r\n dotsGradient?: any;\r\n dotsImage?: string | null;\r\n backgroundGradient?: any;\r\n backgroundTransparent?: boolean;\r\n logoOptions?: any;\r\n cornersSquareGradient?: any;\r\n cornersDotGradient?: any;\r\n cornersSquareOptions?: any;\r\n cornersDotOptions?: any;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornQrcodeOptions>(), {\r\n size: 200,\r\n foreground: \"#131313\",\r\n background: \"#FFFFFF\",\r\n pdColor: null,\r\n pdRadius: 10,\r\n text: \"https://cool-js.com/\",\r\n logo: \"\",\r\n logoSize: 40,\r\n padding: 5,\r\n mode: \"circular\",\r\n ecc: eccLevel.H,\r\n pdOuterRadius: undefined,\r\n pdInnerRadius: undefined\r\n});\r\n\r\n\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\n// 二维码组件id\r\nconst qrcodeId = ref<string>(\"cl-qrcode-\" + uuid());\r\n\r\n/**\r\n * 主绘制方法,根据当前 props 生成二维码并绘制到 canvas。\r\n * 支持多平台(APP、H5、微信小程序),自动适配高分屏。\r\n * 内部调用 drawQrcode 进行二维码点阵绘制。\r\n */\r\nfunction drawer() {\r\n const data = {\r\n ecc: props.ecc,\r\n text: props.text,\r\n size: props.size,\r\n foreground: props.foreground,\r\n background: props.background,\r\n padding: props.padding,\r\n logo: props.logo,\r\n logoSize: props.logoSize,\r\n mode: props.mode,\r\n pdColor: props.pdColor,\r\n pdRadius: props.pdRadius,\r\n pdOuterRadius: props.pdOuterRadius,\r\n pdInnerRadius: props.pdInnerRadius,\r\n dotsGradient: props.dotsGradient,\r\n dotsImage: props.dotsImage,\r\n backgroundGradient: props.backgroundGradient,\r\n backgroundTransparent: props.backgroundTransparent,\r\n logoOptions: props.logoOptions,\r\n cornersSquareGradient: props.cornersSquareGradient, // 优先使用外框的渐变\r\n cornersDotGradient: props.cornersDotGradient, // 优先使用内点的渐变\r\n cornersSquareOptions: props.cornersSquareOptions,\r\n cornersDotOptions: props.cornersDotOptions,\r\n };\r\n\r\n nextTick(() => {\r\n // #ifdef MP-WEIXIN\r\n const query = uni.createSelectorQuery().in(proxy as any);\r\n (query.select('#' + qrcodeId.value) as any)\r\n .fields({ node: true, size: true }, (res: any) => {\r\n if (res?.node) {\r\n const canvas = res.node as any;\r\n const ctx = canvas.getContext('2d') as any;\r\n const dpr = uni.getSystemInfoSync().pixelRatio;\r\n canvas.width = props.size * dpr;\r\n canvas.height = props.size * dpr;\r\n ctx.scale(dpr, dpr);\r\n drawQrcode(ctx, data, canvas);\r\n }\r\n })\r\n .exec();\r\n // #endif\r\n\r\n // #ifndef MP-WEIXIN\r\n const context = uni.createCanvasContext(qrcodeId.value, proxy);\r\n drawQrcode(context, data);\r\n // #endif\r\n });\r\n}\r\n/**\r\n * 获取当前二维码图片的临时文件地址\r\n * @param call 回调函数,返回图片路径,失败返回空字符串\r\n */\r\nfunction toPng(): Promise<string> {\r\n return new Promise((resolve, reject) => {\r\n setTimeout(() => {\r\n // #ifdef MP-WEIXIN\r\n const query = uni.createSelectorQuery().in(proxy as any);\r\n (query.select('#' + qrcodeId.value) as any)\r\n .fields({ node: true }, (res: any) => {\r\n if (res?.node) {\r\n const canvas = res.node as any;\r\n (uni.canvasToTempFilePath as any)({\r\n canvas,\r\n destWidth: props.size * 3,\r\n destHeight: props.size * 3,\r\n success: (result: any) => {\r\n resolve(result.tempFilePath);\r\n },\r\n fail: (err: any) => {\r\n reject(err);\r\n }\r\n });\r\n } else {\r\n reject(new Error('未找到 canvas 节点'));\r\n }\r\n })\r\n .exec();\r\n // #endif\r\n\r\n // #ifndef MP-WEIXIN\r\n uni.canvasToTempFilePath({\r\n canvasId: qrcodeId.value,\r\n destWidth: props.size * 3,\r\n destHeight: props.size * 3,\r\n success: (res) => {\r\n resolve(res.tempFilePath);\r\n },\r\n fail: (err) => {\r\n reject(err);\r\n }\r\n }, proxy);\r\n // #endif\r\n }, 100);\r\n });\r\n}\r\n\r\n// 自动重绘\r\nconst stopWatch = watch(\r\n computed(() => [\r\n props.pdColor,\r\n props.pdRadius,\r\n props.pdOuterRadius,\r\n props.pdInnerRadius,\r\n props.dotsGradient,\r\n props.dotsImage,\r\n props.backgroundGradient,\r\n props.backgroundTransparent,\r\n props.logoOptions,\r\n props.cornersSquareGradient,\r\n props.cornersDotGradient,\r\n props.cornersSquareOptions,\r\n props.cornersDotOptions,\r\n props.foreground,\r\n props.background,\r\n props.text,\r\n props.logo,\r\n props.logoSize,\r\n props.mode,\r\n props.padding,\r\n props.ecc\r\n ]),\r\n () => {\r\n drawer();\r\n }\r\n);\r\n\r\nonMounted(() => {\r\n setTimeout(\r\n () => {\r\n drawer();\r\n },\r\n isHarmony() || isAppIOS() ? 50 : 0\r\n );\r\n});\r\n\r\nonUnmounted(() => {\r\n stopWatch();\r\n});\r\n\r\ndefineExpose({\r\n toPng\r\n});\r\n</script>\r\n",
|
|
40
|
+
"target": "uniapp"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"fileCount": 8,
|
|
44
|
+
"contentHash": "048934914f316f22efac89b3c126f44762d35fd6"
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-radio",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"clsx"
|
|
5
|
+
],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "index.ts",
|
|
9
|
+
"content": "export { default as RebornRadio } from \"./RebornRadio.vue\";\r\n",
|
|
10
|
+
"target": "web"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "reborn-radio.config.ts",
|
|
14
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as radioSizes, colors as radioColors };\r\n\r\nexport default {\r\n slots: {\r\n root: \"inline-flex cursor-pointer select-none\",\r\n wrapper: \"inline-flex items-center gap-2\",\r\n activeIcon:\r\n \"inline-flex items-center justify-center rounded-full border-2 border-transparent text-white transition-all duration-200\",\r\n inactiveIcon:\r\n \"inline-flex items-center justify-center rounded-full border-2 border-gray-4 dark:border-gray-6 transition-all duration-200\",\r\n label: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n activeIcon: \"size-4 text-[10px]\",\r\n inactiveIcon: \"size-4\",\r\n label: \"text-xs\",\r\n },\r\n md: {\r\n activeIcon: \"size-5 text-xs\",\r\n inactiveIcon: \"size-5\",\r\n label: \"text-sm\",\r\n },\r\n lg: {\r\n activeIcon: \"size-6 text-sm\",\r\n inactiveIcon: \"size-6\",\r\n label: \"text-base\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n activeIcon: \"bg-primary border-primary\",\r\n },\r\n secondary: {\r\n activeIcon: \"bg-secondary border-secondary\",\r\n },\r\n success: {\r\n activeIcon: \"bg-success border-success\",\r\n },\r\n info: {\r\n activeIcon: \"bg-info border-info\",\r\n },\r\n warning: {\r\n activeIcon: \"bg-warning border-warning\",\r\n },\r\n error: {\r\n activeIcon: \"bg-error border-error\",\r\n },\r\n neutral: {\r\n activeIcon: \"bg-neutral border-neutral\",\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: \"opacity-50 pointer-events-none\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
15
|
+
"target": "web"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "RebornRadio.vue",
|
|
19
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, useAttrs, useSlots } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { radioColors, radioSizes } from \"./reborn-radio.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface RadioProps {\r\n modelValue?: any;\r\n value?: any;\r\n label?: string;\r\n disabled?: boolean;\r\n size?: (typeof radioSizes)[number];\r\n color?: (typeof radioColors)[number];\r\n activeIcon?: string;\r\n inactiveIcon?: string;\r\n showIcon?: boolean;\r\n class?: any;\r\n ui?: Partial<{\r\n root: ClassValue;\r\n wrapper: ClassValue;\r\n activeIcon: ClassValue;\r\n inactiveIcon: ClassValue;\r\n label: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RadioProps>(), {\r\n disabled: false,\r\n size: \"md\",\r\n color: \"primary\",\r\n activeIcon: \"i-lucide-check\",\r\n inactiveIcon: \"\",\r\n showIcon: true,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: any): void;\r\n (e: \"change\", value: any): void;\r\n}>();\r\n\r\nconst slots = useSlots();\r\nconst attrs = useAttrs();\r\n\r\nconst isChecked = computed(() => props.modelValue === props.value);\r\nconst showLabel = computed(() => !!props.label || !!slots.default);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, uiOverrides.value.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, uiOverrides.value.inactiveIcon) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n };\r\n});\r\n\r\nfunction onTap() {\r\n if (!props.disabled && !isChecked.value) {\r\n emit(\"update:modelValue\", props.value);\r\n emit(\"change\", props.value);\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: props.class })\" :data-disabled=\"disabled\" :data-checked=\"isChecked\" @click=\"onTap\">\r\n <div :class=\"ui.wrapper()\">\r\n <template v-if=\"showIcon\">\r\n <slot v-if=\"isChecked\" name=\"active-icon\">\r\n <div :class=\"ui.activeIcon()\">\r\n <Icon v-if=\"activeIcon\" :name=\"activeIcon\" class=\"size-full\" />\r\n </div>\r\n </slot>\r\n <slot v-else name=\"inactive-icon\">\r\n <div :class=\"ui.inactiveIcon()\">\r\n <Icon v-if=\"inactiveIcon\" :name=\"inactiveIcon\" class=\"size-full\" />\r\n </div>\r\n </slot>\r\n </template>\r\n\r\n <div v-if=\"showLabel\" :class=\"ui.label()\" :data-checked=\"isChecked\">\r\n <slot>{{ label }}</slot>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n",
|
|
20
|
+
"target": "web"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "index.ts",
|
|
24
|
+
"content": "export { default as RebornRadio } from './RebornRadio.vue'\r\nexport { default as RebornRadioGroup } from './RebornRadioGroup.vue'\r\n",
|
|
25
|
+
"target": "uniapp"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "reborn-radio.config.ts",
|
|
29
|
+
"content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-row items-center',\r\n wrapper: 'inline-flex items-center gap-2', // wrapper to align icon and label\r\n activeIcon: 'flex justify-center items-center border-2 border-gray-4 dark:border-gray-600 transition-all duration-200',\r\n inactiveIcon: 'border-2 border-gray-4 dark:border-gray-1 transition-all duration-200',\r\n label: 'text-sm text-gray-7 dark:text-gray-2',\r\n control: 'flex items-center justify-center rounded-full border border-gray-300 dark:border-gray-600 transition-colors',\r\n },\r\n variants: {\r\n color: {\r\n primary: {\r\n activeIcon: 'bg-gradient-to-br from-primary/80 to-primary border-transparent',\r\n label: 'data-[checked=true]:text-primary',\r\n },\r\n secondary: {\r\n activeIcon: 'bg-gradient-to-br from-secondary/80 to-secondary border-transparent',\r\n label: 'data-[checked=true]:text-secondary',\r\n },\r\n success: {\r\n activeIcon: 'bg-gradient-to-br from-success/80 to-success border-transparent',\r\n label: 'data-[checked=true]:text-success',\r\n },\r\n info: {\r\n activeIcon: 'bg-gradient-to-br from-info/80 to-info border-transparent',\r\n label: 'data-[checked=true]:text-info',\r\n },\r\n warning: {\r\n activeIcon: 'bg-gradient-to-br from-warning/80 to-warning border-transparent',\r\n label: 'data-[checked=true]:text-warning',\r\n },\r\n error: {\r\n activeIcon: 'bg-gradient-to-br from-error/80 to-error border-transparent',\r\n label: 'data-[checked=true]:text-error',\r\n },\r\n neutral: {\r\n activeIcon: 'bg-gradient-to-br from-neutral/80 to-neutral border-transparent',\r\n label: 'data-[checked=true]:text-neutral',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n activeIcon: 'w-[32rpx] h-[32rpx]',\r\n inactiveIcon: 'w-[32rpx] h-[32rpx]',\r\n label: 'text-24',\r\n },\r\n md: {\r\n activeIcon: 'w-[36rpx] h-[36rpx]',\r\n inactiveIcon: 'w-[36rpx] h-[36rpx]',\r\n label: 'text-26',\r\n },\r\n lg: {\r\n activeIcon: 'w-[40rpx] h-[40rpx]',\r\n inactiveIcon: 'w-[40rpx] h-[40rpx]',\r\n label: 'text-28',\r\n },\r\n },\r\n isRound: {\r\n true: {\r\n activeIcon: 'rounded-full text-white',\r\n inactiveIcon: 'rounded-full text-white',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none cursor-not-allowed',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'primary',\r\n isRound: true,\r\n },\r\n} as const\r\n\r\nexport { colors as radioColors, sizes as radioSizes }\r\n",
|
|
30
|
+
"target": "uniapp"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "RebornRadio.vue",
|
|
34
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { radioColors, radioSizes } from './reborn-radio.config'\r\nimport { computed, useSlots, inject } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-radio.config'\r\n\r\ndefineOptions({\r\n name: 'RebornRadio',\r\n})\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n disabled: false,\r\n size: 'md',\r\n color: 'primary',\r\n activeIcon: 'i-lucide-check',\r\n inactiveIcon: '',\r\n showIcon: true,\r\n isRound: true,\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\ninterface Props {\r\n modelValue?: any\r\n value?: any\r\n label?: string\r\n disabled?: boolean\r\n size?: typeof radioSizes[number]\r\n color?: typeof radioColors[number]\r\n activeIcon?: string\r\n inactiveIcon?: string\r\n showIcon?: boolean\r\n isRound?: boolean\r\n ui?: {\r\n root?: string\r\n wrapper?: string\r\n activeIcon?: string\r\n inactiveIcon?: string\r\n label?: string\r\n }\r\n customClass?: any\r\n}\r\n\r\nconst slots = useSlots()\r\n\r\nconst { disabled: formDisabled, size: formSize } = useFormInject(props)\r\nconst radioGroup = inject('RebornRadioGroup', null) as any\r\n\r\nconst isGroup = computed(() => !!radioGroup)\r\n\r\nconst modelValue = computed({\r\n get() {\r\n return isGroup.value ? radioGroup.modelValue.value : props.modelValue\r\n },\r\n set(val) {\r\n if (isGroup.value) {\r\n radioGroup.updateValue(val)\r\n } else {\r\n emit('update:modelValue', val)\r\n }\r\n }\r\n})\r\n\r\nconst isDisabled = computed(() => {\r\n return isGroup.value ? (radioGroup.disabled.value || props.disabled) : (props.disabled || formDisabled.value)\r\n})\r\n\r\nconst resolvedSize = computed(() => {\r\n return isGroup.value ? (radioGroup.size.value || props.size) : (props.size || formSize.value)\r\n})\r\n\r\nconst resolvedColor = computed(() => {\r\n return isGroup.value ? (radioGroup.color.value || props.color) : props.color\r\n})\r\n\r\nconst isChecked = computed(() => modelValue.value === props.value)\r\nconst showLabel = computed(() => !!props.label || !!slots.default)\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: resolvedSize.value,\r\n color: resolvedColor.value,\r\n disabled: isDisabled.value,\r\n isRound: props.isRound,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, props.ui?.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, props.ui?.wrapper) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, props.ui?.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, props.ui?.inactiveIcon) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, props.ui?.label) }),\r\n }\r\n})\r\n\r\nfunction onTap() {\r\n if (!isDisabled.value && !isChecked.value) {\r\n if (isGroup.value) {\r\n radioGroup.updateValue(props.value)\r\n } else {\r\n emit('update:modelValue', props.value)\r\n emit('change', props.value)\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" :data-disabled=\"isDisabled\" :data-checked=\"isChecked\"\r\n @tap=\"onTap\">\r\n <view :class=\"ui.wrapper()\">\r\n <template v-if=\"showIcon\">\r\n <slot v-if=\"isChecked\" name=\"active-icon\">\r\n <view :class=\"ui.activeIcon()\">\r\n <view :class=\"activeIcon\" />\r\n </view>\r\n </slot>\r\n <slot v-else name=\"inactive-icon\">\r\n <view :class=\"ui.inactiveIcon()\">\r\n <view :class=\"inactiveIcon\" />\r\n </view>\r\n </slot>\r\n </template>\r\n\r\n <view v-if=\"showLabel\" :class=\"ui.label()\" :data-checked=\"isChecked\">\r\n <slot :isChecked=\"isChecked\">{{ label }}</slot>\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
35
|
+
"target": "uniapp"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"path": "RebornRadioGroup.vue",
|
|
39
|
+
"content": "<script setup lang=\"ts\">\r\nimport { provide, toRef } from 'vue'\r\nimport type { radioColors, radioSizes } from './reborn-radio.config'\r\n\r\ninterface Props {\r\n modelValue?: any\r\n disabled?: boolean\r\n size?: typeof radioSizes[number]\r\n color?: typeof radioColors[number]\r\n fill?: string // Support fill as an alias for color or custom color if needed, but for now map to color logic if it matches\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n disabled: false,\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\nconst updateValue = (value: any) => {\r\n emit('update:modelValue', value)\r\n emit('change', value)\r\n}\r\n\r\nprovide('RebornRadioGroup', {\r\n modelValue: toRef(props, 'modelValue'),\r\n disabled: toRef(props, 'disabled'),\r\n size: toRef(props, 'size'),\r\n color: toRef(props, 'color'),\r\n updateValue\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-radio-group flex flex-wrap gap-4\">\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
40
|
+
"target": "uniapp"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"fileCount": 7,
|
|
44
|
+
"contentHash": "c94923cc33efd3de1820e1dc91a3eca84510d2b0"
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-rate",
|
|
3
|
+
"dependencies": [
|
|
4
|
+
"clsx"
|
|
5
|
+
],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "index.ts",
|
|
9
|
+
"content": "export { default as RebornRate } from \"./RebornRate.vue\";\r\n",
|
|
10
|
+
"target": "web"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "reborn-rate.config.ts",
|
|
14
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { color as rateColors, size as rateSizes };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex flex-row items-center gap-1\",\r\n star: \"relative cursor-pointer transition-all duration-200 ease-out\",\r\n icon: \"transition-all duration-200 ease-out dark:text-gray-2\",\r\n iconActive: \"transition-all duration-200 ease-out\",\r\n value: \"ml-1 font-medium tabular-nums dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n icon: \"size-4\",\r\n iconActive: \"size-4\",\r\n value: \"text-[length:var(--text-size-24)]\",\r\n },\r\n md: {\r\n icon: \"size-5\",\r\n iconActive: \"size-5\",\r\n value: \"text-[length:var(--text-size-28)]\",\r\n },\r\n lg: {\r\n icon: \"size-7\",\r\n iconActive: \"size-7\",\r\n value: \"text-[length:var(--text-size-32)]\",\r\n },\r\n },\r\n color: {\r\n primary: { iconActive: \"text-primary\" },\r\n secondary: { iconActive: \"text-secondary\" },\r\n success: { iconActive: \"text-success\" },\r\n info: { iconActive: \"text-info\" },\r\n warning: { iconActive: \"text-warning\" },\r\n error: { iconActive: \"text-error\" },\r\n neutral: { iconActive: \"text-neutral\" },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: \"opacity-50 pointer-events-none\",\r\n },\r\n },\r\n readonly: {\r\n true: {\r\n star: \"cursor-default active:scale-100\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"warning\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
15
|
+
"target": "web"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"path": "RebornRate.vue",
|
|
19
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from \"clsx\";\r\nimport type { rateColors, rateSizes } from \"./reborn-rate.config\";\r\nimport { computed, ref, watch } from \"vue\";\r\n\r\nimport { tv } from \"~/lib/tv\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme from \"./reborn-rate.config\";\r\n\r\ndefineOptions({\r\n name: \"RebornRate\",\r\n});\r\n\r\nconst props = withDefaults(defineProps<RateProps>(), {\r\n modelValue: 0,\r\n count: 5,\r\n allowHalf: false,\r\n showValue: false,\r\n disabled: false,\r\n readonly: false,\r\n icon: \"lucide:star\",\r\n activeIcon: \"lucide:star\",\r\n size: \"md\",\r\n color: \"warning\",\r\n ui: () => ({}),\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: number): void;\r\n (e: \"change\", value: number): void;\r\n}>();\r\n\r\nexport interface RateProps {\r\n /** 当前评分 */\r\n modelValue?: number;\r\n /** 星星总数 */\r\n count?: number;\r\n /** 允许半星 */\r\n allowHalf?: boolean;\r\n /** 显示分数 */\r\n showValue?: boolean;\r\n /** 是否禁用 */\r\n disabled?: boolean;\r\n /** 是否只读 */\r\n readonly?: boolean;\r\n /** 未选中图标 (Nuxt Icon name) */\r\n icon?: string;\r\n /** 选中图标 (Nuxt Icon name) */\r\n activeIcon?: string;\r\n /** 半星选中图标 (Nuxt Icon name) */\r\n halfIcon?: string;\r\n /** 尺寸 */\r\n size?: (typeof rateSizes)[number];\r\n /** 颜色 */\r\n color?: (typeof rateColors)[number];\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n star: ClassValue;\r\n icon: ClassValue;\r\n iconActive: ClassValue;\r\n value: ClassValue;\r\n }>;\r\n /** 自定义 class */\r\n class?: any;\r\n}\r\n\r\nconst isInteractive = computed(() => !props.disabled && !props.readonly);\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst b = tv(theme);\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size as any,\r\n color: props.color,\r\n disabled: props.disabled,\r\n readonly: props.readonly,\r\n });\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n star: (opts?: { class?: any }) =>\r\n styles.star({ class: cn(opts?.class, uiOverrides.value.star) }),\r\n icon: (opts?: { class?: any }) =>\r\n styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n iconActive: (opts?: { class?: any }) =>\r\n styles.iconActive({ class: cn(opts?.class, uiOverrides.value.iconActive) }),\r\n value: (opts?: { class?: any }) =>\r\n styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n };\r\n});\r\n\r\nfunction getActiveIcon(index: number) {\r\n if (isHalf(index)) {\r\n return props.halfIcon ?? props.activeIcon;\r\n }\r\n return props.activeIcon;\r\n}\r\n\r\n// 当前评分\r\nconst currentValue = ref<number>(props.modelValue);\r\n\r\n// 判断当前星星是否为半星状态\r\nfunction isHalf(index: number): boolean {\r\n return props.allowHalf && currentValue.value >= index - 0.5 && currentValue.value < index;\r\n}\r\n\r\n// 点击事件(Web 版:用鼠标位置判断半星)\r\nfunction onClick(e: MouseEvent, index: number) {\r\n if (!isInteractive.value) {\r\n return;\r\n }\r\n\r\n if (props.allowHalf) {\r\n const target = e.currentTarget as HTMLElement;\r\n const rect = target.getBoundingClientRect();\r\n const midX = rect.left + rect.width / 2;\r\n const newValue = e.clientX < midX ? index - 0.5 : index;\r\n\r\n if (currentValue.value === newValue) {\r\n updateValue(0);\r\n } else {\r\n updateValue(newValue);\r\n }\r\n } else {\r\n // 点击同一个星星则清零\r\n if (currentValue.value === index) {\r\n updateValue(0);\r\n } else {\r\n updateValue(index);\r\n }\r\n }\r\n}\r\n\r\n// Web 增强:鼠标移动 hover 预览\r\nconst hoverValue = ref(-1);\r\nlet moveRaf = 0;\r\n\r\nconst displayValue = computed(() =>\r\n hoverValue.value >= 0 ? hoverValue.value : currentValue.value,\r\n);\r\n\r\nfunction isHalfDisplay(index: number): boolean {\r\n return props.allowHalf && displayValue.value >= index - 0.5 && displayValue.value < index;\r\n}\r\n\r\nfunction isActiveDisplay(index: number): boolean {\r\n return (\r\n displayValue.value >= index - (props.allowHalf ? 0.5 : 0) && displayValue.value >= index - 0.5\r\n );\r\n}\r\n\r\nfunction onMouseMove(e: MouseEvent) {\r\n if (!isInteractive.value) {\r\n return;\r\n }\r\n if (moveRaf) cancelAnimationFrame(moveRaf);\r\n\r\n const x = e.clientX;\r\n const wrapper = e.currentTarget as HTMLElement;\r\n\r\n moveRaf = requestAnimationFrame(() => {\r\n const stars = wrapper.querySelectorAll(\".reborn-rate__star\");\r\n for (let i = stars.length - 1; i >= 0; i--) {\r\n const rect = (stars[i] as HTMLElement).getBoundingClientRect();\r\n if (x >= rect.left) {\r\n if (props.allowHalf) {\r\n const midX = rect.left + rect.width / 2;\r\n hoverValue.value = x < midX ? i + 0.5 : i + 1;\r\n } else {\r\n hoverValue.value = i + 1;\r\n }\r\n return;\r\n }\r\n }\r\n hoverValue.value = 0;\r\n });\r\n}\r\n\r\nfunction onMouseLeave() {\r\n if (moveRaf) cancelAnimationFrame(moveRaf);\r\n hoverValue.value = -1;\r\n}\r\n\r\n// 更新值\r\nfunction updateValue(newValue: number) {\r\n if (currentValue.value !== newValue) {\r\n currentValue.value = newValue;\r\n emit(\"update:modelValue\", newValue);\r\n emit(\"change\", newValue);\r\n }\r\n}\r\n\r\n// 同步外部值\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== currentValue.value) {\r\n currentValue.value = Math.max(0, Math.min(props.count, val));\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.wrapper({ class: props.class })\" @mousemove=\"onMouseMove\" @mouseleave=\"onMouseLeave\">\r\n <div v-for=\"index in count\" :key=\"index\" class=\"reborn-rate__star\" :class=\"ui.star()\"\r\n @click=\"onClick($event, index)\">\r\n <!-- 未激活图标 -->\r\n <div :class=\"ui.icon()\" class=\"opacity-30\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"false\">\r\n <Icon :name=\"props.icon\" class=\"size-full\" />\r\n </slot>\r\n </div>\r\n\r\n <!-- 激活图标(整星 / 半星) -->\r\n <div v-if=\"isActiveDisplay(index)\" class=\"absolute inset-0\" :class=\"ui.iconActive()\"\r\n :style=\"isHalfDisplay(index) ? { clipPath: 'polygon(0 0, 50% 0, 50% 100%, 0 100%)' } : {}\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"true\">\r\n <Icon :name=\"getActiveIcon(index)\" class=\"size-full\" />\r\n </slot>\r\n </div>\r\n </div>\r\n\r\n <!-- 分数显示 -->\r\n <slot name=\"value\" :value=\"currentValue\">\r\n <span v-if=\"showValue\" :class=\"ui.value()\">{{ currentValue }}</span>\r\n </slot>\r\n </div>\r\n</template>\r\n",
|
|
20
|
+
"target": "web"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "index.ts",
|
|
24
|
+
"content": "export { default as RebornRate } from './RebornRate.vue'\r\n",
|
|
25
|
+
"target": "uniapp"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"path": "reborn-rate.config.ts",
|
|
29
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as rateColors, size as rateSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper: 'inline-flex flex-row items-center gap-1',\r\n star: 'relative cursor-pointer transition-transform active:scale-90',\r\n icon: 'transition-colors duration-150 dark:text-gray-2',\r\n iconActive: 'transition-colors duration-150',\r\n value: 'ml-1 font-medium tabular-nums dark:text-gray-1',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n icon: 'size-4',\r\n iconActive: 'size-4',\r\n value: 'text-24',\r\n },\r\n md: {\r\n icon: 'size-5',\r\n iconActive: 'size-5',\r\n value: 'text-28',\r\n },\r\n lg: {\r\n icon: 'size-7',\r\n iconActive: 'size-7',\r\n value: 'text-32',\r\n },\r\n },\r\n color: {\r\n primary: { iconActive: 'text-primary' },\r\n secondary: { iconActive: 'text-secondary' },\r\n success: { iconActive: 'text-success' },\r\n info: { iconActive: 'text-info' },\r\n warning: { iconActive: 'text-warning' },\r\n error: { iconActive: 'text-error' },\r\n neutral: { iconActive: 'text-neutral' },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: 'opacity-50 pointer-events-none',\r\n },\r\n },\r\n readonly: {\r\n true: {\r\n star: 'cursor-default active:scale-100',\r\n },\r\n },\r\n allowHalf: {\r\n true: {},\r\n false: {},\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n size: 'sm',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[30rpx]',\r\n },\r\n },\r\n {\r\n size: 'md',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[40rpx]',\r\n },\r\n },\r\n {\r\n size: 'lg',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[40rpx]',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as (typeof size)[number],\r\n color: 'warning' as (typeof color)[number],\r\n allowHalf: false,\r\n },\r\n}\r\n",
|
|
30
|
+
"target": "uniapp"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"path": "RebornRate.vue",
|
|
34
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { rateColors, rateSizes } from './reborn-rate.config'\r\nimport { computed, getCurrentInstance, ref, watch } from 'vue'\r\n\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-rate.config'\r\n\r\ndefineOptions({\r\n name: 'RebornRate',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RateProps>(), {\r\n modelValue: 0,\r\n count: 5,\r\n allowHalf: false,\r\n showValue: false,\r\n disabled: false,\r\n readonly: false,\r\n icon: 'i-lucide-star',\r\n activeIcon: 'i-lucide-star',\r\n size: 'md',\r\n color: 'warning',\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: number): void\r\n (e: 'change', value: number): void\r\n}>()\r\n\r\nexport interface RateProps {\r\n /** 当前评分 */\r\n modelValue?: number\r\n /** 星星总数 */\r\n count?: number\r\n /** 允许半星 */\r\n allowHalf?: boolean\r\n /** 显示分数 */\r\n showValue?: boolean\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 是否只读 */\r\n readonly?: boolean\r\n /** 未选中图标 class */\r\n icon?: string\r\n /** 选中图标 class */\r\n activeIcon?: string\r\n /** 半星选中图标 class */\r\n halfIcon?: string\r\n /** 尺寸 */\r\n size?: (typeof rateSizes)[number]\r\n /** 颜色 */\r\n color?: (typeof rateColors)[number]\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n star: ClassValue\r\n icon: ClassValue\r\n iconActive: ClassValue\r\n value: ClassValue\r\n }>\r\n /** 自定义 class */\r\n customClass?: any\r\n}\r\n\r\nconst { proxy } = getCurrentInstance()!\r\n\r\n// reborn-form 上下文\r\nconst { disabled, size } = useFormInject(props)\r\n\r\nconst isInteractive = computed(\r\n () => !disabled.value && !props.readonly,\r\n)\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (size.value || props.size) as any,\r\n color: props.color,\r\n disabled: disabled.value,\r\n readonly: props.readonly,\r\n allowHalf: props.allowHalf,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n star: (opts?: { class?: any }) =>\r\n styles.star({ class: cn(opts?.class, uiOverrides.value.star) }),\r\n icon: (opts?: { class?: any }) =>\r\n styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n iconActive: (opts?: { class?: any }) =>\r\n styles.iconActive({ class: cn(opts?.class, uiOverrides.value.iconActive) }),\r\n value: (opts?: { class?: any }) =>\r\n styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n }\r\n})\r\n\r\nfunction activeIcon(index: number) {\r\n if (isHalf(index)) {\r\n return props.halfIcon ?? props.activeIcon\r\n }\r\n return props.activeIcon\r\n}\r\n\r\n// 当前评分\r\nconst currentValue = ref<number>(props.modelValue)\r\n\r\n// 判断当前星星是否为半星状态\r\nfunction isHalf(index: number): boolean {\r\n return props.allowHalf && currentValue.value >= index - 0.5 && currentValue.value < index\r\n}\r\n\r\n// 防止 touchstart 和 tap 重复触发\r\nlet touchHandled = false\r\n\r\n// 点击事件\r\nfunction onTap(index: number) {\r\n if (!isInteractive.value) { return }\r\n\r\n // 如果 touchStart 已处理(半星模式),跳过 tap\r\n if (touchHandled) {\r\n touchHandled = false\r\n return\r\n }\r\n\r\n let newValue = index\r\n\r\n if (props.allowHalf) {\r\n if (currentValue.value === index) {\r\n newValue = index - 0.5\r\n }\r\n else if (currentValue.value === index - 0.5) {\r\n newValue = 0\r\n }\r\n }\r\n else {\r\n // 点击同一个星星则清零\r\n if (currentValue.value === index) {\r\n newValue = 0\r\n }\r\n }\r\n\r\n updateValue(newValue)\r\n}\r\n\r\n// 触摸开始 — 用于半星判断\r\nfunction onTouchStart(e: TouchEvent, index: number) {\r\n if (!isInteractive.value || !props.allowHalf) { return }\r\n\r\n touchHandled = true\r\n\r\n const touch = e.touches[0]\r\n if (!touch) { return }\r\n\r\n // 获取当前星星的位置进行左/右判断\r\n getStarRect(index - 1).then((rect) => {\r\n if (!rect) { return }\r\n const midX = rect.left + rect.width / 2\r\n const newValue = touch.clientX < midX ? index - 0.5 : index\r\n updateValue(newValue)\r\n })\r\n}\r\n\r\n// 触摸移动 — 拖动评分\r\nfunction onTouchMove(e: TouchEvent, index: number) {\r\n if (!isInteractive.value) { return }\r\n\r\n const touch = e.touches[0]\r\n if (!touch) { return }\r\n\r\n // 通过触摸位置计算哪个星星\r\n getAllStarRects().then((rects) => {\r\n if (!rects || rects.length === 0) { return }\r\n\r\n for (let i = rects.length - 1; i >= 0; i--) {\r\n const rect = rects[i]\r\n if (touch.clientX >= rect.left) {\r\n if (props.allowHalf) {\r\n const midX = rect.left + rect.width / 2\r\n const newValue = touch.clientX < midX ? i + 0.5 : i + 1\r\n updateValue(newValue)\r\n }\r\n else {\r\n updateValue(i + 1)\r\n }\r\n return\r\n }\r\n }\r\n\r\n // 在所有星星左边\r\n updateValue(0)\r\n })\r\n}\r\n\r\n// 更新值\r\nfunction updateValue(newValue: number) {\r\n if (currentValue.value !== newValue) {\r\n currentValue.value = newValue\r\n emit('update:modelValue', newValue)\r\n emit('change', newValue)\r\n }\r\n}\r\n\r\n// 获取单个星星的矩形信息\r\nfunction getStarRect(index: number): Promise<any> {\r\n return new Promise((resolve) => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .selectAll('.reborn-rate__star')\r\n .boundingClientRect((nodes: any) => {\r\n resolve(nodes?.[index] ?? null)\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\n// 获取所有星星的矩形信息\r\nfunction getAllStarRects(): Promise<any[]> {\r\n return new Promise((resolve) => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .selectAll('.reborn-rate__star')\r\n .boundingClientRect((nodes: any) => {\r\n resolve(nodes ?? [])\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\n// 同步外部值\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== currentValue.value) {\r\n currentValue.value = Math.max(0, Math.min(props.count, val))\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\">\r\n <view v-for=\"index in count\" :key=\"index\" class=\"reborn-rate__star\" :class=\"ui.star()\" @tap=\"onTap(index)\"\r\n @touchstart=\"onTouchStart($event, index)\" @touchmove=\"onTouchMove($event, index)\">\r\n <!-- 未激活图标 -->\r\n <view :class=\"ui.icon()\" class=\"opacity-30\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"false\">\r\n <view :class=\"props.icon\" class=\"size-full\" />\r\n </slot>\r\n </view>\r\n\r\n <!-- 激活图标(整星 / 半星) -->\r\n <view v-if=\"currentValue >= index - (allowHalf ? 0.5 : 0) && currentValue >= index - 0.5\" class=\"absolute inset-0\"\r\n :class=\"[ui.iconActive(), isHalf(index) && `\r\n w-1/2 overflow-hidden\r\n `]\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"true\" :style=\"isHalf(index) ? { width: '200%' } : {}\">\r\n <view :class=\"activeIcon(index)\" class=\"size-full\" :style=\"isHalf(index) ? { width: '200%' } : {}\" />\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <!-- 分数显示 -->\r\n <slot name=\"value\" :value=\"currentValue\">\r\n <view v-if=\"showValue\" :class=\"ui.value()\">{{ currentValue }}</view>\r\n </slot>\r\n </view>\r\n</template>\r\n",
|
|
35
|
+
"target": "uniapp"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"fileCount": 6,
|
|
39
|
+
"contentHash": "00f107634fea6233c26c9c0331d9d5f425ed3051"
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reborn-root-portal",
|
|
3
|
+
"dependencies": [],
|
|
4
|
+
"files": [
|
|
5
|
+
{
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"content": "import RebornRootPortal from './RebornRootPortal.vue'\r\nimport config from './reborn-root-portal.config'\r\n\r\nexport { RebornRootPortal, config }\r\nexport default RebornRootPortal\r\n"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"path": "reborn-root-portal.config.ts",
|
|
11
|
+
"content": "const config = {\r\n slots: {\r\n base: 'fixed inset-0 pointer-events-none z-[999]',\r\n content: 'pointer-events-auto'\r\n }\r\n} as const\r\n\r\nexport default config\r\n"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"path": "RebornRootPortal.vue",
|
|
15
|
+
"content": "<template>\r\n <teleport to=\"body\">\r\n <slot />\r\n </teleport>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n// RebornRootPortal Web Implementation\r\n</script>\r\n",
|
|
16
|
+
"target": "web"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "RebornRootPortal.vue",
|
|
20
|
+
"content": "<script lang=\"ts\">\nexport default {\n name: 'reborn-root-portal',\n options: {\n virtualHost: true,\n addGlobalClass: true,\n styleIsolation: 'shared'\n }\n}\n</script>\n\n<script lang=\"ts\" setup>\n</script>\n\n<template>\n <!-- #ifdef H5 -->\n <!-- H5端使用 teleport -->\n <teleport to=\"body\">\n <!-- #endif -->\n <!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n <!-- #ifndef MP-DINGTALK -->\n <!-- 小程序使用 root-portal -->\n <root-portal>\n <!-- #endif -->\n <!-- #endif -->\n <view>\n <slot />\n </view>\n <!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n <!-- #ifndef MP-DINGTALK -->\n </root-portal>\n <!-- #endif -->\n <!-- #endif -->\n <!-- #ifdef H5 -->\n </teleport>\n <!-- #endif -->\n</template>\n\n<!-- #ifdef APP-PLUS -->\n<script module=\"render\" lang=\"renderjs\">\nexport default {\n mounted() {\n if (this.$ownerInstance.$el) {\n (document.querySelector('uni-app') || document.body).appendChild(this.$ownerInstance.$el)\n }\n },\n beforeDestroy() {\n if (this.$ownerInstance.$el) {\n (document.querySelector('uni-app') || document.body).removeChild(this.$ownerInstance.$el)\n }\n }\n}\n</script>\n<!-- #endif -->\n",
|
|
21
|
+
"target": "uniapp"
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"fileCount": 4,
|
|
25
|
+
"contentHash": "9b676448b7d1b2c63e55a2ec8466e5274de8374d"
|
|
26
|
+
}
|