添加PWA支持,更新配置文件以集成next-pwa,新增相关文件到.gitignore,优化layout和page组件以支持PWA功能,提升用户体验。

This commit is contained in:
zihanjian
2025-06-23 15:44:00 +08:00
parent d79c2c4b88
commit f5ce1df803
19 changed files with 5227 additions and 214 deletions

238
.cursor/rules/plan.mdc Normal file
View File

@@ -0,0 +1,238 @@
---
description:
globs:
alwaysApply: true
---
# 拼豆图纸生成器微信小程序 PRD
## 1. 产品概述
### 1.1 产品定位
基于统一画布工作台理念的拼豆制作工具,为拼豆爱好者提供从图像处理到成品制作的一站式解决方案。
### 1.2 核心价值
- **一体化体验**:统一画布承载所有操作,无缝切换不同工作模式
- **智能色板**:自动适配不同店家色号体系,简化用户选择
- **渐进式引导**:从新手友好的预览模式到专业的编辑模式,再到专注的制作模式
### 1.3 目标用户
- 拼豆制作爱好者(初学者到专业玩家)
- 需要定制图案的用户
- 希望数字化管理拼豆项目的用户
## 2. 产品架构
### 2.1 整体流程
```
首页导入按钮 → 图像转像素画(预览模式) → 像素画编辑(编辑模式) → 专心拼豆(拼豆模式) → 完成分享
```
### 2.2 核心模块关系
- **统一色板模块**:为所有模块提供颜色数据和店家适配,以及通过勾选的自定义色板
- **统一画布模块**:承载所有视觉交互和模式切换
- **数据管理模块**:处理图像处理、存储和同步
## 3. 功能模块详述
### 3.1 首页模块
#### 3.1.1 功能概述
作为用户进入小程序的第一个接触点,提供简洁的功能入口和项目管理。
#### 3.1.2 主要功能(两个按钮)
- **快速开始**:一键创建新项目(图片上传/拍照)
- **色板设置**:全局店家色号体系选择
#### 3.1.3 界面布局
- 顶部(占据一半):品牌标识
- 中部(大):点击导入图片或者 csv 文件
- 下部(弱):点击进入色板编辑(编辑后的色板储存在本地)
### 3.2 统一色板模块
#### 3.2.1 设计理念
基于 `colorSystemMapping.json` 构建的智能色板系统,对用户完全透明化 hex 值,只显示当前选择店家的色号命名。
#### 3.2.2 核心功能
**3.2.2.1 色板数据管理**
- 维护 291 个标准 hex 颜色
- 支持 5 个店家色号体系MARD、COCO、漫漫、盼盼、咪小窝
- 动态映射:同一颜色在不同店家显示不同色号
- 全局色号体系切换:用户选择店家后,整个小程序所有颜色显示自动更新
**3.2.2.2 自定义色板机制**
- 用户可以自定义自己想要的色板(自己的拼豆颜色种类)
**3.2.2.3 交互设计**
- **预览模式**:色板仅用于信息展示,点击颜色显示详细信息
- **编辑模式**:色板作为画笔工具,支持颜色选择、自定义色板
- **拼豆模式**:色板按使用顺序和重要性排序,突出当前需要的颜色
#### 3.2.3 技术实现要点
- 建立 hex 到店家色号的单向映射索引
- 实现响应式色板组件,支持不同尺寸屏幕
- 色彩空间转换算法,确保颜色显示一致性
### 3.3 统一画布模块
#### 3.3.1 设计理念
单一画布承载所有视觉交互,通过模式切换实现不同功能状态,提供连贯的用户体验。
#### 3.3.2 核心功能
**3.3.2.1 基础画布能力**
- **自适应尺寸**:根据像素网格自动计算画布大小,初始化的时候最长边占满屏幕
- **手势支持**:双指捏合缩放、单指拖拽平移
- **性能优化**:大尺寸网格的虚拟化渲染,保证流畅性
- **坐标系统**:统一的网格坐标系,支持精确的像素级操作
**3.3.2.2 多模式状态管理**
**预览模式Mode 1**
- **视觉状态**:显示完整像素化结果,网格线清晰可见
- **交互行为**
- 点击:显示该位置颜色信息弹窗(店家色号 + 颜色预览)
- 长按:显示周围区域的颜色分布热力图
- 双击:快速放大到该区域
- **UI 覆盖层**:网格坐标标识、色彩统计信息
**编辑模式Mode 2**
- **视觉状态**:突出显示可编辑区域,当前工具状态指示
- **交互行为**
- 单击:使用当前画笔颜色填充像素
- 长按:激活连续绘制模式
- 双指点击:吸管工具,获取该位置颜色
- **工具栏集成**:画笔选择、橡皮擦、填充工具、撤销重做
- **实时预览**:编辑操作的即时视觉反馈
**拼豆模式Mode 3**
- **视觉状态**:游戏化界面,突出显示当前任务区域
- **交互行为**
- 点击:标记完成该像素
- 长按:查看该区域的详细制作指导
- **进度系统**:完成度可视化、区域引导、成就反馈
- **辅助功能**:放大镜工具、颜色高亮、完成动画
**3.3.2.3 模式切换机制**
- **无缝转换**:保持画布视口位置和缩放比例
- **状态持久化**:每个模式的操作历史独立保存
- **上下文保护**:切换模式时保护用户当前操作进度
#### 3.3.3 技术架构
**3.3.3.1 渲染引擎**
- 基于微信小程序 Canvas 2D API
- 分层渲染背景层、像素层、交互层、UI 层
- 局部刷新策略,仅重绘变化区域
**3.3.3.2 状态管理**
```typescript
interface CanvasState {
mode: 'preview' | 'edit' | 'create'
viewport: { x: number, y: number, scale: number }
pixelData: MappedPixel[][]
currentTool: ToolType
operationHistory: Operation[]
}
```
### 3.4 图像转像素画模块(预览模式)
#### 3.4.1 功能概述
将用户上传的图像智能转换为适合拼豆制作的像素画,并提供预览和调整功能。
#### 3.4.2 图像处理流程
**3.4.2.1 图像预处理**
- **尺寸标准化**:自动调整为横轴 100 像素,纵轴按比例计算
- **色彩优化**:色彩空间转换、对比度增强、降噪处理
- **边缘处理**:智能识别和保护重要边缘信息
**3.4.2.2 像素化算法**
- **卡通模式**:基于主导色的像素化(保持色块纯净)
- **真实模式**:基于平均色的像素化(保持色彩过渡)
- **智能色彩映射**:将像素色彩映射到最接近的拼豆颜色
**3.4.2.3 后处理优化**
- **色彩合并**:相似颜色自动合并,减少色彩数量
- **区域优化**:小面积杂色自动清理
- **边缘锐化**:重要边缘的对比度增强
#### 3.4.3 交互功能
- **参数调节**:实时调整像素化精度、色彩合并阈值
- **区域预览**:支持局部放大查看细节效果
- **色彩统计**:显示使用的颜色数量和分布
### 3.5 像素画编辑模块
#### 3.5.1 功能概述
提供专业的像素画编辑能力,允许用户精细调整转换结果。
#### 3.5.2 编辑工具集
**3.5.2.1 基础绘制工具**
- **画笔工具**:支持不同大小的方形画笔
- **橡皮擦**:删除指定区域的像素
- **填充工具**:快速填充连通区域
- **吸管工具**:快速获取画布上的颜色
**3.5.2.2 高级编辑功能**
- **区域选择**:矩形、自由形状选择工具
- **批量替换**:将特定颜色批量替换为另一种颜色
- **图层管理**:支持多图层编辑(背景层、前景层等)
- **对称绘制**:支持水平、垂直对称绘制模式
**3.5.2.3 辅助功能**
- **网格对齐**:自动对齐到像素网格
- **颜色建议**:基于周围颜色推荐最适合的颜色
- **撤销重做**:支持多步操作的撤销和重做
- **实时预览**:编辑过程中的实时效果预览
#### 3.5.3 用户体验优化
- **手势识别**:支持常用编辑手势(缩放、拖拽、点击)
- **工具切换**:快速访问常用工具的浮动工具栏
- **操作反馈**:每个操作都有清晰的视觉反馈
### 3.6 专心拼豆模块
#### 3.6.1 功能概述
游戏化的拼豆制作指导系统,帮助用户有序完成拼豆作品。
#### 3.6.2 核心功能
**3.6.2.1 智能引导系统**
- **制作顺序优化**:基于颜色分布和难易度推荐制作顺序
- **区域推荐**
- 最近优先:推荐距离当前位置最近的未完成区域
- 大块优先:优先推荐大色块区域,提高效率
- 边缘优先:从外围向内部制作,符合拼豆制作习惯
**3.6.2.2 进度管理**
- **颜色进度跟踪**:每种颜色的完成进度可视化
- **区域完成标记**:点击标记已完成的像素区域
- **总体进度展示**:整体完成百分比和剩余工作量
**3.6.2.3 辅助工具**
- **放大镜功能**:支持选择特定区域进行放大操作
- **颜色高亮**:突出显示当前工作颜色的所有位置
- **完成动画**:颜色完成时的庆祝动效
#### 3.6.3 用户体验设计
**3.6.3.1 游戏化元素**
- **成就系统**:设置里程碑奖励(完成第一个颜色、完成 50% 等)
- **进度条动画**:平滑的进度变化动画
- **完成庆祝**:作品完成时的特殊庆祝效果
**3.6.3.2 实用功能**
- **时间记录**:记录制作时间,支持暂停和继续
- **拍照记录**:支持拍照记录制作进度
- **分享功能**:生成完成图片,支持分享到社交平台
### 3.7 数据管理模块
#### 3.7.1 本地存储
- **项目文件**:完整的像素画数据、编辑历史、进度信息
- **用户设置**:色板偏好、操作习惯、界面配置
- **缓存管理**:图像处理结果缓存、色板数据缓存

