@funnycaptcha/click-order 0.2.0 → 0.6.0

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 CHANGED
@@ -29,51 +29,100 @@ function proofInput(c) {
29
29
 
30
30
  // src/render.ts
31
31
  var STR = {
32
- zh: { title: "\u6309 1 \u2192 2 \u2192 3 \u2192 4 \u987A\u5E8F\u70B9\u51FB", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5" },
33
- en: { title: "Click in order 1 \u2192 2 \u2192 3 \u2192 4", success: "Verified", fail: "Wrong order, try again" }
32
+ zh: { title: "\u6309 1 -> 2 -> 3 -> 4 \u987A\u5E8F\u70B9\u51FB\uFF08\u4F4D\u7F6E\u4F1A\u53D8\uFF09", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u987A\u5E8F\u9519\u8BEF\uFF0C\u8BF7\u91CD\u8BD5", refresh: "\u5237\u65B0" },
33
+ en: { title: "Click in order 1 -> 2 -> 3 -> 4 (positions shift)", success: "Verified", fail: "Wrong order, try again", refresh: "Refresh" }
34
34
  };
35
- var SLOTS = [
36
- { x: 20, y: 20 },
37
- { x: 160, y: 20 },
38
- { x: 20, y: 100 },
39
- { x: 160, y: 100 }
40
- ];
35
+ var AREA_W = 280;
36
+ var AREA_H = 180;
37
+ var BOX_W = 100;
38
+ var BOX_H = 60;
39
+ var MIN_DIST = 110;
40
+ function generateSlots(count) {
41
+ const maxX = AREA_W - BOX_W;
42
+ const maxY = AREA_H - BOX_H;
43
+ const slots = [];
44
+ let attempts = 0;
45
+ while (slots.length < count && attempts < 1e3) {
46
+ attempts++;
47
+ const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };
48
+ let ok = true;
49
+ for (const s of slots) {
50
+ const dx = candidate.x - s.x;
51
+ const dy = candidate.y - s.y;
52
+ if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST) {
53
+ ok = false;
54
+ break;
55
+ }
56
+ }
57
+ if (ok) slots.push(candidate);
58
+ }
59
+ while (slots.length < count) {
60
+ slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });
61
+ }
62
+ return slots;
63
+ }
41
64
  function createClickOrderInstance(container, config) {
42
65
  const t = STR[config.locale];
66
+ const theme = config.theme ?? "light";
43
67
  let current;
44
68
  let listeners = [];
45
69
  let startTime = Date.now();
46
70
  let clicked = [];
71
+ let doneSet = /* @__PURE__ */ new Set();
47
72
  let finished = false;
48
73
  function render() {
49
74
  current = generateChallenge();
50
75
  clicked = [];
76
+ doneSet = /* @__PURE__ */ new Set();
51
77
  finished = false;
52
78
  startTime = Date.now();
79
+ const slots = generateSlots(current.targets.length);
53
80
  const boxes = current.targets.map((num, i) => {
54
- const slot = SLOTS[i];
55
- return `<div class="fc-click-box" data-num="${num}" style="left:${slot.x}px;top:${slot.y}px;">${num}</div>`;
81
+ const slot = slots[i];
82
+ return `<div class="fc-click-box" data-idx="${i}" data-num="${num}" style="left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;">${num}</div>`;
56
83
  }).join("");
57
84
  container.innerHTML = `
58
85
  <style>
59
- .fc-click{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
60
- .fc-click-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}
61
- .fc-click-area{position:relative;width:280px;height:180px;background:#f5f5f5;border-radius:8px;border:1px solid #e0e0e0;overflow:hidden}
62
- .fc-click-box{position:absolute;width:100px;height:60px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:#4a90d9;background:#fff;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s}
86
+ .fc-click{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
87
+ .fc-click[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
88
+ .fc-click[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
89
+ .fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
90
+ .fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
91
+ .fc-click-area{position:relative;width:${AREA_W}px;height:${AREA_H}px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}
92
+ .fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}
63
93
  .fc-click-box:hover{transform:scale(1.05)}
64
- .fc-click-box-active{background:#4a90d9;color:#fff}
65
- .fc-click-box-done{background:#2e7d32;border-color:#2e7d32;color:#fff;cursor:default}
94
+ .fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}
95
+ .fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}
66
96
  .fc-click-box-done:hover{transform:none}
67
- .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}
97
+ .fc-click-row{display:flex;justify-content:center;margin-top:10px}
98
+ .fc-click-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
99
+ .fc-click-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
100
+ .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
68
101
  </style>
69
- <div class="fc-click">
102
+ <div class="fc-click" data-theme="${theme}">
70
103
  <div class="fc-click-title">${t.title}</div>
71
104
  <div class="fc-click-area">${boxes}</div>
72
105
  <div class="fc-click-msg"></div>
106
+ <div class="fc-click-row">
107
+ <button class="fc-click-refresh">${t.refresh}</button>
108
+ </div>
73
109
  </div>
74
110
  `;
75
111
  const area = container.querySelector(".fc-click-area");
76
112
  const msg = container.querySelector(".fc-click-msg");
113
+ const refreshBtn = container.querySelector(".fc-click-refresh");
114
+ refreshBtn.addEventListener("click", render);
115
+ function relocateBoxes(excludeIndex) {
116
+ const remaining = current.targets.map((_, i) => i).filter((i) => i !== excludeIndex && !doneSet.has(i));
117
+ if (remaining.length === 0) return;
118
+ const newSlots = generateSlots(remaining.length);
119
+ remaining.forEach((idx, j) => {
120
+ const box = area.querySelector(`[data-idx="${idx}"]`);
121
+ const slot = newSlots[j];
122
+ box.style.left = `${Math.round(slot.x)}px`;
123
+ box.style.top = `${Math.round(slot.y)}px`;
124
+ });
125
+ }
77
126
  area.querySelectorAll(".fc-click-box").forEach((el) => {
78
127
  el.addEventListener("click", async () => {
79
128
  if (finished) return;
@@ -82,7 +131,9 @@ function createClickOrderInstance(container, config) {
82
131
  const num = Number(box.dataset.num);
83
132
  const expected = current.order[clicked.length];
84
133
  if (num === expected) {
134
+ const idx = Number(box.dataset.idx);
85
135
  clicked.push(num);
136
+ doneSet.add(idx);
86
137
  box.classList.add("fc-click-box-done");
87
138
  if (clicked.length === current.order.length) {
88
139
  finished = true;
@@ -94,6 +145,8 @@ function createClickOrderInstance(container, config) {
94
145
  msg.textContent = t.success;
95
146
  config.onVerify?.(result);
96
147
  listeners.forEach((cb) => cb(result));
148
+ } else {
149
+ relocateBoxes(idx);
97
150
  }
98
151
  } else {
99
152
  finished = true;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createClickOrderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const clickOrderPlugin: CaptchaPlugin = {\n id: 'click-order',\n category: 'interactive',\n create: createClickOrderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '点选顺序' : 'Click Order',\n description: locale === 'zh'\n ? '按数字顺序依次点击目标,考验眼手协调'\n : 'Click targets in numeric order. Tests coordination.',\n tags: ['click', 'order', 'interactive'],\n }),\n};\n\ndefineCaptcha(clickOrderPlugin);\n\nexport function mountClickOrder(container: HTMLElement, config: CaptchaConfig) {\n return createClickOrderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyOrder, type ClickOrderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '按 1 → 2 → 3 → 4 顺序点击', success: '验证成功', fail: '顺序错误,请重试' },\n en: { title: 'Click in order 1 → 2 → 3 → 4', success: 'Verified', fail: 'Wrong order, try again' },\n};\n\n// 4 个不重叠的预设槽位(2x2 网格),280x180 区域内\nconst SLOTS = [\n { x: 20, y: 20 },\n { x: 160, y: 20 },\n { x: 20, y: 100 },\n { x: 160, y: 100 },\n];\n\nexport function createClickOrderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: ClickOrderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let clicked: number[] = [];\n let finished = false;\n\n function render() {\n current = generateChallenge();\n clicked = [];\n finished = false;\n startTime = Date.now();\n const boxes = current.targets.map((num, i) => {\n const slot = SLOTS[i]!;\n return `<div class=\"fc-click-box\" data-num=\"${num}\" style=\"left:${slot.x}px;top:${slot.y}px;\">${num}</div>`;\n }).join('');\n container.innerHTML = `\n <style>\n .fc-click{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}\n .fc-click-title{font-size:14px;color:#333;margin-bottom:12px;text-align:center}\n .fc-click-area{position:relative;width:280px;height:180px;background:#f5f5f5;border-radius:8px;border:1px solid #e0e0e0;overflow:hidden}\n .fc-click-box{position:absolute;width:100px;height:60px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:#4a90d9;background:#fff;border:2px solid #4a90d9;border-radius:8px;cursor:pointer;user-select:none;transition:transform .15s,background .15s,color .15s}\n .fc-click-box:hover{transform:scale(1.05)}\n .fc-click-box-active{background:#4a90d9;color:#fff}\n .fc-click-box-done{background:#2e7d32;border-color:#2e7d32;color:#fff;cursor:default}\n .fc-click-box-done:hover{transform:none}\n .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:#2e7d32}\n </style>\n <div class=\"fc-click\">\n <div class=\"fc-click-title\">${t.title}</div>\n <div class=\"fc-click-area\">${boxes}</div>\n <div class=\"fc-click-msg\"></div>\n </div>\n `;\n const area = container.querySelector('.fc-click-area') as HTMLDivElement;\n const msg = container.querySelector('.fc-click-msg') as HTMLDivElement;\n\n area.querySelectorAll('.fc-click-box').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const box = el as HTMLElement;\n if (box.classList.contains('fc-click-box-done')) return; // 已点过\n const num = Number(box.dataset.num);\n const expected = current.order[clicked.length]!;\n if (num === expected) {\n // 点击正确:高亮并锁定\n clicked.push(num);\n box.classList.add('fc-click-box-done');\n if (clicked.length === current.order.length) {\n // 全部正确\n finished = true;\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n }\n } else {\n // 顺序错误:重置\n finished = true;\n box.classList.add('fc-click-box-active');\n msg.textContent = t.fail;\n setTimeout(render, 700);\n }\n });\n });\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 点选顺序题目:4 个带数字的目标打乱位置,用户按 1→2→3→4 顺序点击\nexport interface ClickOrderChallenge {\n // 显示在 4 个槽位上的数字([1,2,3,4] 的随机排列)\n targets: number[];\n // 正确的点击顺序\n order: number[];\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:order 固定为 [1,2,3,4],targets 为打乱后的展示顺序\nexport function generateChallenge(): ClickOrderChallenge {\n const targets = shuffle([1, 2, 3, 4]);\n return { targets, order: [1, 2, 3, 4] };\n}\n\n// 校验用户点击顺序是否正确\nexport function verifyOrder(c: ClickOrderChallenge, userOrder: number[]): boolean {\n if (userOrder.length !== c.order.length) return false;\n return userOrder.every((v, i) => v === c.order[i]);\n}\n\n// 构造 proof 输入串:order 以逗号连接\nexport function proofInput(c: ClickOrderChallenge): string {\n return c.order.join(',');\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type ClickOrderChallenge } from './challenge.js';\n\nexport function issueClickOrderChallenge(): { challenge: ClickOrderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { order: challenge.order } };\n}\n\nexport async function verifyClickOrderProof(\n challenge: ClickOrderChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(proofInput(challenge));\n return expected === proof;\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createClickOrderInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const clickOrderPlugin: CaptchaPlugin = {\n id: 'click-order',\n category: 'interactive',\n create: createClickOrderInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '点选顺序' : 'Click Order',\n description: locale === 'zh'\n ? '按数字顺序依次点击目标,考验眼手协调'\n : 'Click targets in numeric order. Tests coordination.',\n tags: ['click', 'order', 'interactive'],\n }),\n};\n\ndefineCaptcha(clickOrderPlugin);\n\nexport function mountClickOrder(container: HTMLElement, config: CaptchaConfig) {\n return createClickOrderInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyOrder, type ClickOrderChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '按 1 -> 2 -> 3 -> 4 顺序点击(位置会变)', success: '验证成功', fail: '顺序错误,请重试', refresh: '刷新' },\n en: { title: 'Click in order 1 -> 2 -> 3 -> 4 (positions shift)', success: 'Verified', fail: 'Wrong order, try again', refresh: 'Refresh' },\n};\n\n// 区域尺寸\nconst AREA_W = 280;\nconst AREA_H = 180;\nconst BOX_W = 100;\nconst BOX_H = 60;\n// 最小间距(任意两个槽位中心/左上角之间的欧氏距离),防重叠\nconst MIN_DIST = 110;\n\n// 在 AREA_W×AREA_H 区域内随机生成 count 个不重叠槽位\nfunction generateSlots(count: number): { x: number; y: number }[] {\n const maxX = AREA_W - BOX_W;\n const maxY = AREA_H - BOX_H;\n const slots: { x: number; y: number }[] = [];\n let attempts = 0;\n while (slots.length < count && attempts < 1000) {\n attempts++;\n const candidate = { x: Math.random() * maxX, y: Math.random() * maxY };\n let ok = true;\n for (const s of slots) {\n const dx = candidate.x - s.x;\n const dy = candidate.y - s.y;\n if (Math.sqrt(dx * dx + dy * dy) < MIN_DIST) { ok = false; break; }\n }\n if (ok) slots.push(candidate);\n }\n // 兜底:若拒绝采样未填满,剩余用伪随机补齐(极少触发)\n while (slots.length < count) {\n slots.push({ x: Math.random() * maxX, y: Math.random() * maxY });\n }\n return slots;\n}\n\nexport function createClickOrderInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: ClickOrderChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let clicked: number[] = [];\n let doneSet = new Set<number>();\n let finished = false;\n\n function render() {\n current = generateChallenge();\n clicked = [];\n doneSet = new Set<number>();\n finished = false;\n startTime = Date.now();\n const slots = generateSlots(current.targets.length);\n const boxes = current.targets.map((num, i) => {\n const slot = slots[i]!;\n return `<div class=\"fc-click-box\" data-idx=\"${i}\" data-num=\"${num}\" style=\"left:${Math.round(slot.x)}px;top:${Math.round(slot.y)}px;\">${num}</div>`;\n }).join('');\n container.innerHTML = `\n <style>\n .fc-click{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .fc-click[data-theme=\"light\"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}\n .fc-click[data-theme=\"dark\"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}\n .fc-click{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-click-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .fc-click-area{position:relative;width:${AREA_W}px;height:${AREA_H}px;background:var(--fc-surface);border-radius:8px;border:1px solid var(--fc-border);overflow:hidden}\n .fc-click-box{position:absolute;width:${BOX_W}px;height:${BOX_H}px;display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:bold;color:var(--fc-accent);background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:8px;cursor:pointer;user-select:none;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),transform .15s,background .15s,color .15s,border-color .15s}\n .fc-click-box:hover{transform:scale(1.05)}\n .fc-click-box-active{background:var(--fc-accent);color:var(--fc-bg)}\n .fc-click-box-done{background:var(--fc-success);border-color:var(--fc-success);color:#fff;cursor:default}\n .fc-click-box-done:hover{transform:none}\n .fc-click-row{display:flex;justify-content:center;margin-top:10px}\n .fc-click-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}\n .fc-click-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n .fc-click-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n </style>\n <div class=\"fc-click\" data-theme=\"${theme}\">\n <div class=\"fc-click-title\">${t.title}</div>\n <div class=\"fc-click-area\">${boxes}</div>\n <div class=\"fc-click-msg\"></div>\n <div class=\"fc-click-row\">\n <button class=\"fc-click-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const area = container.querySelector('.fc-click-area') as HTMLDivElement;\n const msg = container.querySelector('.fc-click-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-click-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n function relocateBoxes(excludeIndex: number) {\n const remaining = current.targets.map((_, i) => i).filter(i => i !== excludeIndex && !doneSet.has(i));\n if (remaining.length === 0) return;\n const newSlots = generateSlots(remaining.length);\n remaining.forEach((idx, j) => {\n const box = area.querySelector(`[data-idx=\"${idx}\"]`) as HTMLElement;\n const slot = newSlots[j]!;\n box.style.left = `${Math.round(slot.x)}px`;\n box.style.top = `${Math.round(slot.y)}px`;\n });\n }\n\n area.querySelectorAll('.fc-click-box').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const box = el as HTMLElement;\n if (box.classList.contains('fc-click-box-done')) return; // 已点过\n const num = Number(box.dataset.num);\n const expected = current.order[clicked.length]!;\n if (num === expected) {\n // 点击正确:高亮并锁定\n const idx = Number(box.dataset.idx);\n clicked.push(num);\n doneSet.add(idx);\n box.classList.add('fc-click-box-done');\n if (clicked.length === current.order.length) {\n // 全部正确\n finished = true;\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n relocateBoxes(idx);\n }\n } else {\n // 顺序错误:重置\n finished = true;\n box.classList.add('fc-click-box-active');\n msg.textContent = t.fail;\n setTimeout(render, 700);\n }\n });\n });\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 点选顺序题目:4 个带数字的目标打乱位置,用户按 1→2→3→4 顺序点击\nexport interface ClickOrderChallenge {\n // 显示在 4 个槽位上的数字([1,2,3,4] 的随机排列)\n targets: number[];\n // 正确的点击顺序\n order: number[];\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:order 固定为 [1,2,3,4],targets 为打乱后的展示顺序\nexport function generateChallenge(): ClickOrderChallenge {\n const targets = shuffle([1, 2, 3, 4]);\n return { targets, order: [1, 2, 3, 4] };\n}\n\n// 校验用户点击顺序是否正确\nexport function verifyOrder(c: ClickOrderChallenge, userOrder: number[]): boolean {\n if (userOrder.length !== c.order.length) return false;\n return userOrder.every((v, i) => v === c.order[i]);\n}\n\n// 构造 proof 输入串:order 以逗号连接\nexport function proofInput(c: ClickOrderChallenge): string {\n return c.order.join(',');\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type ClickOrderChallenge } from './challenge.js';\n\nexport function issueClickOrderChallenge(): { challenge: ClickOrderChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { order: challenge.order } };\n}\n\nexport async function verifyClickOrderProof(\n challenge: ClickOrderChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(proofInput(challenge));\n return expected === proof;\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnycaptcha/click-order",
3
- "version": "0.2.0",
3
+ "version": "0.6.0",
4
4
  "description": "Click-order captcha — FunnyChapter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@funnycaptcha/core": "0.2.0"
18
+ "@funnycaptcha/core": "0.6.0"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public",