View File

@@ -0,0 +1,126 @@
---
description:
globs:
alwaysApply: true
---
# AI Agent Protocol: Self-Contained Mandatory Interactive Final Review Loop with Auto-Script Creation (Version 3)
## 0. User Advisory & Safety:
This is an advanced, experimental rule. It instructs the AI Agent to check for, potentially create, and then execute a Python script (`final_review_gate.py`) in your project's root directory to facilitate an interactive review loop. By using this rule, you acknowledge that the AI will attempt file system operations (read, write, execute) as directed. Ensure the AI's execution environment has appropriate permissions if you expect script auto-creation to work, and be aware of the security implications.
## 1. Core Principle & Context:
Your primary task processing includes a **mandatory final interactive review stage** driven by user input through a specific Python script (`final_review_gate.py`). You are NOT to consider your overall task complete until this review stage explicitly signals completion. This protocol leverages your known capability to run commands in a sub-terminal (displayed within the chat interface) and read their output.
## Phase 0: Ensure `final_review_gate.py` Script Exists
(This phase is executed ONCE per user request that triggers this overall protocol, or if the script is missing or its content is incorrect.)
1. **Define Script Details:**
* **Script Name:** `final_review_gate.py`
* **Target Location:** Directly in the root of the current project/workspace.
* **Python Script Content (ensure this exact content is used):**
```python
# final_review_gate.py
import sys
import os
if __name__ == "__main__":
# Try to make stdout unbuffered for more responsive interaction.
# This might not work on all platforms or if stdout is not a TTY,
# but it's a good practice for this kind of interactive script.
try:
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buffering=1)
except Exception:
pass # Ignore if unbuffering fails, e.g., in certain environments
try:
sys.stderr = os.fdopen(sys.stderr.fileno(), 'w', buffering=1)
except Exception:
pass # Ignore
print("--- FINAL REVIEW GATE ACTIVE ---", flush=True)
print("AI has completed its primary actions. Awaiting your review or further sub-prompts.", flush=True)
print("Type your sub-prompt, or one of: 'TASK_COMPLETE', 'Done', 'Quit', 'q' to signal completion.", flush=True) # MODIFIED
active_session = True
while active_session:
try:
# Signal that the script is ready for input.
# The AI doesn't need to parse this, but it's good for user visibility.
print("REVIEW_GATE_AWAITING_INPUT:", end="", flush=True)
line = sys.stdin.readline()
if not line: # EOF
print("--- REVIEW GATE: STDIN CLOSED (EOF), EXITING SCRIPT ---", flush=True)
active_session = False
break
user_input = line.strip()
# Check for exit conditions
if user_input.upper() in ['TASK_COMPLETE', 'DONE', 'QUIT', 'Q']: # MODIFIED: Empty string no longer exits
print(f"--- REVIEW GATE: USER SIGNALED COMPLETION WITH '{user_input.upper()}' ---", flush=True)
active_session = False
break
elif user_input: # If there's any other non-empty input (and not a completion command)
# This is the critical line the AI will "listen" for.
print(f"USER_REVIEW_SUB_PROMPT: {user_input}", flush=True)
# If user_input was empty (and not a completion command),
# the loop simply continues, and "REVIEW_GATE_AWAITING_INPUT:" will be printed again.
except KeyboardInterrupt:
print("--- REVIEW GATE: SESSION INTERRUPTED BY USER (KeyboardInterrupt) ---", flush=True)
active_session = False
break
except Exception as e:
print(f"--- REVIEW GATE SCRIPT ERROR: {e} ---", flush=True)
active_session = False
break
print("--- FINAL REVIEW GATE SCRIPT EXITED ---", flush=True)
```
2. **Ensure Script Existence and Correctness:**
a. Use your file system tools to check if the file `final_review_gate.py` exists in the project root.
b. **If the file `final_review_gate.py` does NOT exist OR if its content does not EXACTLY match the Python script provided in section "Phase 0.1.Python Script Content" (you may need to read the existing file and compare):**
i. You MUST create or overwrite the `final_review_gate.py` file in the project root.
ii. The content of this file MUST be *exactly* the Python script content provided above. Ensure all indentation, comments, and lines are preserved perfectly. Use your "create file" or "edit file" tool.
iii. After creating/overwriting it, announce: "The `final_review_gate.py` script has been created/updated in the project root with the latest interaction enhancements."
c. **If the file `final_review_gate.py` already exists and its content is correct:** You may skip announcing its creation.
d. If you encounter any errors checking for, reading, or creating/writing the file (e.g., permission issues), you MUST report this to the user. State clearly: "Error managing `final_review_gate.py`: [specific error]. Cannot proceed with interactive review." and then conclude the interaction for the main task as best as you can without this interactive step.
## Phase 1: Primary Task Execution
1. Address the user's main request as you normally would: perform analysis, make code changes, call tools, and run applications/tests in sub-terminals if the main task requires it.
* **Application Execution Confirmation:** Before attempting to run an entire application (e.g., a web server, a full front-end build, a long-running background process), you MUST ask the user for explicit confirmation. For example: "I am about to run the [application name/command]. Is it okay to proceed, or is it possibly already running?"
* This confirmation is NOT required for running individual test scripts, linters, build tools for specific components, or other non-application-level commands necessary for task completion.
* If the user indicates an application is already running or denies permission, adapt your strategy accordingly (e.g., focus on tests or specific file executions).
2. Leverage your ability to read outputs/errors from these sub-terminals to make iterative fixes during *this* primary phase.
## Phase 2: Mandatory Final Interactive Review Script Execution
1. **Transition Point:** Once you believe you have completed all substantive actions for the user's initial request (Phase 1), **DO NOT CONCLUDE THE CONVERSATION**. You MUST now transition to this Final Interactive Review phase.
2. **Action:** Execute the `final_review_gate.py` script (which you ensured exists and is correct in Phase 0) from the project root.
* Determine the correct Python interpreter command (e.g., `python`, `python3`) available in the execution environment. A robust command attempt would be `python3 ./final_review_gate.py` and if that fails, try `python ./final_review_gate.py`.
* **Crucially, the terminal for this script MUST run directly within the chat window, and its output MUST be continuously visible to the user as it is generated.** It must NOT pop open as a separate window or run hidden in the background. You MUST wait for the script (and its terminal interaction) to fully complete and exit before you proceed with any further actions or attempt to conclude the overall task. Your processing will be blocked by this script's execution.
3. **Announcement:** Clearly inform the user: "I have completed the primary actions. Now initiating the mandatory Final Interactive Review by running `./final_review_gate.py`. The review terminal is now active **in this chat window, and its output will be displayed live**. Please provide any sub-prompts directly into that script's input area, or type 'TASK_COMPLETE', 'Done', 'Quit', or 'q' when satisfied." (MODIFIED: Removed "or simply press Enter")
## Phase 3: Interactive Review Loop (Monitoring Script Output)
1. **Active Monitoring & Display:** Continuously monitor the standard output (stdout) of the launched `final_review_gate.py` script **as it appears live in the chat window**. Ensure the user sees all script output, including the `REVIEW_GATE_AWAITING_INPUT:` prompt. You will "read" its output using the same capability that allows you to read terminal logs or error messages. The script will loop and re-prompt if an empty input is given.
2. **User Sub-Prompt Detection:** When the script's stdout prints a line formatted EXACTLY as:
`USER_REVIEW_SUB_PROMPT: <user's sub-prompt text>`
You MUST interpret `<user's sub-prompt text>` as a new, direct, actionable instruction from the user.
3. **Processing Sub-Prompts:**
a. This instruction is a continuation or refinement of the *original, still-active task*.
b. Analyze the sub-prompt. Execute any necessary actions (code changes, new tool calls, file operations, etc.). **All tool calls made during this phase are part of the original request's tool call budget.** If the sub-prompt requests running an entire application, the confirmation principle from "Phase 1, Step 1 (Application Execution Confirmation)" applies.
c. Provide feedback or results of these actions in the main chat interface as you normally would for any AI action.
d. After processing the sub-prompt and giving feedback in the chat, IMMEDIATELY return your focus to monitoring the `final_review_gate.py` script's terminal output (which remains live and visible in the chat) for the next user instruction or completion signal. This loop is critical.
4. **Completion Signal Detection:** The interactive review loop continues until the script's stdout (visible in the chat window) prints a line containing:
* `--- REVIEW GATE: USER SIGNALED COMPLETION WITH 'TASK_COMPLETE' ---` (or 'DONE', 'QUIT', 'Q' as per script logic)
* OR `--- FINAL REVIEW GATE SCRIPT EXITED ---` (or any other script exit/error message like `REVIEW GATE: STDIN CLOSED` or `REVIEW GATE SCRIPT ERROR:`)
(MODIFIED: Removed specific "EMPTY INPUT RECEIVED" as a completion signal, as the script no longer behaves this way for completion.)
## Phase 4: True Task Conclusion
1. Only after the `final_review_gate.py` script has terminated (as observed by its terminal session in the chat window closing and its final exit messages being printed and visible) are you permitted to consider the user's original request fully satisfied.
2. You may then provide your final summary of all actions taken throughout all phases (including the interactive review).
## Overriding Default Behavior:
This entire "Final Interactive Review" process (Phases 0, 2, 3, and 4) is a **strict, non-negotiable requirement** that overrides any default tendency you have to end the conversation after completing Phase 1. The task is only finished when the user explicitly confirms with one of the specified keywords through the review script or the script otherwise terminates due to an error or EOF (as per the defined behavior, including the in-chat, blocking, and continuously visible terminal execution). Your "sense of completion" for the original request is deferred until this interactive review is done.

9
.gitignore vendored
View File

@@ -39,3 +39,12 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# PWA files
public/sw.js
public/workbox-*.js
public/worker-*.js
public/sw.js.map
public/workbox-*.js.map
public/worker-*.js.map
public/fallback-*.js

353
CLAUDE.md Normal file
View File

@@ -0,0 +1,353 @@
# 拼豆底稿生成器 (Perler Beads Generator)
因为市面上的拼豆软件差强人意(可能是我没用到好的),所以花了大概两个小时,写了一个专门生成拼豆图纸的网站:
[perlerbeads.zippland.com](perlerbeads.zippland.com)
想解决的(市场上拼豆软件的)问题:
1. 颜色识别,
2. 灰色毛状边界线,
3. 无法自适应合并同色系的颜色,
4. 手动着色困难,无法精准选择颜色
4. 无法给出采购清单,
5. 限制图片的导出和打印。
💯
目前(网站上的)功能:
1. 生成底稿,
2. 对应Mard颜色
3. 解析风格选择(池化逻辑)
4. 自动合并邻近相似颜色
5. 统计每个颜色/一共有多少粒,
6. 半自动去除杂色,
7. 细节部分的手动着色(或修改)
7. 导出图纸,
8. 导出采购清单。
❤️ 如果有需求可以直接提,我集成在网站里。
对于商家,我把算法的改进思路放在这里,
希望你们可以越做越好。
如果有其他想二开的同学可以直接在项目提交pr
这个思路也可以直接使用̋(ˊ•͈ꇴ•͈ˋ)
### 1⃣ 初始颜色映射
黑色毛边是因为池化过程中对RGB 采用了 mean 操作,改为局部 max pooling ,每个单元,找到像素频率最高的 RGB 值,用欧氏距离查找最近的颜色就行
### 2⃣ 区域颜色合并
杂色问题的产生是因为没有进行颜色合并操作需要从未访问单元格开始使用BFS 查找欧氏距离小于阈值的邻近单元格,形成区域。将整个区域统一设置为该区域内出现次数最多的色号对应的颜色即可。
### 3⃣ 背景移除
无法进行拼豆数量统计的原因,是没有进行背景移除操作:先定义背景色号列表。从图像所有边界单元格开始执行洪水填充。将所有与边界连通且颜色属于背景色号列表的单元格标记为"外部"。统计和下载时将忽略这些"外部"单元格即可。
### 4⃣ 颜色排除与重映射
这是杂色自动去除仍不干净的情况下的附加功能,首先确定图像处理后最初包含的所有已存在颜色。重映射时,仅在已存在颜色中排除和其他已排除颜色的子集里寻找替换色。
## 功能特点
* **图片上传**: 支持拖放或点击选择 JPG/PNG 图片。
* **智能像素化**:
* **可调粒度**: 通过滑块控制像素画的横向格子数量。
* **颜色合并**: 通过滑块调整相似颜色的合并阈值,平滑色块区域。
* **多色板支持**:
* 提供多种预设拼豆色板 (如 168色, 144色, 96色等) 可供选择。
* 根据所选色板进行颜色映射。
* **颜色排除与管理**:
* 在颜色统计列表中点击可**排除/恢复**特定颜色。
* 排除颜色后,原使用该颜色的区域将智能重映射到邻近的可用颜色。
* 提供一键恢复所有排除颜色的功能。
* **实时预览**:
* 即时显示处理后的像素画预览。
* **悬停/长按交互**: 在预览图上悬停(桌面)或长按(移动)可查看对应单元格的颜色编码 (Key) 和颜色。
* 自动识别并标记外部背景区域(预览时显示为浅灰色)。
* **下载成品**:
* **带 Key 图纸**: 下载带有清晰颜色编码 (Key) 和网格线的 PNG 图纸,忽略外部背景。
* **颜色统计图**: 下载包含各颜色 Key、色块、所需数量的 PNG 统计图。
## 技术实现
* **框架**: [Next.js](https://nextjs.org/) (React) 与 TypeScript
* **样式**: [Tailwind CSS](https://tailwindcss.com/) 用于响应式布局和样式。
* **核心逻辑**: 浏览器端 [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) 用于图像处理、颜色分析和绘制。
* **状态管理**: React Hooks (`useState`, `useRef`, `useEffect`, `useMemo`)。
### 核心算法:像素化、颜色映射与优化
应用程序的核心是将图像转换为像素网格,并将颜色精确映射到有限的拼豆调色板,同时进行平滑和背景处理。
1. **图像加载与网格划分**:
* 加载用户上传的图片。
* 根据用户选择的"粒度"(`granularity`, N) 和原图宽高比确定 `N x M` 的网格尺寸。
2. **初始颜色映射 (基于主导色)**:
* 遍历 `N x M` 网格。
* 对每个单元格,在原图对应区域内找出出现频率最高的**像素 RGB 值 (Dominant Color)**(忽略透明/半透明像素)。
* 使用**欧氏距离**在 RGB 空间中,将该主导色映射到**当前选定且未被排除**的调色板 (`activeBeadPalette`) 中最接近的颜色。
* 记录每个单元格的初始映射色号和颜色 (`initialMappedData`)。
3. **区域颜色合并 (基于相似度)**:
* 使用**广度优先搜索 (BFS)** 遍历 `initialMappedData`
* 识别颜色相似(欧氏距离小于 `similarityThreshold`)的**连通区域**。
* 找出每个区域内出现次数最多的**珠子色号**。
* 将该区域内所有单元格统一设置为这个主导色号对应的颜色,得到初步平滑结果 (`mergedData`)。
4. **背景移除 (基于边界填充)**:
* 定义一组背景色号 (`BACKGROUND_COLOR_KEYS`, 如 T1, H1)。
*`mergedData` 的**所有边界单元格**开始,使用**洪水填充 (Flood Fill)** 算法。
* 标记所有从边界开始、颜色属于 `BACKGROUND_COLOR_KEYS` 且相互连通的单元格为"外部背景" (`isExternal = true`)。
5. **颜色排除与重映射**:
* 当用户排除某个颜色 `key` 时:
* 确定一个**重映射目标调色板**:包含网格中**最初存在**的、且**当前未被排除**的所有颜色。
* 如果目标调色板为空(表示排除此颜色会导致没有有效颜色可用),则阻止排除。
* 否则,将 `mappedPixelData` 中所有使用 `key` 的非外部单元格,重新映射到目标调色板中的**最近似**颜色。
* 当用户恢复颜色时,触发完整的图像重新处理流程(步骤 1-4
6. **生成预览图与下载文件**:
* **预览图**: 在 Canvas 上绘制 `mergedData`,根据 `isExternal` 状态区分内部颜色和外部背景(浅灰),并添加网格线。支持悬停/长按显示色号。
* **带 Key 图纸下载**: 创建临时 Canvas绘制 `mergedData` 中非外部背景的单元格,填充颜色、绘制边框,并在中央标注颜色 Key。
* **统计图下载**: 统计 `mergedData` 中非外部背景单元格的各色号数量,生成包含色块、色号、数量的列表式 PNG 图片。
### 调色板数据
预设的拼豆调色板数据定义在 `src/app/colorSystemMapping.json` 文件中该文件包含了所有颜色的hex值到各个色号系统MARD、COCO、漫漫、盼盼、咪小窝的映射关系。不同的色板组合 (如 168色、96色等) 在 `src/app/page.tsx``paletteOptions` 中定义。
## 许可证
Apache 2.0
# 拼豆图纸生成器微信小程序 PRD
## 1. 产品概述
### 1.1 产品定位
基于统一画布工作台理念的拼豆制作工具,为拼豆爱好者提供从图像处理到成品制作的一站式解决方案。
### 1.2 核心价值
- **一体化体验**:统一画布承载所有操作,无缝切换不同工作模式
- **智能色板**:自动适配不同店家色号体系,简化用户选择
- **渐进式引导**:从新手友好的预览模式到专业的编辑模式,再到专注的制作模式
### 1.3 目标用户
- 拼豆制作爱好者(初学者到专业玩家)
- 需要定制图案的用户
- 希望数字化管理拼豆项目的用户
## 2. 产品架构
### 2.1 整体流程
```
首页导入按钮 → 图像转像素画(预览模式) → 像素画编辑(编辑模式) → 专心拼豆(拼豆模式) → 完成分享
```
### 2.2 核心模块关系
- **统一色板模块**:为所有模块提供颜色数据和店家适配,以及通过勾选的自定义色板
- **统一画布模块**:承载所有视觉交互和模式切换
- **数据管理模块**:处理图像处理、存储和同步
## 3. 功能模块详述
### 3.1 首页模块
#### 3.1.1 功能概述
作为用户进入小程序的第一个接触点,提供简洁的功能入口和项目管理。
#### 3.1.2 主要功能(两个按钮)
- **快速开始**:一键创建新项目(图片上传/拍照)
- **色板设置**:全局店家色号体系选择
#### 3.1.3 界面布局
- 顶部(占据一半):品牌标识
- 中部(大):点击导入图片或者 csv 文件
- 下部(弱):点击进入色板编辑(编辑后的色板储存在本地)
### 3.2 统一色板模块
#### 3.2.1 设计理念
基于 `colorSystemMapping.json` 构建的智能色板系统,对用户完全透明化 hex 值,只显示当前选择店家的色号命名。
#### 3.2.2 核心功能
**3.2.2.1 色板数据管理**
- 维护 291 个标准 hex 颜色
- 支持 5 个店家色号体系MARD、COCO、漫漫、盼盼、咪小窝
- 动态映射:同一颜色在不同店家显示不同色号
- 全局色号体系切换:用户选择店家后,整个小程序所有颜色显示自动更新
**3.2.2.2 自定义色板机制**
- 用户可以自定义自己想要的色板(自己的拼豆颜色种类)
**3.2.2.3 交互设计**
- **预览模式**:色板仅用于信息展示,点击颜色显示详细信息
- **编辑模式**:色板作为画笔工具,支持颜色选择、自定义色板
- **拼豆模式**:色板按使用顺序和重要性排序,突出当前需要的颜色
#### 3.2.3 技术实现要点
- 建立 hex 到店家色号的单向映射索引
- 实现响应式色板组件,支持不同尺寸屏幕
- 色彩空间转换算法,确保颜色显示一致性
### 3.3 统一画布模块
#### 3.3.1 设计理念
单一画布承载所有视觉交互,通过模式切换实现不同功能状态,提供连贯的用户体验。
#### 3.3.2 核心功能
**3.3.2.1 基础画布能力**
- **自适应尺寸**:根据像素网格自动计算画布大小,初始化的时候最长边占满屏幕
- **手势支持**:双指捏合缩放、单指拖拽平移
- **性能优化**:大尺寸网格的虚拟化渲染,保证流畅性
- **坐标系统**:统一的网格坐标系,支持精确的像素级操作
**3.3.2.2 多模式状态管理**
**预览模式Mode 1**
- **视觉状态**:显示完整像素化结果,网格线清晰可见
- **交互行为**
- 点击:显示该位置颜色信息弹窗(店家色号 + 颜色预览)
- 长按:显示周围区域的颜色分布热力图
- 双击:快速放大到该区域
- **UI 覆盖层**:网格坐标标识、色彩统计信息
**编辑模式Mode 2**
- **视觉状态**:突出显示可编辑区域,当前工具状态指示
- **交互行为**
- 单击:使用当前画笔颜色填充像素
- 长按:激活连续绘制模式
- 双指点击:吸管工具,获取该位置颜色
- **工具栏集成**:画笔选择、橡皮擦、填充工具、撤销重做
- **实时预览**:编辑操作的即时视觉反馈
**拼豆模式Mode 3**
- **视觉状态**:游戏化界面,突出显示当前任务区域
- **交互行为**
- 点击:标记完成该像素
- 长按:查看该区域的详细制作指导
- **进度系统**:完成度可视化、区域引导、成就反馈
- **辅助功能**:放大镜工具、颜色高亮、完成动画
**3.3.2.3 模式切换机制**
- **无缝转换**:保持画布视口位置和缩放比例
- **状态持久化**:每个模式的操作历史独立保存
- **上下文保护**:切换模式时保护用户当前操作进度
#### 3.3.3 技术架构
**3.3.3.1 渲染引擎**
- 基于微信小程序 Canvas 2D API
- 分层渲染背景层、像素层、交互层、UI 层
- 局部刷新策略,仅重绘变化区域
**3.3.3.2 状态管理**
```typescript
interface CanvasState {
mode: 'preview' | 'edit' | 'create'
viewport: { x: number, y: number, scale: number }
pixelData: MappedPixel[][]
currentTool: ToolType
operationHistory: Operation[]
}
```
### 3.4 图像转像素画模块(预览模式)
#### 3.4.1 功能概述
将用户上传的图像智能转换为适合拼豆制作的像素画,并提供预览和调整功能。
#### 3.4.2 图像处理流程
**3.4.2.1 图像预处理**
- **尺寸标准化**:自动调整为横轴 100 像素,纵轴按比例计算
- **色彩优化**:色彩空间转换、对比度增强、降噪处理
- **边缘处理**:智能识别和保护重要边缘信息
**3.4.2.2 像素化算法**
- **卡通模式**:基于主导色的像素化(保持色块纯净)
- **真实模式**:基于平均色的像素化(保持色彩过渡)
- **智能色彩映射**:将像素色彩映射到最接近的拼豆颜色
**3.4.2.3 后处理优化**
- **色彩合并**:相似颜色自动合并,减少色彩数量
- **区域优化**:小面积杂色自动清理
- **边缘锐化**:重要边缘的对比度增强
#### 3.4.3 交互功能
- **参数调节**:实时调整像素化精度、色彩合并阈值
- **区域预览**:支持局部放大查看细节效果
- **色彩统计**:显示使用的颜色数量和分布
### 3.5 像素画编辑模块
#### 3.5.1 功能概述
提供专业的像素画编辑能力,允许用户精细调整转换结果。
#### 3.5.2 编辑工具集
**3.5.2.1 基础绘制工具**
- **画笔工具**:支持不同大小的方形画笔
- **橡皮擦**:删除指定区域的像素
- **填充工具**:快速填充连通区域
- **吸管工具**:快速获取画布上的颜色
**3.5.2.2 高级编辑功能**
- **区域选择**:矩形、自由形状选择工具
- **批量替换**:将特定颜色批量替换为另一种颜色
- **图层管理**:支持多图层编辑(背景层、前景层等)
- **对称绘制**:支持水平、垂直对称绘制模式
**3.5.2.3 辅助功能**
- **网格对齐**:自动对齐到像素网格
- **颜色建议**:基于周围颜色推荐最适合的颜色
- **撤销重做**:支持多步操作的撤销和重做
- **实时预览**:编辑过程中的实时效果预览
#### 3.5.3 用户体验优化
- **手势识别**:支持常用编辑手势(缩放、拖拽、点击)
- **工具切换**:快速访问常用工具的浮动工具栏
- **操作反馈**:每个操作都有清晰的视觉反馈
### 3.6 专心拼豆模块
#### 3.6.1 功能概述
游戏化的拼豆制作指导系统,帮助用户有序完成拼豆作品。
#### 3.6.2 核心功能
**3.6.2.1 智能引导系统**
- **制作顺序优化**:基于颜色分布和难易度推荐制作顺序
- **区域推荐**
- 最近优先:推荐距离当前位置最近的未完成区域
- 大块优先:优先推荐大色块区域,提高效率
- 边缘优先:从外围向内部制作,符合拼豆制作习惯
**3.6.2.2 进度管理**
- **颜色进度跟踪**:每种颜色的完成进度可视化
- **区域完成标记**:点击标记已完成的像素区域
- **总体进度展示**:整体完成百分比和剩余工作量
**3.6.2.3 辅助工具**
- **放大镜功能**:支持选择特定区域进行放大操作
- **颜色高亮**:突出显示当前工作颜色的所有位置
- **完成动画**:颜色完成时的庆祝动效
#### 3.6.3 用户体验设计
**3.6.3.1 游戏化元素**
- **成就系统**:设置里程碑奖励(完成第一个颜色、完成 50% 等)
- **进度条动画**:平滑的进度变化动画
- **完成庆祝**:作品完成时的特殊庆祝效果
**3.6.3.2 实用功能**
- **时间记录**:记录制作时间,支持暂停和继续
- **拍照记录**:支持拍照记录制作进度
- **分享功能**:生成完成图片,支持分享到社交平台
### 3.7 数据管理模块
#### 3.7.1 本地存储
- **项目文件**:完整的像素画数据、编辑历史、进度信息
- **用户设置**:色板偏好、操作习惯、界面配置
- **缓存管理**:图像处理结果缓存、色板数据缓存

View File

@@ -1,7 +1,28 @@
import type { NextConfig } from "next";
const withPWA = require("next-pwa")({
dest: "public",
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === "development",
buildExcludes: [/middleware-manifest\.json$/],
runtimeCaching: [
{
urlPattern: /^https?.*/,
handler: "NetworkFirst",
options: {
cacheName: "offlineCache",
expiration: {
maxEntries: 200,
maxAgeSeconds: 30 * 24 * 60 * 60,
},
},
},
],
});
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
export default withPWA(nextConfig);

4265
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@vercel/analytics": "^1.5.0",
"next": "15.3.1",
"next-pwa": "^5.6.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
@@ -22,6 +23,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"sharp": "^0.34.2",
"tailwindcss": "^4",
"typescript": "^5"
}

BIN
public/icon-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
public/icon-256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/icon-384x384.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

49
public/manifest.json Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "拼豆底稿生成器 - 像素画图纸生成工具",
"short_name": "拼豆生成器",
"description": "一个超酷的拼豆底稿生成器,快速上传图片并生成像素画图纸的工具",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#000000",
"orientation": "portrait-primary",
"icons": [
{
"src": "/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/screenshot-mobile.png",
"sizes": "390x844",
"type": "image/png",
"form_factor": "narrow"
},
{
"src": "/screenshot-desktop.png",
"sizes": "1920x1080",
"type": "image/png",
"form_factor": "wide"
}
],
"categories": ["utilities", "productivity"],
"lang": "zh-CN"
}

33
scripts/generate-cert.js Normal file
View File

@@ -0,0 +1,33 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const certDir = path.join(__dirname, '..', 'certificates');
// 创建证书目录
if (!fs.existsSync(certDir)) {
fs.mkdirSync(certDir);
}
console.log('生成本地 HTTPS 证书...');
try {
// 生成私钥
execSync(`openssl genrsa -out ${certDir}/localhost-key.pem 2048`);
// 生成证书请求
execSync(`openssl req -new -key ${certDir}/localhost-key.pem -out ${certDir}/localhost.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=Dev/CN=localhost"`);
// 生成自签名证书
execSync(`openssl x509 -req -in ${certDir}/localhost.csr -signkey ${certDir}/localhost-key.pem -out ${certDir}/localhost.pem -days 365`);
console.log('证书生成成功!');
console.log(`证书位置: ${certDir}/localhost.pem`);
console.log(`私钥位置: ${certDir}/localhost-key.pem`);
// 清理 CSR 文件
fs.unlinkSync(`${certDir}/localhost.csr`);
} catch (error) {
console.error('生成证书失败:', error.message);
process.exit(1);
}

62
scripts/generate-icons.js Normal file
View File

@@ -0,0 +1,62 @@
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const sizes = [192, 256, 384, 512];
const generateIcon = async (size) => {
const svg = `
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8B5CF6;stop-opacity:1" />
<stop offset="50%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#10B981;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="${size}" height="${size}" rx="${size * 0.15}" fill="#000000"/>
<rect width="${size}" height="${size}" rx="${size * 0.15}" fill="url(#grad1)" opacity="0.1"/>
<!-- 拼豆图案 -->
<g transform="translate(${size * 0.2}, ${size * 0.2})">
${generatePixelPattern(size * 0.6)}
</g>
<!-- 文字 -->
<text x="${size / 2}" y="${size * 0.85}" font-family="Arial, sans-serif" font-size="${size * 0.1}" font-weight="bold" text-anchor="middle" fill="white">拼豆</text>
</svg>
`;
const outputPath = path.join(__dirname, '..', 'public', `icon-${size}x${size}.png`);
await sharp(Buffer.from(svg))
.png()
.toFile(outputPath);
console.log(`生成图标: ${outputPath}`);
};
function generatePixelPattern(size) {
const pixelSize = size / 8;
const colors = ['#8B5CF6', '#3B82F6', '#10B981', '#F59E0B', '#EF4444'];
let pattern = '';
for (let y = 0; y < 6; y++) {
for (let x = 0; x < 6; x++) {
if (Math.random() > 0.3) {
const color = colors[Math.floor(Math.random() * colors.length)];
pattern += `<rect x="${x * pixelSize}" y="${y * pixelSize}" width="${pixelSize * 0.9}" height="${pixelSize * 0.9}" rx="${pixelSize * 0.1}" fill="${color}" opacity="0.8"/>`;
}
}
}
return pattern;
}
async function generateAllIcons() {
for (const size of sizes) {
await generateIcon(size);
}
}
generateAllIcons().catch(console.error);

45
server.js Normal file
View File

@@ -0,0 +1,45 @@
const { createServer } = require('https');
const { parse } = require('url');
const next = require('next');
const fs = require('fs');
const path = require('path');
const dev = process.env.NODE_ENV !== 'production';
const hostname = '0.0.0.0';
const port = 3001;
const app = next({ dev });
const handle = app.getRequestHandler();
// 检查证书是否存在
const certPath = path.join(__dirname, 'certificates', 'localhost.pem');
const keyPath = path.join(__dirname, 'certificates', 'localhost-key.pem');
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
console.error('HTTPS 证书不存在!请先运行: node scripts/generate-cert.js');
process.exit(1);
}
const httpsOptions = {
key: fs.readFileSync(keyPath),
cert: fs.readFileSync(certPath),
};
app.prepare().then(() => {
createServer(httpsOptions, async (req, res) => {
try {
const parsedUrl = parse(req.url, true);
await handle(req, res, parsedUrl);
} catch (err) {
console.error('Error occurred handling', req.url, err);
res.statusCode = 500;
res.end('internal server error');
}
}).listen(port, hostname, (err) => {
if (err) throw err;
console.log(`> HTTPS Server ready on https://${hostname}:${port}`);
console.log('> 使用自签名证书,浏览器可能会显示安全警告');
console.log('> 在手机上访问时,请使用电脑的局域网 IP 地址');
console.log(`> 例如: https://YOUR_COMPUTER_IP:${port}`);
});
});

View File

@@ -1,4 +1,4 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import "./globals.css";
@@ -16,6 +16,29 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "七卡瓦拼豆底稿生成器 | Perler Beads Generator",
description: "上传图片,调整精细度,一键生成像素画图纸,简单实用的像素画生成工具",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "拼豆生成器",
},
icons: {
icon: [
{ url: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/icon-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: [
{ url: "/icon-192x192.png", sizes: "192x192", type: "image/png" },
],
},
};
export const viewport: Viewport = {
themeColor: "#000000",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
};
export default function RootLayout({

View File

@@ -2,6 +2,7 @@
import React, { useState, useRef, ChangeEvent, DragEvent, useEffect, useMemo, useCallback } from 'react';
import Script from 'next/script';
import InstallPWA from '../components/InstallPWA';
// 导入像素化工具和类型
import {
@@ -1749,6 +1750,9 @@ export default function Home() {
{/* 添加自定义动画样式 */}
<style dangerouslySetInnerHTML={{ __html: floatAnimation }} />
{/* PWA 安装按钮 */}
<InstallPWA />
{/* ++ 修改:添加 onLoad 回调函数 ++ */}
<Script
async

141
src/app/pwa-debug/page.tsx Normal file
View File

@@ -0,0 +1,141 @@
'use client';
import { useEffect, useState } from 'react';
export default function PWADebug() {
const [debugInfo, setDebugInfo] = useState<{
manifest: object | null | { error: string };
serviceWorker: object | null;
https: boolean;
standalone: boolean;
installable: boolean;
installPromptSupported?: boolean;
}>({
manifest: null,
serviceWorker: null,
https: false,
standalone: false,
installable: false,
});
useEffect(() => {
const checkPWA = async () => {
const info: {
manifest?: object | null | { error: string };
serviceWorker?: object | null;
https?: boolean;
standalone?: boolean;
installable?: boolean;
installPromptSupported?: boolean;
} = {};
// 检查 HTTPS
info.https = window.location.protocol === 'https:' || window.location.hostname === 'localhost';
// 检查 Service Worker
if ('serviceWorker' in navigator) {
try {
const registrations = await navigator.serviceWorker.getRegistrations();
info.serviceWorker = {
supported: true,
registrations: registrations.length,
active: registrations.some(reg => reg.active),
};
} catch (e) {
info.serviceWorker = { error: e instanceof Error ? e.message : 'Unknown error' };
}
} else {
info.serviceWorker = { supported: false };
}
// 检查 manifest
const manifestLink = document.querySelector('link[rel="manifest"]');
if (manifestLink) {
try {
const response = await fetch(manifestLink.getAttribute('href') || '');
const manifest = await response.json();
info.manifest = manifest;
} catch (e) {
info.manifest = { error: e instanceof Error ? e.message : 'Unknown error' };
}
} else {
info.manifest = { error: 'No manifest link found' };
}
// 检查是否独立模式
info.standalone = window.matchMedia('(display-mode: standalone)').matches;
// 检查 beforeinstallprompt
info.installPromptSupported = 'onbeforeinstallprompt' in window;
setDebugInfo({
manifest: info.manifest || null,
serviceWorker: info.serviceWorker || null,
https: info.https || false,
standalone: info.standalone || false,
installable: info.installable || false,
installPromptSupported: info.installPromptSupported,
});
};
checkPWA();
}, []);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold mb-8">PWA </h1>
<div className="space-y-6">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4"></h2>
<ul className="space-y-2">
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.https ? 'bg-green-500' : 'bg-red-500'}`}></span>
HTTPS: {debugInfo.https ? '是' : '否'} ({typeof window !== 'undefined' ? window.location.protocol : 'N/A'})
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.serviceWorker ? 'bg-green-500' : 'bg-red-500'}`}></span>
Service Worker: {JSON.stringify(debugInfo.serviceWorker, null, 2)}
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.standalone ? 'bg-green-500' : 'bg-gray-400'}`}></span>
: {debugInfo.standalone ? '是' : '否'}
</li>
<li className="flex items-center gap-2">
<span className={`w-4 h-4 rounded-full ${debugInfo.installPromptSupported ? 'bg-green-500' : 'bg-red-500'}`}></span>
: {debugInfo.installPromptSupported ? '支持' : '不支持'}
</li>
</ul>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4">Manifest </h2>
<pre className="bg-gray-100 dark:bg-gray-900 p-4 rounded overflow-auto">
{JSON.stringify(debugInfo.manifest, null, 2)}
</pre>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow">
<h2 className="text-xl font-semibold mb-4"></h2>
<div className="space-y-2 text-gray-600 dark:text-gray-300">
<p><strong>iOS Safari:</strong></p>
<ol className="list-decimal list-inside ml-4">
<li></li>
<li>&ldquo;&rdquo;</li>
<li>&ldquo;&rdquo;</li>
</ol>
<p className="mt-4"><strong>Android Chrome/Edge:</strong></p>
<ol className="list-decimal list-inside ml-4">
<li></li>
<li>&ldquo;&rdquo;&ldquo;&rdquo;</li>
<li>&ldquo;&rdquo;&ldquo;&rdquo;</li>
</ol>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
'use client';
import { useEffect, useState } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export default function InstallPWA() {
const [supportsPWA, setSupportsPWA] = useState(false);
const [promptInstall, setPromptInstall] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
const handler = (e: BeforeInstallPromptEvent) => {
e.preventDefault();
console.log('PWA 安装提示已准备');
setSupportsPWA(true);
setPromptInstall(e);
};
window.addEventListener('beforeinstallprompt', handler as EventListener);
// 检查是否已安装
if (window.matchMedia('(display-mode: standalone)').matches) {
setIsInstalled(true);
}
return () => window.removeEventListener('beforeinstallprompt', handler as EventListener);
}, []);
const onClick = async (evt: React.MouseEvent) => {
evt.preventDefault();
if (!promptInstall) {
return;
}
promptInstall.prompt();
const { outcome } = await promptInstall.userChoice;
if (outcome === 'accepted') {
setPromptInstall(null);
setSupportsPWA(false);
}
};
if (isInstalled) {
return null;
}
if (!supportsPWA) {
return null;
}
return (
<button
className="fixed bottom-6 right-6 bg-gradient-to-r from-purple-500 to-blue-500 text-white px-6 py-3 rounded-full shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-200 flex items-center gap-2 z-50"
onClick={onClick}
aria-label="安装应用"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m0 0l-4-4m4 4l4-4M5 12h14" />
</svg>
</button>
);
}