Compare commits

..

45 Commits

Author SHA1 Message Date
yuantao
9d9df71f28 修复 修改uniapp-error-monitor依赖为最新版本 2025-12-02 16:39:26 +08:00
yuantao
22b1fcdca2 修复 修改uniapp-error-monitor依赖为最新版本 2025-12-02 16:17:39 +08:00
yuantao
cd2b838647 优化 重构错误监控架构,移除tool.js中的错误监控代码,改为使用uniapp-error-monitor外部库,实现职责分离和代码简化 2025-12-02 10:28:16 +08:00
yuantao
1ca6e6c77a 优化 错误监控功能重构 - 集成uniapp-error-monitor依赖,实现优雅降级机制和完整环境兼容 2025-12-01 19:07:15 +08:00
yuantao
3fbb7294fb 新增 API请求错误自动上报功能 2025-12-01 16:10:42 +08:00
yuantao
ba839a1e47 新增 错误上报功能增加项目名称和版本号信息 2025-12-01 16:03:05 +08:00
yuantao
31566408d3 新增 错误上报功能增加项目名称和版本号信息 2025-12-01 15:11:05 +08:00
yuantao
5c6035b6ab 优化 移除多余的环境变量 2025-12-01 14:56:14 +08:00
yuantao
328a53a343 优化 环境控制,错误监控只在生产环境启用 2025-12-01 14:44:29 +08:00
yuantao
bfcf289acb 新增 全局JS错误监控模块,集成WebHook上报功能 2025-12-01 14:40:51 +08:00
yuantao
d6957e7f36 iflow描述文件调优 2025-12-01 14:01:23 +08:00
yuantao
35fdd22a4f iflow描述文件调优 2025-11-21 12:59:09 +08:00
yuantao
6ce1e37cbd iflow描述文件调优 2025-11-21 11:13:34 +08:00
yuantao
1f3600103e 编译配置更新 2025-11-13 10:05:13 +08:00
yuantao
d8e560536a iflow描述文件调优 2025-11-11 18:29:27 +08:00
yuantao
2b99835356 补全遗失的状态管理引用 2025-11-11 17:09:54 +08:00
yuantao
ca6bf7f211 iflow描述文件调优 2025-10-31 16:36:40 +08:00
yuantao
5e1299db5c iflow描述文件调优 2025-10-31 16:30:34 +08:00
yuantao
49c6854e8e 优化原子类样式 2025-10-31 14:51:27 +08:00
yuantao
d5a9bb95c3 iflow提交命令调优 2025-10-31 13:53:08 +08:00
yuantao
c247cbe1c7 iflow描述文件调优 2025-10-29 14:45:00 +08:00
yuantao
7030498a83 iflow描述文件调优 2025-10-29 13:50:28 +08:00
yuantao
edba077718 iflow描述文件调优 2025-10-29 10:02:55 +08:00
yuantao
cee0c70427 导入全局样式变量 2025-10-29 09:58:25 +08:00
yuantao
de533d2ef5 iflow描述文件调优 2025-10-28 18:02:14 +08:00
yuantao
6f9c9e06f1 iflow描述文件调优 2025-10-28 14:44:53 +08:00
yuantao
c7baff94e8 修改iflow描述文件 2025-10-28 13:47:00 +08:00
yuantao
81a16966a4 新增初始化tabbar以及静态资源 2025-10-28 13:00:47 +08:00
yuantao
1f42981fd4 修复:vite编译配置在热重载的情况下导致内存溢出的问题 2025-10-28 11:52:58 +08:00
yuantao
ad0f64a48a 添加初始化目录结构 2025-10-28 10:33:00 +08:00
yuantao
726985d38f 调整iflow配置文件 2025-10-23 14:16:40 +08:00
yuantao
729d5af382 iflow命令修改 2025-10-15 17:33:31 +08:00
yuantao
b5d4684289 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-15 09:10:41 +08:00
838d5bdb22 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-15 01:21:25 +08:00
101e3eb2a1 - 添加了三个新的 iFlow CLI 命令配置文件
- cleanproject.toml: 用于清理开发工件的命令
  - commit.toml: 用于分析更改并创建有意义提交消息的命令
  - docs.toml: 用于智能文档管理和更新的命令
  - 这些命令将帮助提高开发效率和项目维护质量
2025-10-15 01:21:22 +08:00
yuantao
2280b5c3f4 优化 项目中可以使用@别名了 2025-10-14 18:00:13 +08:00
yuantao
e3823f7439 更新描述 2025-10-14 13:38:20 +08:00
yuantao
48125bd0ec js规范调整 2025-10-14 09:48:33 +08:00
5c01f10808 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-12 17:20:50 +08:00
3f47967f8f 新增agents配置 2025-10-12 17:20:47 +08:00
yuantao
1e29e845e8 Merge branch 'master' of https://git.pandorastudio.cn/yuantao/template-MP 2025-10-11 09:24:12 +08:00
yuantao
35ccffc9e4 移除无用的npm脚本 2025-10-11 09:22:49 +08:00
0c658a3f31 node版本更新 2025-10-09 02:00:24 +08:00
yuantao
0516ab54cf 自述文件修改 2025-09-29 14:32:27 +08:00
yuantao
f620288c5b IFLOW描述文件更新;
优化 编译配置增加动态修改manifest.json文件;
2025-09-29 14:27:36 +08:00
42 changed files with 4735 additions and 126 deletions

8
.env
View File

@@ -1,2 +1,6 @@
VITE_BASE_URL='' #接口地址 VITE_BASE_URL= #接口地址
VITE_ASSETSURL='https://cdn.vrupup.com/s/1598/assets/' #资源地址 VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/ #资源地址
VITE_APPID=wx9cb717d8151d8486 #小程序APPID
VITE_UNI_APPID=_UNI_8842336 #UNI-APPID
VITE_LIBVERSION=3.0.0 #微信小程序基础库
VITE_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=9a401eb2-065a-4882-82e9-b438bcd1eac4#WEBHOOK地址

View File

@@ -0,0 +1,30 @@
---
agent-type: code-reviewer
name: code-reviewer
description: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code.
when-to-use: Expert code review specialist. Proactively reviews code for quality, security, and maintainability. Use immediately after writing or modifying code.
allowed-tools:
model: gpt-4
inherit-tools: true
inherit-mcps: true
color: blue
---
You are a senior code reviewer with deep expertise in configuration security and production reliability. Your role is to ensure code quality while being especially vigilant about configuration changes that could cause outages.
When invoked:
1. Run git diff to see recent changes
2. Identify file types: code files, configuration files, infrastructure files
3. Apply appropriate review strategies for each type
4. Begin review immediately with heightened scrutiny for configuration changes
## Configuration Change Review (CRITICAL FOCUS)
### Magic Number Detection
For ANY numeric value change in configuration files:
- **ALWAYS QUESTION**: Why this specific value? What's the justification?
- **REQUIRE EVIDENCE**: Has this been tested under production-like load?
- **CHECK BOUNDS**: Is this within recommended ranges for your system?
- **ASSESS IMPACT**: What happens if this limit is reached?
Focus on fixing the underlying issue, not just symptoms. Always prioritize preventing production outages.

36
.iflow/agents/debugger.md Normal file
View File

@@ -0,0 +1,36 @@
---
agent-type: debugger
name: debugger
description: Debugging specialist for errors, test failures, and unexpected behavior. Use proactively when encountering any issues.
when-to-use: Debugging specialist for errors, test failures, and unexpected behavior. Use proactively when encountering any issues.
allowed-tools:
model: gpt-4
inherit-tools: true
inherit-mcps: true
color: red
---
You are an expert debugger specializing in root cause analysis.
When invoked:
1. Capture error message and stack trace
2. Identify reproduction steps
3. Isolate the failure location
4. Implement minimal fix
5. Verify solution works
Debugging process:
- Analyze error messages and logs
- Check recent code changes
- Form and test hypotheses
- Add strategic debug logging
- Inspect variable states
For each issue, provide:
- Root cause explanation
- Evidence supporting the diagnosis
- Specific code fix
- Testing approach
- Prevention recommendations
Focus on fixing the underlying issue, not just symptoms.

View File

@@ -0,0 +1,23 @@
---
agent-type: frontend-developer
name: frontend-developer
description: Build React components, implement responsive layouts, and handle client-side state management.
when-to-use: Build React components, implement responsive layouts, and handle client-side state management.
allowed-tools:
model: gpt-4
inherit-tools: true
inherit-mcps: true
color: orange
---
You are a frontend developer specializing in modern web development with React and TypeScript. Create responsive, accessible, and performant user interfaces.
Core skills:
- React components and hooks
- TypeScript for type safety
- Responsive CSS with Tailwind or styled-components
- State management (Redux, Zustand, Context API)
- Performance optimization
- Accessibility (WCAG compliance)
- Testing with React Testing Library
Implement modern patterns like server components, suspense, and progressive enhancement.

View File

@@ -0,0 +1,41 @@
---
agent-type: javascript-pro
name: javascript-pro
description: Master modern JavaScript with ES6+, async patterns, and Node.js APIs. Handles promises, event loops, and browser/Node compatibility. Use PROACTIVELY for JavaScript optimization, async debugging, or complex JS patterns.
when-to-use: Master modern JavaScript with ES6+, async patterns, and Node.js APIs. Handles promises, event loops, and browser/Node compatibility. Use PROACTIVELY for JavaScript optimization, async debugging, or complex JS patterns.
allowed-tools:
model: sonnet
inherit-tools: true
inherit-mcps: true
color: orange
---
You are a JavaScript expert specializing in modern JS and async programming.
## Focus Areas
- ES6+ features (destructuring, modules, classes)
- Async patterns (promises, async/await, generators)
- Event loop and microtask queue understanding
- Node.js APIs and performance optimization
- Browser APIs and cross-browser compatibility
- TypeScript migration and type safety
## Approach
1. Prefer async/await over promise chains
2. Use functional patterns where appropriate
3. Handle errors at appropriate boundaries
4. Avoid callback hell with modern patterns
5. Consider bundle size for browser code
## Output
- Modern JavaScript with proper error handling
- Async code with race condition prevention
- Module structure with clean exports
- Jest tests with async test patterns
- Performance profiling results
- Polyfill strategy for browser compatibility
Support both Node.js and browser environments. Include JSDoc comments.

View File

@@ -0,0 +1,38 @@
---
agent-type: test-automator
name: test-automator
description: Create comprehensive test suites with unit, integration, and e2e tests. Sets up CI pipelines, mocking strategies, and test data. Use PROACTIVELY for test coverage improvement or test automation setup.
when-to-use: Create comprehensive test suites with unit, integration, and e2e tests. Sets up CI pipelines, mocking strategies, and test data. Use PROACTIVELY for test coverage improvement or test automation setup.
allowed-tools:
model: sonnet
inherit-tools: true
inherit-mcps: true
color: brown
---
You are a test automation specialist focused on comprehensive testing strategies.
## Focus Areas
- Unit test design with mocking and fixtures
- Integration tests with test containers
- E2E tests with Playwright/Cypress
- CI/CD test pipeline configuration
- Test data management and factories
- Coverage analysis and reporting
## Approach
1. Test pyramid - many unit, fewer integration, minimal E2E
2. Arrange-Act-Assert pattern
3. Test behavior, not implementation
4. Deterministic tests - no flakiness
5. Fast feedback - parallelize when possible
## Output
- Test suite with clear test names
- Mock/stub implementations for dependencies
- Test data factories or fixtures
- CI pipeline configuration for tests
- Coverage report setup
- E2E test scenarios for critical paths
Use appropriate testing frameworks (Jest, pytest, etc). Include both happy and edge cases.

View File

@@ -0,0 +1,39 @@
---
agent-type: typescript-pro
name: typescript-pro
description: Master TypeScript with advanced types, generics, and strict type safety. Handles complex type systems, decorators, and enterprise-grade patterns. Use PROACTIVELY for TypeScript architecture, type inference optimization, or advanced typing patterns.
when-to-use: Master TypeScript with advanced types, generics, and strict type safety. Handles complex type systems, decorators, and enterprise-grade patterns. Use PROACTIVELY for TypeScript architecture, type inference optimization, or advanced typing patterns.
allowed-tools:
model: sonnet
inherit-tools: true
inherit-mcps: true
color: yellow
---
You are a TypeScript expert specializing in advanced typing and enterprise-grade development.
## Focus Areas
- Advanced type systems (generics, conditional types, mapped types)
- Strict TypeScript configuration and compiler options
- Type inference optimization and utility types
- Decorators and metadata programming
- Module systems and namespace organization
- Integration with modern frameworks (React, Node.js, Express)
## Approach
1. Leverage strict type checking with appropriate compiler flags
2. Use generics and utility types for maximum type safety
3. Prefer type inference over explicit annotations when clear
4. Design robust interfaces and abstract classes
5. Implement proper error boundaries with typed exceptions
6. Optimize build times with incremental compilation
## Output
- Strongly-typed TypeScript with comprehensive interfaces
- Generic functions and classes with proper constraints
- Custom utility types and advanced type manipulations
- Jest/Vitest tests with proper type assertions
- TSConfig optimization for project requirements
- Type declaration files (.d.ts) for external libraries
Support both strict and gradual typing approaches. Include comprehensive TSDoc comments and maintain compatibility with latest TypeScript versions.

View File

@@ -0,0 +1,41 @@
---
agent-type: ui-ux-designer
name: ui-ux-designer
description: Create interface designs, wireframes, and design systems. Masters user research, prototyping, and accessibility standards. Use PROACTIVELY for design systems, user flows, or interface optimization.
when-to-use: Create interface designs, wireframes, and design systems. Masters user research, prototyping, and accessibility standards. Use PROACTIVELY for design systems, user flows, or interface optimization.
allowed-tools:
model: sonnet
inherit-tools: true
inherit-mcps: true
color: purple
---
You are a UI/UX designer specializing in user-centered design and interface systems.
## Focus Areas
- User research and persona development
- Wireframing and prototyping workflows
- Design system creation and maintenance
- Accessibility and inclusive design principles
- Information architecture and user flows
- Usability testing and iteration strategies
## Approach
1. User needs first - design with empathy and data
2. Progressive disclosure for complex interfaces
3. Consistent design patterns and components
4. Mobile-first responsive design thinking
5. Accessibility built-in from the start
## Output
- User journey maps and flow diagrams
- Low and high-fidelity wireframes
- Design system components and guidelines
- Prototype specifications for development
- Accessibility annotations and requirements
- Usability testing plans and metrics
Focus on solving user problems. Include design rationale and implementation notes.

View File

@@ -0,0 +1,11 @@
# Command: cleanproject
# Description: Clean up development artifacts while preserving your working code
# Category: utility
# Version: 1
# Author: 10169
description = "Clean up development artifacts while preserving your working code"
prompt = """
I'll help clean up development artifacts while preserving your working code. I'll identify cleanup targets using native tools: Glob tool to find temporary and debug files, Grep tool to detect debug statements in code, Read tool to verify file contents before removal. Critical directories are automatically protected: .claude directory (commands and configurations), .git directory (version control), node_modules, vendor (dependency directories), Essential configuration files. When I find multiple items to clean, I'll create a todo list to process them systematically.
"""

View File

@@ -0,0 +1,11 @@
# Command: commit
# Description: Analyze your changes and create a meaningful commit message
# Category: utility
# Version: 1
# Author: 10169
description = "分析您的更改并创建有意义的提交消息"
prompt = """
我会分析您的更改并创建一条有意义的提交消息并使用简单的git命令进行提交更改。我会分析更改以确定修改了哪些文件、更改的性质功能、修复、重构等以及受影响的范围/组件。基于分析结果、以及遵循项目的IFLOW上下文中的提交规范我会创建一条常规的中文提交消息其中包含类型新增|修复|文档|优化|其他)、主题(用现在时清晰描述)。
"""

11
.iflow/commands/docs.toml Normal file
View File

@@ -0,0 +1,11 @@
# Command: docs
# Description: Smart documentation management and updates
# Category: documentation
# Version: 1
# Author: 10169
description = "智能文档管理和更新"
prompt = """
我将根据代码变更更新、整理和维护文档,帮助您智能地管理文档。我将同步文档与实现变更,识别过时的章节,提出改进建议,并确保文档之间的一致性。功能包括自动文档更新、交叉引用链接、版本跟踪和质量评估。
"""

View File

@@ -1,16 +1,18 @@
{ {
"language": "zh-CN",
"mcpServers": { "mcpServers": {
"Framelink Figma MCP": { "bing-cn-mcp": {
"description": "为AI编程工具提供Figma设计文件访问能力支持获取设计数据和下载图像资源帮助实现设计到代码的一键转换。", "description": "MCP 服务器,提供中文必应搜索结果抓取和网页内容抓取功能,支持中文搜索",
"command": "npx", "command": "npx",
"args": ["-y", "@iflow-mcp/figma-developer-mcp@0.5.0", "--stdio"], "args": ["-y", "@iflow-mcp/bing-cn-mcp"],
"env": { "env": {
"FIGMA_API_KEY": "" "USER_AGENT": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
} }
}, },
"playwright": { "chrome-devtools": {
"description": "Chrome浏览器自动化工具支持页面操作、性能分析、网络监控等功能",
"command": "npx", "command": "npx",
"args": ["@playwright/mcp@latest"] "args": ["-y", "@iflow-mcp/chrome-devtools-mcp"]
}, },
"context7": { "context7": {
"description": "为开发者提供最新技术文档和代码示例的智能检索服务,支持库搜索和文档获取功能", "description": "为开发者提供最新技术文档和代码示例的智能检索服务,支持库搜索和文档获取功能",
@@ -22,15 +24,6 @@
"command": "npx", "command": "npx",
"args": ["-y", "@iflow-mcp/fetch@1.0.2"] "args": ["-y", "@iflow-mcp/fetch@1.0.2"]
}, },
"server-puppeteer": {
"description": "基于Puppeteer的浏览器自动化工具支持页面导航、截图、元素操作和JavaScript执行等功能",
"command": "npx",
"args": ["-y", "@iflow-mcp/server-puppeteer@0.6.2"],
"env": {
"PUPPETEER_LAUNCH_OPTIONS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}",
"ALLOW_DANGEROUS": "{\"executablePath\":\"C:\\Program Files\\Google\\Chrome\\Application\"}"
}
},
"math-tools": { "math-tools": {
"description": "提供34个数学运算工具包含算术运算、统计计算、数据科学和表达式求值功能支持高精度计算。", "description": "提供34个数学运算工具包含算术运算、统计计算、数据科学和表达式求值功能支持高精度计算。",
"command": "npx", "command": "npx",
@@ -54,10 +47,13 @@
"command": "npx", "command": "npx",
"args": ["-y", "@iflow-mcp/server-filesystem@0.6.2", "E:\\"] "args": ["-y", "@iflow-mcp/server-filesystem@0.6.2", "E:\\"]
}, },
"mcp-server-code-runner": { "Framelink Figma MCP": {
"description": "多语言代码执行器支持35种编程语言的代码片段执行包括JavaScript、Python、Go、Java等主流语言", "description": "为AI编程工具提供Figma设计文件访问能力支持获取设计数据和下载图像资源帮助实现设计到代码的一键转换。",
"command": "npx", "command": "npx",
"args": ["-y", "@iflow-mcp/mcp-server-code-runner"] "args": ["-y", "@iflow-mcp/figma-developer-mcp@0.5.0", "--stdio"],
"env": {
"FIGMA_API_KEY": ""
}
}, },
"gitlab": { "gitlab": {
"description": "提供GitLab项目管理、文件操作、Issue管理、合并请求等功能的MCP服务器工具集", "description": "提供GitLab项目管理、文件操作、Issue管理、合并请求等功能的MCP服务器工具集",

1
.manifest-updated Normal file
View File

@@ -0,0 +1 @@
Manifest updated by HBuilder first compile

View File

@@ -1 +1 @@
20.0.0 24.0.1

View File

@@ -9,6 +9,7 @@ export default {
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "./uni.scss";
@import '@/common/styles/common.css'; @import '@/common/styles/common.css';
@import '@/common/styles/base.scss'; @import '@/common/styles/base.scss';
@import '@/uview-plus/index.scss'; @import '@/uview-plus/index.scss';

292
IFLOW.md
View File

@@ -1,4 +1,4 @@
# 项目概述 # 项目概述 - IFLOW 上下文
这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数,方便快速开发微信小程序。 这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数,方便快速开发微信小程序。
@@ -8,115 +8,301 @@
* **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面。 * **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面。
* **uView-Plus**: 基于 UniApp 的 UI 组件库。 * **uView-Plus**: 基于 UniApp 的 UI 组件库。
* **z-paging**: 一个用于处理分页加载的组件库。 * **z-paging**: 一个用于处理分页加载的组件库。
* **Vuex**: 状态管理库,用于统一管理应用状态。
* **Vite**: 现代化构建工具配置了自定义插件用于manifest.json的appid替换。
* **luch-request**: 网络请求库,提供请求封装和拦截器功能。
## 目录结构 ## 目录结构
``` ```
. .
├── api/ # 接口相关 ├── api/ # 接口相关
│ ├── modules/ # 业务接口 │ ├── modules/ # 业务接口(目录存在待完善)
│ └── request.js # 请求封装 │ └── request.js # 请求封装
├── common/ # 公共资源 ├── common/ # 公共资源
│ ├── styles/ # 全局样式 │ ├── styles/ # 全局样式
│ │ ├── common.css # codefun原子类样式 │ │ ├── common.css # codefun原子类样式(预留)
│ │ └── base.scss # 全局样式变量 │ │ └── base.scss # 全局样式变量(预留)
│ └── utils/ # 工具函数 │ └── utils/ # 工具函数
├── components/ # 公共组件 │ └── tool.js # 常用工具函数
├── components/ # 公共组件(目录存在待完善)
├── wxcomponents/ # 微信原生组件
│ └── painter/ # 海报绘制组件
├── uni_modules/ # uni-app 组件 ├── uni_modules/ # uni-app 组件
│ └── z-paging/ # 分页组件库
├── lib/ # 第三方库 ├── lib/ # 第三方库
│ └── luch-request/ # luch-request 网络请求库 │ └── luch-request/ # luch-request 网络请求库
├── uview-plus/ # uView-Plus 组件库 ├── uview-plus/ # uView-Plus 组件库
├── mixins/ # Vue 混入 ├── mixins/ # Vue 混入
├── store/ # 状态管理 │ └── global.js # 全局混入
├── pages/ # 主包页面 ├── pages/ # 主包页面
│ └── index/ # 首页
├── subPages/ # 分包页面 ├── subPages/ # 分包页面
│ └── welcome/ # 欢迎页
├── static/ # 静态资源文件
│ └── assets/ # 静态图片资源
├── store/ # 状态管理
├── App.vue # 应用入口 ├── App.vue # 应用入口
├── main.js # 主入口文件 ├── main.js # 主入口文件
├── pages.json # 页面配置 ├── pages.json # 页面配置
├── manifest.json # 应用配置 ├── manifest.json # 应用配置
├── uni.scss # 全局样式变量 ├── uni.scss # 全局样式变量
├── vite.config.js # Vite 编译配置 ├── vite.config.js # Vite 编译配置
── .env # 环境变量 ── .nvmdrc # Node.js 版本要求
├── .env # 环境变量
├── .iflow/ # iFlow CLI 配置
├── package.json # 项目依赖配置
└── uni.promisify.adaptor.js # uni-app promise 适配器
``` ```
# 开发环境与运行 ## 开发环境与运行
## 环境要求 ### 环境要求
* Node.js (版本信息在 `.nvmdrc` 文件中指定) * Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 24.0.1)
* npm 或 yarn * npm 或 yarn
## 安装依赖 ### 当前依赖
```json
{
"dependencies": {
"dotenv": "^17.2.2",
"dayjs": "*",
"vue": "^3.5.21"
}
}
```
### 安装依赖
```bash ```bash
npm install npm install
``` ```
## 运行项目 ### 运行项目
由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试。具体的运行命令取决于你的开发环境。 由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试
## 构建项目 1. 使用 HBuilderX 打开项目
2. 选择"运行到小程序模拟器"或"运行到手机或模拟器"
3. 选择微信开发者工具
### 构建项目
同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。 同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。
# 代码规范与开发约定 ## 代码规范与开发约定
## 样式 ## 样式规范
* 全局样式文件位于 `common/styles/` 目录下,包括 `common.css``normal.scss` * `uni.scss` 提供了全局样式变量
* 样式优先使用codefun原子类样式。 * **全局样式**: 位于 `common/styles/` 目录
* 样式规范应遵循项目中已有的风格。 * `common.css`: CodeFun原子类样式用于快速布局
* `base.scss`: SCSS变量、Mixins和常用样式类生成器
* **样式规范**: 遵循项目中已有的样式风格,统一使用原子类
## JavaScript ## JavaScript规范
* 严格遵循ES6规范 * **ES6+**: 严格遵循ES6+语法规范
* 遵循JavaScript函数式编程范式 * **函数式编程**: 优先使用函数式编程范式
* 方法类函数应该使用 `function` 进行定义。 * **函数定义**: 使用 `function` 关键字定义方法函数
* 避免出现超过4个以上的 `ref`超过4个则使用 `reactive` * **响应式数据**:
* 页面的生命周期按需进行导入,如(`import { onLoad } from '@dcloudio/uni-app'`)。 * 少于4个 `ref` 时直接使用 `ref`
* 全局变量都集中放置于代码顶部。 * 超过4个时使用 `reactive` 进行封装
* 变量名使用小驼峰命名法。 * **生命周期**: 通过 `@dcloudio/uni-app` 按需导入,如:
* 常量名使用全大写。 ```javascript
* 状态类变量命名参考 `isLogin``isOpen` import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
* 事件类方法命名参考 `onClick``onSelect` ```
* 变量都应该写有注释说明、类型说明。 页面的加载、挂载生命周期不要使用 `onMounted` 应该使用 `onLoad`
* `Promise` 方法使用 `async` `await` 写法,并进行容错处理。 * **变量命名**:
* 字符串拼接使用ES6的模板语法。 * 小驼峰命名法: `userName`, `orderList`
* JavaScript规范应遵循项目中已有的风格。 * 常量全大写: `MAX_LENGTH`, `DEFAULT_VALUE`
* 状态类变量: `isLogin`, `isOpen`, `isLoading`
* 事件方法: `onClick`, `onSelect`, `onSubmit`
* **注释规范**: 变量和方法必须有注释说明和类型说明
* **异步处理**: 所有Promise类方法使用 `async/await` 写法,避免 `.then` 嵌套
* **字符串拼接**: 使用ES6模板语法 `` `string ${variable}` ``
## 静态资源 ## 静态资源
* 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `<template></template>` 中直接使用。 * **资源路径**: 使用全局混入的 `ASSETSURL` 变量
* 所有静态资源URL应该使用 `ASSETSURL` 进行拼接,如:`${ASSETSURL}simple.png` * **使用方式**:
```javascript
// 在template中
background-image: url($ASSETSURL + 'image.png')
// 在JavaScript中
const imageUrl = `${ASSETSURL}image.png`
```
## 工具函数 (tool.js) ## 工具函数 (tool.js)
`common/utils/tool.js` 文件提供了一系列常用的工具函数: `common/utils/tool.js` 提供完整的工具函数
* **提示与加载**: `alert`, `loading`, `hideLoading` ### 提示与加载
* **页面跳转**: `navigateTo`, `redirectTo`, `reLaunch`, `switchTab`, `navigateBack` * `alert()`: 文字轻提示
* **本地存储**: `storage`, `removeStorage`, `getStorageInfo` * `loading()`: 显示加载状态
* **其他功能**: `copy` (复制文本), `saveImageToPhotos` (保存图片), `requestPayment` (微信支付), `upload` (文件上传) * `hideLoading()`: 隐藏加载状态
## 网络请求 ### 页面跳转
* `navigateTo()`: 跳转到指定页面
* `redirectTo()`: 关闭当前页面并跳转
* `reLaunch()`: 关闭所有页面并跳转
* `switchTab()`: 跳转到TabBar页面
* `navigateBack()`: 返回上一页面
### 本地存储
* `storage()`: 设置或获取本地存储
* `removeStorage()`: 移除本地存储项
* `getStorageInfo()`: 获取存储信息
### 其他功能
* `copy()`: 复制文本到剪贴板
* `saveImageToPhotos()`: 保存图片到相册
* `requestPayment()`: 发起微信支付
* `upload()`: 文件上传
* `loadFont()`: 动态加载字体
### 网络请求
* 网络请求使用 `lib/luch-request` 库进行封装。 * 网络请求使用 `lib/luch-request` 库进行封装。
* 全局配置在 `api/request.js` 中定义包括基础URL、请求头、SSL验证等。 * 全局配置在 `api/request.js` 中定义包括基础URL、请求头、SSL验证等。
* 包含请求和响应拦截器,用于处理通用逻辑(如错误提示、鉴权等)。 * 包含请求和响应拦截器,用于处理通用逻辑(如错误提示、鉴权等)。
* 基础URL通过环境变量 `VITE_BASE_URL` 配置。
* 各业务板块的接口都应存放在 `api/modules` 下,并将单个接口进行导出以便页面按需导入。 * 各业务板块的接口都应存放在 `api/modules` 下,并将单个接口进行导出以便页面按需导入。
## 组件 ### 状态管理 (Vuex)
* 项目集成了 `uView-Plus``z-paging` 两个组件库,组件均已全局导入,可以直接使用 * 项目集成了Vuex进行全局状态管理
* `uView-Plus` 组件的使用方式是 `u-icon``u-input``u-textarea` * 状态管理文件位于 `store/index.js`
* 全局组件放在 `components/` 目录下 * 使用 `createStore` 创建store实例
* 页面独立组件放在页面根目录下的 `components/` * 目前处于基础状态包含空的state、mutations、actions和getters
* 每个组件应该附带 `README.MD` 文档。
* 组件编写应遵循项目中已有的风格。
## 页面 ## 组件规范
* 页面配置在 `pages.json` 中管理 * `uni_modules` 目录中的组件无需导入直接可以进行使用
* 主包页面放在 `pages/` 目录下,分包页面放在 `subPages/` 目录下。 * **组件库**: 集成 `uView-Plus` 和 `z-paging`
* 注释、结构规范应遵循项目中已有的风格。 * **自动导入**: `uView-Plus` 通过 `easycom` 自动导入
```javascript
// 直接使用无需import
<u-button>按钮</u-button>
<u-icon name="home" />
```
* **组件分类**:
* **全局组件**: 放在 `components/` 目录
* **页面组件**: 放在页面根目录的 `components/` 目录
* **微信原生组件**: 放在页面根目录的 `wxcomponents/` 目录
## 分页功能
* **分页组件**: 使用 `z-paging` 实现高性能分页
* **使用方式**:
```vue
<template>
<z-paging
ref="paging"
v-model="dataList"
@query="queryList"
>
<view v-for="item in dataList" :key="item.id">
{{ item.name }}
</view>
</z-paging>
</template>
<script setup>
import { ref } from 'vue'
const paging = ref(null)
const dataList = ref([])
// 分页查询函数
const queryList = async (pageNo, pageSize) => {
try {
const result = await api.getList({ pageNo, pageSize })
paging.value.complete(result.data.list)
} catch (error) {
paging.value.complete(false)
}
}
// 刷新数据
const refresh = () => {
paging.value.reload()
}
</script>
```
### 路由配置
* **主包页面**: 放在 `pages/` 目录
* **分包页面**: 放在 `subPages/` 目录
* **TabBar配置**: 在 `pages.json` 中配置底部导航
### Vite 配置
* `vite.config.js` 包含了自定义插件用于在编译时替换 `manifest.json` 中的 appid
* 仅在首次编译时执行,避免热重载时重复执行导致内存占用高
* 支持环境变量配置:
* `VITE_APPID` - 微信小程序APPID
* `VITE_UNI_APPID` - uni-app APPID
* `VITE_LIBVERSION` - 微信小程序基础库版本
### 环境配置
* 项目使用环境变量管理不同环境的配置,通过 `.env` 文件配置:
* `VITE_BASE_URL` - 接口地址
* `VITE_ASSETSURL` - 资源地址
* `VITE_APPID` - 小程序APPID
* `VITE_UNI_APPID` - UNI-APPID
* `VITE_LIBVERSION` - 微信小程序基础库版本
### 全局配置
* `main.js` 中配置了 `uni.$zp` 全局配置:
* `'empty-view-text'` - 空数据提示文字
* `'refresher-enabled'` - 是否启用下拉刷新
## 代码提交规范
### 提交信息格式
* **新增功能**: `新增 功能描述`
* **错误修复**: `修复 问题描述`
* **性能优化**: `优化 优化内容`
* **文档更新**: `文档 更新内容`
### 示例
```bash
git commit -m "新增 推广海报生成功能"
git commit -m "修复 购物车数量计算错误"
git commit -m "优化 接口请求性能"
```
### 其他
* 项目中的分享功能应该使用微信原生的分享功能,通过 `button` 或 `u-button` 组件的 `open-type="share"` 属性实现。
* 项目中的头像编辑和获取功能应该使用微信原生获取头像昵称的开放能力,通过 `button` 或 `u-button` 组件的 `open-type="chooseAvatar"` 属性和其触发的 `@chooseavatar` 事件实现。
* 项目中的昵称编辑和获取功能应该使用微信原生获取头像昵称的开放能力,通过 `input` 或 `u-input` 组件的 `open-type="nickname"` 属性实现。
* 项目集成了promise适配器 `uni.promisify.adaptor.js`,用于统一处理异步操作。
## 最佳实践
### 页面开发
* 使用Composition API (setup语法糖)
* 遵循Vue3响应式原理
* 合理使用computed和watch
### 组件开发
* 保持组件单一职责
* 使用props进行父子通信
* 合理使用emits进行事件派发
### 状态管理
* 合理划分store模块
* 避免过度依赖全局状态
* 及时清理不需要的状态
### 性能优化
* 使用z-paging进行列表优化
* 合理使用图片懒加载
* 避免不必要的重复渲染

View File

@@ -5,24 +5,34 @@
## Ŀ¼<C4BF> ## Ŀ¼<C4BF>
``` ```
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>api/ # <20>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> api/ # <20>ӿ<EFBFBD><D3BF><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>modules/ # ģ<EFBFBD>黯 API <20><><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> modules/ # ҵ<EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>request.js # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> request.js # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>common/ # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common/ # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>styles/ # ȫ<><C8AB><EFBFBD><EFBFBD>ʽ <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> styles/ # ȫ<><C8AB><EFBFBD><EFBFBD>ʽ
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD>utils/ # <EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common.css # codefunԭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>components/ # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> base.scss # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>uview-plus/ # uView-Plus <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> utils/ # <20><><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>lib/ # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> tool.ts # <20><><EFBFBD>ù<EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>mixins/ # Vue <EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> components/ # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>pages/ # <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni_modules/ # uni-app <20><><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>subPages/ # <20>ְ<EFBFBD>ҳ<EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> z-paging/ # <20><>ҳ<EFBFBD><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>wxcomponents/ # ΢<><CEA2>ԭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> lib/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>App.vue # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> luch-request/ # luch-request <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>main.js # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uview-plus/ # uView-Plus <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>pages.json # ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> mixins/ # Vue <EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>manifest.json # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> global.ts # ȫ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>uni.scss # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> store/ # ״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> index.ts # Vuex store
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages/ # <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> subPages/ # <20>ְ<EFBFBD>ҳ<EFBFBD><D2B3>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> App.vue # Ӧ<><D3A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> main.js # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages.json # ҳ<><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> manifest.json # Ӧ<><D3A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni.scss # ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vite.config.js # Vite <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .nvmdrc # Node.js <20>汾Ҫ<E6B1BE><D2AA>
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .env # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .env # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
``` ```
@@ -42,7 +52,7 @@
### <20><>ʽ ### <20><>ʽ
- common.css: ȫ<>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD>ʽ - common.css: ȫ<>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD>ʽ
- normal.scss: <20><><EFBFBD>õ<EFBFBD> SCSS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - base.scss: <20><><EFBFBD>õ<EFBFBD> SCSS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
### <20><><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD> (tool.js) ### <20><><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD> (tool.js)

View File

@@ -1,5 +1,6 @@
import Request from '@/lib/luch-request/index.js' import Request from '@/lib/luch-request/index.js'
import tool from '@/common/utils/tool.js' import tool from '@/common/utils/tool.js'
import { reportError } from 'uniapp-error-monitor'
const baseUrl = import.meta.env.VITE_BASE_URL const baseUrl = import.meta.env.VITE_BASE_URL
const http = new Request() const http = new Request()
@@ -31,6 +32,10 @@ http.interceptors.response.use(response => {
if (response.statusCode == 401 || response.data.code == 401) { if (response.statusCode == 401 || response.data.code == 401) {
} }
if (response.data.code !== 1 || response.data.code !== 200) {
reportError('api', response)
}
if (response.statusCode == 200) { if (response.statusCode == 200) {
return Promise.resolve(response) return Promise.resolve(response)
} else { } else {

View File

@@ -1,5 +1,5 @@
// 定义内外边距历遍1-40 // 定义内外边距历遍1-100
@for $i from 0 through 40 { @for $i from 0 through 100 {
// 只要双数和能被5除尽的数 // 只要双数和能被5除尽的数
@if $i % 2==0 or $i % 5==0 { @if $i % 2==0 or $i % 5==0 {
// 得出u-margin-30或者u-m-30 // 得出u-margin-30或者u-m-30

13
main.js
View File

@@ -1,8 +1,10 @@
import App from './App' import App from './App'
import uviewPlus from '/uview-plus' import uviewPlus from '/uview-plus'
import globalMixin from './mixins/global' import globalMixin from './mixins/global'
import store from './store'
import { createSSRApp } from 'vue' import { createSSRApp } from 'vue'
import './uni.promisify.adaptor' import './uni.promisify.adaptor'
import { initErrorMonitor } from 'uniapp-error-monitor'
uni.$zp = { uni.$zp = {
config: { config: {
@@ -15,5 +17,16 @@ export function createApp() {
const app = createSSRApp(App) const app = createSSRApp(App)
app.use(uviewPlus) app.use(uviewPlus)
app.use(globalMixin) app.use(globalMixin)
app.use(store)
// 初始化错误监控
initErrorMonitor({
webhookUrl: import.meta.env.VITE_WEBHOOK, // 必填
enableGlobalError: true, // 启用全局错误捕获
enablePromiseError: true, // 启用 Promise 错误捕获
enableConsoleError: false, // 禁用 console.error 捕获
// forceEnable: true,
})
return { app } return { app }
} }

View File

@@ -1,25 +1,26 @@
{ {
"name" : "", "name": "template",
"appid" : "", "appid": "_UNI_8842336",
"description" : "", "description": "",
"versionName" : "1.0.0", "versionName": "1.0.0",
"versionCode" : "100", "versionCode": "100",
"vueVersion" : "3", "vueVersion": "3",
"uniStatistics" : { "uniStatistics": {
"enable" : false "enable": false
}, },
"mp-weixin" : { "mp-weixin": {
"appid" : "", "appid": "wx9cb717d8151d8486",
"setting" : { "setting": {
"urlCheck" : false, "urlCheck": false,
"es6" : true, "es6": true,
"postcss" : true, "postcss": true,
"minified" : true "minified": true
}, },
"lazyCodeLoading" : "requiredComponents", "lazyCodeLoading": "requiredComponents",
"usingComponents" : true, "usingComponents": true,
"optimization" : { "optimization": {
"subPackages" : true "subPackages": true
} },
"libVersion": "3.0.0"
} }
} }

318
package-lock.json generated Normal file
View File

@@ -0,0 +1,318 @@
{
"name": "template-mp",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"dayjs": "*",
"dotenv": "^17.2.2",
"uniapp-error-monitor": "^1.0.0",
"vue": "^3.5.21"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"license": "MIT"
},
"node_modules/@vue/compiler-core": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz",
"integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/shared": "3.5.25",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz",
"integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz",
"integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@vue/compiler-core": "3.5.25",
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-ssr": "3.5.25",
"@vue/shared": "3.5.25",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz",
"integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz",
"integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz",
"integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/shared": "3.5.25"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz",
"integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.25",
"@vue/runtime-core": "3.5.25",
"@vue/shared": "3.5.25",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz",
"integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.25",
"@vue/shared": "3.5.25"
},
"peerDependencies": {
"vue": "3.5.25"
}
},
"node_modules/@vue/shared": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz",
"integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT"
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/uniapp-error-monitor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/uniapp-error-monitor/-/uniapp-error-monitor-1.0.0.tgz",
"integrity": "sha512-B2dyJV8Sx9W6k/J1vg/dKf7Tsbzs50tGrVz/Qm+ruMbZ/3u0oIzvjgrnF96hdlJx21JUgWt5cigfOaTyx7qfmA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
},
"peerDependencies": {
"@dcloudio/uni-app": "^3.0.0"
},
"peerDependenciesMeta": {
"@dcloudio/uni-app": {
"optional": true
}
}
},
"node_modules/vue": {
"version": "3.5.25",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",
"@vue/runtime-dom": "3.5.25",
"@vue/server-renderer": "3.5.25",
"@vue/shared": "3.5.25"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
}
}

View File

@@ -1,7 +1,8 @@
{ {
"dependencies": { "dependencies": {
"dotenv": "^17.2.2",
"dayjs": "*", "dayjs": "*",
"dotenv": "^17.2.2",
"uniapp-error-monitor": "*",
"vue": "^3.5.21" "vue": "^3.5.21"
} }
} }

View File

@@ -7,7 +7,11 @@
"subPackages": [ "subPackages": [
{ {
"root": "subPages", "root": "subPages",
"pages": [] "pages": [
{
"path": "welcome/index"
}
]
} }
], ],
"easycom": { "easycom": {

View File

@@ -3,7 +3,6 @@
</template> </template>
<script setup> <script setup>
</script> </script>
<style> <style>

BIN
static/assets/icon_home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,13 @@
<template>
<div>
</div>
</template>
<script setup>
</script>
<style lang="scss" scoped>
</style>

View File

@@ -74,3 +74,6 @@ $uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px; $uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色 $uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px; $uni-font-size-paragraph:15px;
/* 全局资源URL变量 */
$ASSETSURL:'https://cdn.vrupup.com/s/1732/assets/';

View File

@@ -3,9 +3,6 @@
"version": "3.2.5", "version": "3.2.5",
"description": "types for uview-plus", "description": "types for uview-plus",
"main": "index.d.ts", "main": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/ijry/uview-plus.git" "url": "git+https://github.com/ijry/uview-plus.git"

View File

@@ -1,9 +1,75 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni' import uni from '@dcloudio/vite-plugin-uni'
import { configDotenv } from 'dotenv' import { resolve } from 'path'
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import dotenv from 'dotenv'
// 自定义插件:替换 manifest.json 中的 appid
// 仅在通过HBuilder首次编译时执行避免热重载时重复执行导致内存占用高
function replaceManifestAppid() {
return {
name: 'replace-manifest-appid',
buildStart() {
// 检查是否通过HBuilder编译
// 检查是否已经执行过插件
const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated')
const isFirstCompile = !existsSync(manifestUpdatedFlag)
// 仅首次编译时执行
if (!isFirstCompile) {
console.log('跳过 manifest appid 更新(已执行过首次编译)')
return
}
// 获取环境变量,明确指定路径为项目根目录
dotenv.config({ path: resolve(__dirname, '.env') })
const appid = process.env.VITE_APPID
const uni_appId = process.env.VITE_UNI_APPID
const libVersion = process.env.VITE_LIBVERSION || '3.0.0'
if (appid && uni_appId) {
// 读取 manifest.json 文件
const manifestPath = resolve(__dirname, 'manifest.json')
let manifestContent = readFileSync(manifestPath, 'utf-8')
// 解析 JSON
const manifest = JSON.parse(manifestContent)
// 替换
manifest.appid = uni_appId
if (manifest['mp-weixin']) {
manifest['mp-weixin'].appid = appid
manifest['mp-weixin'].libVersion = libVersion
}
// 写回文件
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4))
// 创建标记文件,表示已执行过插件
writeFileSync(manifestUpdatedFlag, 'Manifest updated by HBuilder first compile')
console.log(`Manifest appid 已更新为: ${uni_appId}`)
console.log(`Manifest mp-weixin appid 已更新为: ${appid}`)
} else {
console.warn('未找到 VITE_APPID 和 VITE_UNI_APPID 环境变量,使用默认值')
}
},
}
}
// 检查是否通过HBuilder编译决定是否启用插件
const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated')
if (existsSync(manifestUpdatedFlag)) {
unlinkSync(manifestUpdatedFlag)
console.log('已清理 manifest 更新标记文件')
}
const plugins = manifestUpdatedFlag ? [replaceManifestAppid(), uni()] : [uni()]
export default defineConfig({ export default defineConfig({
plugins: [uni(), configDotenv()], plugins,
resolve: {
alias: {
'@': '/src',
},
},
build: { build: {
minify: 'terser', minify: 'terser',
terserOptions: { terserOptions: {

View File

@@ -0,0 +1,54 @@
/* eslint-disable */
// 四则运算
!(function () {
var calculate = function (s) {
s = s.trim();
const stack = new Array();
let preSign = '+';
let numStr = '';
const n = s.length;
for (let i = 0; i < n; ++i) {
if (s[i] === '.' || (!isNaN(Number(s[i])) && s[i] !== ' ')) {
numStr += s[i];
} else if (s[i] === '(') {
let isClose = 1;
let j = i;
while (isClose > 0) {
j += 1;
if (s[j] === '(') isClose += 1;
if (s[j] === ')') isClose -= 1;
}
numStr = `${calculate(s.slice(i + 1, j))}`;
i = j;
}
if ((isNaN(Number(s[i])) && s[i] !== '.') || i === n - 1) {
let num = parseFloat(numStr);
switch (preSign) {
case '+':
stack.push(num);
break;
case '-':
stack.push(-num);
break;
case '*':
stack.push(stack.pop() * num);
break;
case '/':
stack.push(stack.pop() / num);
break;
default:
break;
}
preSign = s[i];
numStr = '';
}
}
let ans = 0;
while (stack.length) {
ans += stack.pop();
}
return ans;
};
module.exports = calculate;
})();

View File

@@ -0,0 +1,361 @@
/**
* LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用
* 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3
*/
const util = require('./util');
const sha1 = require('./sha1');
const SAVED_FILES_KEY = 'savedFiles';
const KEY_TOTAL_SIZE = 'totalSize';
const KEY_PATH = 'path';
const KEY_TIME = 'time';
const KEY_SIZE = 'size';
// 可存储总共为 6M目前小程序可允许的最大本地存储为 10M
let MAX_SPACE_IN_B = 6 * 1024 * 1024;
let savedFiles = {};
export default class Dowloader {
constructor() {
// app 如果设置了最大存储空间,则使用 app 中的
if (getApp().PAINTER_MAX_LRU_SPACE) {
MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE;
}
wx.getStorage({
key: SAVED_FILES_KEY,
success: function (res) {
if (res.data) {
savedFiles = res.data;
}
},
});
}
/**
* 下载文件,会用 lru 方式来缓存文件到本地
* @param {String} url 文件的 url
*/
download(url, lru) {
return new Promise((resolve, reject) => {
if (!(url && util.isValidUrl(url))) {
resolve(url);
return;
}
const fileName = getFileName(url);
if (!lru) {
// 无 lru 情况下直接判断 临时文件是否存在,不存在重新下载
wx.getFileInfo({
filePath: fileName,
success: () => {
resolve(url);
},
fail: () => {
if (util.isOnlineUrl(url)) {
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
} else if (util.isDataUrl(url)) {
transformBase64File(url, lru).then(path => {
resolve(path);
}, () => {
reject();
});
}
},
})
return
}
const file = getFile(fileName);
if (file) {
if (file[KEY_PATH].indexOf('//usr/') !== -1) {
wx.getFileInfo({
filePath: file[KEY_PATH],
success() {
resolve(file[KEY_PATH]);
},
fail(error) {
console.error(`base64 file broken, ${JSON.stringify(error)}`);
transformBase64File(url, lru).then(path => {
resolve(path);
}, () => {
reject();
});
}
})
} else {
// 检查文件是否正常,不正常需要重新下载
wx.getSavedFileInfo({
filePath: file[KEY_PATH],
success: (res) => {
resolve(file[KEY_PATH]);
},
fail: (error) => {
console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`);
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
},
});
}
} else {
if (util.isOnlineUrl(url)) {
downloadFile(url, lru).then((path) => {
resolve(path);
}, () => {
reject();
});
} else if (util.isDataUrl(url)) {
transformBase64File(url, lru).then(path => {
resolve(path);
}, () => {
reject();
});
}
}
});
}
}
function getFileName(url) {
if (util.isDataUrl(url)) {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(url) || [];
const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
return fileName;
} else {
return url;
}
}
function transformBase64File(base64data, lru) {
return new Promise((resolve, reject) => {
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
if (!format) {
console.error('base parse failed');
reject();
return;
}
const fileName = `${sha1.hex_sha1(bodyData)}.${format}`;
const path = `${wx.env.USER_DATA_PATH}/${fileName}`;
const buffer = wx.base64ToArrayBuffer(bodyData.replace(/[\r\n]/g, ""));
wx.getFileSystemManager().writeFile({
filePath: path,
data: buffer,
encoding: 'binary',
success() {
wx.getFileInfo({
filePath: path,
success: (tmpRes) => {
const newFileSize = tmpRes.size;
lru ? doLru(newFileSize).then(() => {
saveFile(fileName, newFileSize, path, true).then((filePath) => {
resolve(filePath);
});
}, () => {
resolve(path);
}) : resolve(path);
},
fail: (error) => {
// 文件大小信息获取失败,则此文件也不要进行存储
console.error(`getFileInfo ${path} failed, ${JSON.stringify(error)}`);
resolve(path);
},
});
},
fail(err) {
console.log(err)
}
})
});
}
function downloadFile(url, lru) {
return new Promise((resolve, reject) => {
wx.downloadFile({
url: url,
success: function (res) {
if (res.statusCode !== 200) {
console.error(`downloadFile ${url} failed res.statusCode is not 200`);
reject();
return;
}
const {
tempFilePath
} = res;
wx.getFileInfo({
filePath: tempFilePath,
success: (tmpRes) => {
const newFileSize = tmpRes.size;
lru ? doLru(newFileSize).then(() => {
saveFile(url, newFileSize, tempFilePath).then((filePath) => {
resolve(filePath);
});
}, () => {
resolve(tempFilePath);
}) : resolve(tempFilePath);
},
fail: (error) => {
// 文件大小信息获取失败,则此文件也不要进行存储
console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`);
resolve(res.tempFilePath);
},
});
},
fail: function (error) {
console.error(`downloadFile failed, ${JSON.stringify(error)} `);
reject();
},
});
});
}
function saveFile(key, newFileSize, tempFilePath, isDataUrl = false) {
return new Promise((resolve, reject) => {
if (isDataUrl) {
const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
savedFiles[key] = {};
savedFiles[key][KEY_PATH] = tempFilePath;
savedFiles[key][KEY_TIME] = new Date().getTime();
savedFiles[key][KEY_SIZE] = newFileSize;
savedFiles['totalSize'] = newFileSize + totalSize;
wx.setStorage({
key: SAVED_FILES_KEY,
data: savedFiles,
});
resolve(tempFilePath);
return;
}
wx.saveFile({
tempFilePath: tempFilePath,
success: (fileRes) => {
const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
savedFiles[key] = {};
savedFiles[key][KEY_PATH] = fileRes.savedFilePath;
savedFiles[key][KEY_TIME] = new Date().getTime();
savedFiles[key][KEY_SIZE] = newFileSize;
savedFiles['totalSize'] = newFileSize + totalSize;
wx.setStorage({
key: SAVED_FILES_KEY,
data: savedFiles,
});
resolve(fileRes.savedFilePath);
},
fail: (error) => {
console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`);
// 由于 saveFile 成功后res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件
resolve(tempFilePath);
// 如果出现错误就直接情况本地的所有文件因为你不知道是不是因为哪次lru的某个文件未删除成功
reset();
},
});
});
}
/**
* 清空所有下载相关内容
*/
function reset() {
wx.removeStorage({
key: SAVED_FILES_KEY,
success: () => {
wx.getSavedFileList({
success: (listRes) => {
removeFiles(listRes.fileList);
},
fail: (getError) => {
console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`);
},
});
},
});
}
function doLru(size) {
if (size > MAX_SPACE_IN_B) {
return Promise.reject()
}
return new Promise((resolve, reject) => {
let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0;
if (size + totalSize <= MAX_SPACE_IN_B) {
resolve();
return;
}
// 如果加上新文件后大小超过最大限制,则进行 lru
const pathsShouldDelete = [];
// 按照最后一次的访问时间,从小到大排序
const allFiles = JSON.parse(JSON.stringify(savedFiles));
delete allFiles[KEY_TOTAL_SIZE];
const sortedKeys = Object.keys(allFiles).sort((a, b) => {
return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME];
});
for (const sortedKey of sortedKeys) {
totalSize -= savedFiles[sortedKey].size;
pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]);
delete savedFiles[sortedKey];
if (totalSize + size < MAX_SPACE_IN_B) {
break;
}
}
savedFiles['totalSize'] = totalSize;
wx.setStorage({
key: SAVED_FILES_KEY,
data: savedFiles,
success: () => {
// 保证 storage 中不会存在不存在的文件数据
if (pathsShouldDelete.length > 0) {
removeFiles(pathsShouldDelete);
}
resolve();
},
fail: (error) => {
console.error(`doLru setStorage failed, ${JSON.stringify(error)}`);
reject();
},
});
});
}
function removeFiles(pathsShouldDelete) {
for (const pathDel of pathsShouldDelete) {
let delPath = pathDel;
if (typeof pathDel === 'object') {
delPath = pathDel.filePath;
}
if (delPath.indexOf('//usr/') !== -1) {
wx.getFileSystemManager().unlink({
filePath: delPath,
fail(error) {
console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
}
})
} else {
wx.removeSavedFile({
filePath: delPath,
fail: (error) => {
console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`);
},
});
}
}
}
function getFile(key) {
if (!savedFiles[key]) {
return;
}
savedFiles[key]['time'] = new Date().getTime();
wx.setStorage({
key: SAVED_FILES_KEY,
data: savedFiles,
});
return savedFiles[key];
}

View File

@@ -0,0 +1,102 @@
/* eslint-disable */
// 当ctx传入当前文件const grd = ctx.createCircularGradient() 和
// const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理
// 先分析在外部创建grd再传入使用就可以
!(function () {
var api = {
isGradient: function(bg) {
if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) {
return true;
}
return false;
},
doGradient: function(bg, width, height, ctx) {
if (bg.startsWith('linear')) {
linearEffect(width, height, bg, ctx);
} else if (bg.startsWith('radial')) {
radialEffect(width, height, bg, ctx);
}
},
}
function analizeGrad(string) {
const colorPercents = string.substring(0, string.length - 1).split("%,");
const colors = [];
const percents = [];
for (let colorPercent of colorPercents) {
colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(" ")).trim());
percents.push(colorPercent.substring(colorPercent.lastIndexOf(" "), colorPercent.length) / 100);
}
return {colors: colors, percents: percents};
}
function radialEffect(width, height, bg, ctx) {
const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]);
const grd = ctx.createRadialGradient(0, 0, 0, 0, 0, width < height ? height / 2 : width / 2);
for (let i = 0; i < colorPer.colors.length; i++) {
grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
}
ctx.fillStyle = grd;
//ctx.fillRect(-(width / 2), -(height / 2), width, height);
}
function analizeLinear(bg, width, height) {
const direction = bg.match(/([-]?\d{1,3})deg/);
const dir = direction && direction[1] ? parseFloat(direction[1]) : 0;
let coordinate;
switch (dir) {
case 0: coordinate = [0, -height / 2, 0, height / 2]; break;
case 90: coordinate = [width / 2, 0, -width / 2, 0]; break;
case -90: coordinate = [-width / 2, 0, width / 2, 0]; break;
case 180: coordinate = [0, height / 2, 0, -height / 2]; break;
case -180: coordinate = [0, -height / 2, 0, height / 2]; break;
default:
let x1 = 0;
let y1 = 0;
let x2 = 0;
let y2 = 0;
if (direction[1] > 0 && direction[1] < 90) {
x1 = (width / 2) - ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
x2 = -x1;
y1 = -y2;
} else if (direction[1] > -180 && direction[1] < -90) {
x1 = -(width / 2) + ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
x2 = -x1;
y1 = -y2;
} else if (direction[1] > 90 && direction[1] < 180) {
x1 = (width / 2) + (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
x2 = -x1;
y1 = -y2;
} else {
x1 = -(width / 2) - (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2;
y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1;
x2 = -x1;
y1 = -y2;
}
coordinate = [x1, y1, x2, y2];
break;
}
return coordinate;
}
function linearEffect(width, height, bg, ctx) {
const param = analizeLinear(bg, width, height);
const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]);
const content = bg.match(/linear-gradient\((.+)\)/)[1];
const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1));
for (let i = 0; i < colorPer.colors.length; i++) {
grd.addColorStop(colorPer.percents[i], colorPer.colors[i]);
}
ctx.fillStyle = grd
//ctx.fillRect(-(width / 2), -(height / 2), width, height);
}
module.exports = { api }
})();

View File

@@ -0,0 +1,734 @@
const QR = require('./qrcode.js');
const GD = require('./gradient.js');
export const penCache = {
// 用于存储带 id 的 view 的 rect 信息
viewRect: {},
textLines: {},
};
export const clearPenCache = id => {
if (id) {
penCache.viewRect[id] = null;
penCache.textLines[id] = null;
} else {
penCache.viewRect = {};
penCache.textLines = {};
}
};
export default class Painter {
constructor(ctx, data) {
this.ctx = ctx;
this.data = data;
}
paint(callback) {
this.style = {
width: this.data.width.toPx(),
height: this.data.height.toPx(),
};
this._background();
for (const view of this.data.views) {
this._drawAbsolute(view);
}
this.ctx.draw(false, () => {
callback && callback();
});
}
_background() {
this.ctx.save();
const { width, height } = this.style;
const bg = this.data.background;
this.ctx.translate(width / 2, height / 2);
this._doClip(this.data.borderRadius, width, height);
if (!bg) {
// 如果未设置背景,则默认使用透明色
this.ctx.fillStyle = 'transparent';
this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
} else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') {
// 背景填充颜色
this.ctx.fillStyle = bg;
this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
} else if (GD.api.isGradient(bg)) {
GD.api.doGradient(bg, width, height, this.ctx);
this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
} else {
// 背景填充图片
this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height);
}
this.ctx.restore();
}
_drawAbsolute(view) {
if (!(view && view.type)) {
// 过滤无效 view
return;
}
// 证明 css 为数组形式,需要合并
if (view.css && view.css.length) {
/* eslint-disable no-param-reassign */
view.css = Object.assign(...view.css);
}
switch (view.type) {
case 'image':
this._drawAbsImage(view);
break;
case 'text':
this._fillAbsText(view);
break;
case 'rect':
this._drawAbsRect(view);
break;
case 'qrcode':
this._drawQRCode(view);
break;
default:
break;
}
}
_border({ borderRadius = 0, width, height, borderWidth = 0, borderStyle = 'solid' }) {
let r1 = 0,
r2 = 0,
r3 = 0,
r4 = 0;
const minSize = Math.min(width, height);
if (borderRadius) {
const border = borderRadius.split(/\s+/);
if (border.length === 4) {
r1 = Math.min(border[0].toPx(false, minSize), width / 2, height / 2);
r2 = Math.min(border[1].toPx(false, minSize), width / 2, height / 2);
r3 = Math.min(border[2].toPx(false, minSize), width / 2, height / 2);
r4 = Math.min(border[3].toPx(false, minSize), width / 2, height / 2);
} else {
r1 = r2 = r3 = r4 = Math.min(borderRadius && borderRadius.toPx(false, minSize), width / 2, height / 2);
}
}
const lineWidth = borderWidth && borderWidth.toPx(false, minSize);
this.ctx.lineWidth = lineWidth;
if (borderStyle === 'dashed') {
this.ctx.setLineDash([(lineWidth * 4) / 3, (lineWidth * 4) / 3]);
// this.ctx.lineDashOffset = 2 * lineWidth
} else if (borderStyle === 'dotted') {
this.ctx.setLineDash([lineWidth, lineWidth]);
}
const notSolid = borderStyle !== 'solid';
this.ctx.beginPath();
notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
r1 !== 0 && this.ctx.arc(-width / 2 + r1, -height / 2 + r1, r1 + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI); //左上角圆弧
this.ctx.lineTo(
r2 === 0 ? (notSolid ? width / 2 : width / 2 + lineWidth / 2) : width / 2 - r2,
-height / 2 - lineWidth / 2,
); // 顶边线
notSolid && r2 === 0 && this.ctx.moveTo(width / 2 + lineWidth / 2, -height / 2 - lineWidth); // 右边虚线规避重叠规则
r2 !== 0 && this.ctx.arc(width / 2 - r2, -height / 2 + r2, r2 + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI); // 右上角圆弧
this.ctx.lineTo(
width / 2 + lineWidth / 2,
r3 === 0 ? (notSolid ? height / 2 : height / 2 + lineWidth / 2) : height / 2 - r3,
); // 右边线
notSolid && r3 === 0 && this.ctx.moveTo(width / 2 + lineWidth, height / 2 + lineWidth / 2); // 底边虚线规避重叠规则
r3 !== 0 && this.ctx.arc(width / 2 - r3, height / 2 - r3, r3 + lineWidth / 2, 0, 0.5 * Math.PI); // 右下角圆弧
this.ctx.lineTo(
r4 === 0 ? (notSolid ? -width / 2 : -width / 2 - lineWidth / 2) : -width / 2 + r4,
height / 2 + lineWidth / 2,
); // 底边线
notSolid && r4 === 0 && this.ctx.moveTo(-width / 2 - lineWidth / 2, height / 2 + lineWidth); // 左边虚线规避重叠规则
r4 !== 0 && this.ctx.arc(-width / 2 + r4, height / 2 - r4, r4 + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI); // 左下角圆弧
this.ctx.lineTo(
-width / 2 - lineWidth / 2,
r1 === 0 ? (notSolid ? -height / 2 : -height / 2 - lineWidth / 2) : -height / 2 + r1,
); // 左边线
notSolid && r1 === 0 && this.ctx.moveTo(-width / 2 - lineWidth, -height / 2 - lineWidth / 2); // 顶边虚线规避重叠规则
if (!notSolid) {
this.ctx.closePath();
}
}
/**
* 根据 borderRadius 进行裁减
*/
_doClip(borderRadius, width, height, borderStyle) {
if (borderRadius && width && height) {
// 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会
// globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white相对默认的 black 要好点
this.ctx.globalAlpha = 0;
this.ctx.fillStyle = 'white';
this._border({
borderRadius,
width,
height,
borderStyle,
});
this.ctx.fill();
// 在 ios 的 6.6.6 版本上 clip 有 bug禁掉此类型上的 clip也就意味着在此版本微信的 ios 设备下无法使用 border 属性
if (!(getApp().systemInfo && getApp().systemInfo.version <= '6.6.6' && getApp().systemInfo.platform === 'ios')) {
this.ctx.clip();
}
this.ctx.globalAlpha = 1;
}
}
/**
* 画边框
*/
_doBorder(view, width, height) {
if (!view.css) {
return;
}
const { borderRadius, borderWidth, borderColor, borderStyle } = view.css;
if (!borderWidth) {
return;
}
this.ctx.save();
this._preProcess(view, true);
this.ctx.strokeStyle = borderColor || 'black';
this._border({
borderRadius,
width,
height,
borderWidth,
borderStyle,
});
this.ctx.stroke();
this.ctx.restore();
}
_preProcess(view, notClip) {
let width = 0;
let height;
let extra;
const paddings = this._doPaddings(view);
switch (view.type) {
case 'text': {
const textArray = String(view.text).split('\n');
// 处理多个连续的'\n'
for (let i = 0; i < textArray.length; ++i) {
if (textArray[i] === '') {
textArray[i] = ' ';
}
}
const fontWeight = view.css.fontWeight || '400';
const textStyle = view.css.textStyle || 'normal';
if (!view.css.fontSize) {
view.css.fontSize = '20rpx';
}
this.ctx.font = `${textStyle} ${fontWeight} ${view.css.fontSize.toPx()}px "${
view.css.fontFamily || 'sans-serif'
}"`;
// 计算行数
let lines = 0;
const linesArray = [];
for (let i = 0; i < textArray.length; ++i) {
const textLength = this.ctx.measureText(textArray[i]).width;
const minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
let partWidth = view.css.width
? view.css.width.toPx(false, this.style.width) - paddings[1] - paddings[3]
: textLength;
if (partWidth < minWidth) {
partWidth = minWidth;
}
const calLines = Math.ceil(textLength / partWidth);
// 取最长的作为 width
width = partWidth > width ? partWidth : width;
lines += calLines;
linesArray[i] = calLines;
}
lines = view.css.maxLines < lines ? view.css.maxLines : lines;
const lineHeight = view.css.lineHeight ? view.css.lineHeight.toPx() : view.css.fontSize.toPx();
height = lineHeight * lines;
extra = {
lines: lines,
lineHeight: lineHeight,
textArray: textArray,
linesArray: linesArray,
};
break;
}
case 'image': {
// image的长宽设置成auto的逻辑处理
const ratio = getApp().systemInfo.pixelRatio ? getApp().systemInfo.pixelRatio : 2;
// 有css却未设置width或height则默认为auto
if (view.css) {
if (!view.css.width) {
view.css.width = 'auto';
}
if (!view.css.height) {
view.css.height = 'auto';
}
}
if (!view.css || (view.css.width === 'auto' && view.css.height === 'auto')) {
width = Math.round(view.sWidth / ratio);
height = Math.round(view.sHeight / ratio);
} else if (view.css.width === 'auto') {
height = view.css.height.toPx(false, this.style.height);
width = (view.sWidth / view.sHeight) * height;
} else if (view.css.height === 'auto') {
width = view.css.width.toPx(false, this.style.width);
height = (view.sHeight / view.sWidth) * width;
} else {
width = view.css.width.toPx(false, this.style.width);
height = view.css.height.toPx(false, this.style.height);
}
break;
}
default:
if (!(view.css.width && view.css.height)) {
console.error('You should set width and height');
return;
}
width = view.css.width.toPx(false, this.style.width);
height = view.css.height.toPx(false, this.style.height);
break;
}
let x;
if (view.css && view.css.right) {
if (typeof view.css.right === 'string') {
x = this.style.width - view.css.right.toPx(true, this.style.width);
} else {
// 可以用数组方式,把文字长度计算进去
// [right, 文字id, 乘数(默认 1]
const rights = view.css.right;
x =
this.style.width -
rights[0].toPx(true, this.style.width) -
penCache.viewRect[rights[1]].width * (rights[2] || 1);
}
} else if (view.css && view.css.left) {
if (typeof view.css.left === 'string') {
x = view.css.left.toPx(true, this.style.width);
} else {
const lefts = view.css.left;
x = lefts[0].toPx(true, this.style.width) + penCache.viewRect[lefts[1]].width * (lefts[2] || 1);
}
} else {
x = 0;
}
//const y = view.css && view.css.bottom ? this.style.height - height - view.css.bottom.toPx(true) : (view.css && view.css.top ? view.css.top.toPx(true) : 0);
let y;
if (view.css && view.css.bottom) {
y = this.style.height - height - view.css.bottom.toPx(true, this.style.height);
} else {
if (view.css && view.css.top) {
if (typeof view.css.top === 'string') {
y = view.css.top.toPx(true, this.style.height);
} else {
const tops = view.css.top;
y = tops[0].toPx(true, this.style.height) + penCache.viewRect[tops[1]].height * (tops[2] || 1);
}
} else {
y = 0;
}
}
const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0;
// 当设置了 right 时,默认 align 用 right反之用 left
const align = view.css && view.css.align ? view.css.align : view.css && view.css.right ? 'right' : 'left';
const verticalAlign = view.css && view.css.verticalAlign ? view.css.verticalAlign : 'top';
// 记录绘制时的画布
let xa = 0;
switch (align) {
case 'center':
xa = x;
break;
case 'right':
xa = x - width / 2;
break;
default:
xa = x + width / 2;
break;
}
let ya = 0;
switch (verticalAlign) {
case 'center':
ya = y;
break;
case 'bottom':
ya = y - height / 2;
break;
default:
ya = y + height / 2;
break;
}
this.ctx.translate(xa, ya);
// 记录该 view 的有效点击区域
// TODO ,旋转和裁剪的判断
// 记录在真实画布上的左侧
let left = x;
if (align === 'center') {
left = x - width / 2;
} else if (align === 'right') {
left = x - width;
}
var top = y;
if (verticalAlign === 'center') {
top = y - height / 2;
} else if (verticalAlign === 'bottom') {
top = y - height;
}
if (view.rect) {
view.rect.left = left;
view.rect.top = top;
view.rect.right = left + width;
view.rect.bottom = top + height;
view.rect.x = view.css && view.css.right ? x - width : x;
view.rect.y = y;
} else {
view.rect = {
left: left,
top: top,
right: left + width,
bottom: top + height,
x: view.css && view.css.right ? x - width : x,
y: y,
};
}
view.rect.left = view.rect.left - paddings[3];
view.rect.top = view.rect.top - paddings[0];
view.rect.right = view.rect.right + paddings[1];
view.rect.bottom = view.rect.bottom + paddings[2];
if (view.type === 'text') {
view.rect.minWidth = view.css.fontSize.toPx() + paddings[1] + paddings[3];
}
this.ctx.rotate(angle);
if (!notClip && view.css && view.css.borderRadius && view.type !== 'rect') {
this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
}
this._doShadow(view);
if (view.id) {
penCache.viewRect[view.id] = {
width,
height,
left: x,
top: y,
right: x + width,
bottom: y + height,
};
}
return {
width: width,
height: height,
x: x,
y: y,
extra: extra,
};
}
_doPaddings(view) {
const { padding } = view.css ? view.css : {};
let pd = [0, 0, 0, 0];
if (padding) {
const pdg = padding.split(/\s+/);
if (pdg.length === 1) {
const x = pdg[0].toPx();
pd = [x, x, x, x];
}
if (pdg.length === 2) {
const x = pdg[0].toPx();
const y = pdg[1].toPx();
pd = [x, y, x, y];
}
if (pdg.length === 3) {
const x = pdg[0].toPx();
const y = pdg[1].toPx();
const z = pdg[2].toPx();
pd = [x, y, z, y];
}
if (pdg.length === 4) {
const x = pdg[0].toPx();
const y = pdg[1].toPx();
const z = pdg[2].toPx();
const a = pdg[3].toPx();
pd = [x, y, z, a];
}
}
return pd;
}
// 画文字的背景图片
_doBackground(view) {
this.ctx.save();
const { width: rawWidth, height: rawHeight } = this._preProcess(view, true);
const { background } = view.css;
let pd = this._doPaddings(view);
const width = rawWidth + pd[1] + pd[3];
const height = rawHeight + pd[0] + pd[2];
this._doClip(view.css.borderRadius, width, height, view.css.borderStyle);
if (GD.api.isGradient(background)) {
GD.api.doGradient(background, width, height, this.ctx);
} else {
this.ctx.fillStyle = background;
}
this.ctx.fillRect(-(width / 2), -(height / 2), width, height);
this.ctx.restore();
}
_drawQRCode(view) {
this.ctx.save();
const { width, height } = this._preProcess(view);
QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color);
this.ctx.restore();
this._doBorder(view, width, height);
}
_drawAbsImage(view) {
if (!view.url) {
return;
}
this.ctx.save();
const { width, height } = this._preProcess(view);
// 获得缩放到图片大小级别的裁减框
let rWidth = view.sWidth;
let rHeight = view.sHeight;
let startX = 0;
let startY = 0;
// 绘画区域比例
const cp = width / height;
// 原图比例
const op = view.sWidth / view.sHeight;
if (cp >= op) {
rHeight = rWidth / cp;
startY = Math.round((view.sHeight - rHeight) / 2);
} else {
rWidth = rHeight * cp;
startX = Math.round((view.sWidth - rWidth) / 2);
}
if (view.css && view.css.mode === 'scaleToFill') {
this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height);
} else {
this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height);
view.rect.startX = startX / view.sWidth;
view.rect.startY = startY / view.sHeight;
view.rect.endX = (startX + rWidth) / view.sWidth;
view.rect.endY = (startY + rHeight) / view.sHeight;
}
this.ctx.restore();
this._doBorder(view, width, height);
}
_fillAbsText(view) {
if (!view.text) {
return;
}
if (view.css.background) {
// 生成背景
this._doBackground(view);
}
this.ctx.save();
const { width, height, extra } = this._preProcess(view, view.css.background && view.css.borderRadius);
this.ctx.fillStyle = view.css.color || 'black';
if (view.id && penCache.textLines[view.id]) {
this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
for (const i of penCache.textLines[view.id]) {
const { measuredWith, text, x, y, textDecoration } = i;
if (view.css.textStyle === 'stroke') {
this.ctx.strokeText(text, x, y, measuredWith);
} else {
this.ctx.fillText(text, x, y, measuredWith);
}
if (textDecoration) {
const fontSize = view.css.fontSize.toPx();
this.ctx.lineWidth = fontSize / 13;
this.ctx.beginPath();
this.ctx.moveTo(...textDecoration.moveTo);
this.ctx.lineTo(...textDecoration.lineTo);
this.ctx.closePath();
this.ctx.strokeStyle = view.css.color;
this.ctx.stroke();
}
}
} else {
const { lines, lineHeight, textArray, linesArray } = extra;
// 如果设置了id则保留 text 的长度
if (view.id) {
let textWidth = 0;
for (let i = 0; i < textArray.length; ++i) {
const _w = this.ctx.measureText(textArray[i]).width;
textWidth = _w > textWidth ? _w : textWidth;
}
penCache.viewRect[view.id].width = width ? (textWidth < width ? textWidth : width) : textWidth;
}
let lineIndex = 0;
for (let j = 0; j < textArray.length; ++j) {
const preLineLength = Math.ceil(textArray[j].length / linesArray[j]);
let start = 0;
let alreadyCount = 0;
for (let i = 0; i < linesArray[j]; ++i) {
// 绘制行数大于最大行数,则直接跳出循环
if (lineIndex >= lines) {
break;
}
alreadyCount = preLineLength;
let text = textArray[j].substr(start, alreadyCount);
let measuredWith = this.ctx.measureText(text).width;
// 如果测量大小小于width一个字符的大小则进行补齐如果测量大小超出 width则进行减除
// 如果已经到文本末尾,也不要进行该循环
while (
start + alreadyCount <= textArray[j].length &&
(width - measuredWith > view.css.fontSize.toPx() || measuredWith - width > view.css.fontSize.toPx())
) {
if (measuredWith < width) {
text = textArray[j].substr(start, ++alreadyCount);
} else {
if (text.length <= 1) {
// 如果只有一个字符时,直接跳出循环
break;
}
text = textArray[j].substr(start, --alreadyCount);
// break;
}
measuredWith = this.ctx.measureText(text).width;
}
start += text.length;
// 如果是最后一行了,发现还有未绘制完的内容,则加...
if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) {
while (this.ctx.measureText(`${text}...`).width > width) {
if (text.length <= 1) {
// 如果只有一个字符时,直接跳出循环
break;
}
text = text.substring(0, text.length - 1);
}
text += '...';
measuredWith = this.ctx.measureText(text).width;
}
this.ctx.textAlign = view.css.textAlign ? view.css.textAlign : 'left';
let x;
let lineX;
switch (view.css.textAlign) {
case 'center':
x = 0;
lineX = x - measuredWith / 2;
break;
case 'right':
x = width / 2;
lineX = x - measuredWith;
break;
default:
x = -(width / 2);
lineX = x;
break;
}
const y =
-(height / 2) +
(lineIndex === 0 ? view.css.fontSize.toPx() : view.css.fontSize.toPx() + lineIndex * lineHeight);
lineIndex++;
if (view.css.textStyle === 'stroke') {
this.ctx.strokeText(text, x, y, measuredWith);
} else {
this.ctx.fillText(text, x, y, measuredWith);
}
const fontSize = view.css.fontSize.toPx();
let textDecoration;
if (view.css.textDecoration) {
this.ctx.lineWidth = fontSize / 13;
this.ctx.beginPath();
if (/\bunderline\b/.test(view.css.textDecoration)) {
this.ctx.moveTo(lineX, y);
this.ctx.lineTo(lineX + measuredWith, y);
textDecoration = {
moveTo: [lineX, y],
lineTo: [lineX + measuredWith, y],
};
}
if (/\boverline\b/.test(view.css.textDecoration)) {
this.ctx.moveTo(lineX, y - fontSize);
this.ctx.lineTo(lineX + measuredWith, y - fontSize);
textDecoration = {
moveTo: [lineX, y - fontSize],
lineTo: [lineX + measuredWith, y - fontSize],
};
}
if (/\bline-through\b/.test(view.css.textDecoration)) {
this.ctx.moveTo(lineX, y - fontSize / 3);
this.ctx.lineTo(lineX + measuredWith, y - fontSize / 3);
textDecoration = {
moveTo: [lineX, y - fontSize / 3],
lineTo: [lineX + measuredWith, y - fontSize / 3],
};
}
this.ctx.closePath();
this.ctx.strokeStyle = view.css.color;
this.ctx.stroke();
}
if (view.id) {
penCache.textLines[view.id]
? penCache.textLines[view.id].push({
text,
x,
y,
measuredWith,
textDecoration,
})
: (penCache.textLines[view.id] = [
{
text,
x,
y,
measuredWith,
textDecoration,
},
]);
}
}
}
}
this.ctx.restore();
this._doBorder(view, width, height);
}
_drawAbsRect(view) {
this.ctx.save();
const { width, height } = this._preProcess(view);
if (GD.api.isGradient(view.css.color)) {
GD.api.doGradient(view.css.color, width, height, this.ctx);
} else {
this.ctx.fillStyle = view.css.color;
}
const { borderRadius, borderStyle, borderWidth } = view.css;
this._border({
borderRadius,
width,
height,
borderWidth,
borderStyle,
});
this.ctx.fill();
this.ctx.restore();
this._doBorder(view, width, height);
}
// shadow 支持 (x, y, blur, color), 不支持 spread
// shadow:0px 0px 10px rgba(0,0,0,0.1);
_doShadow(view) {
if (!view.css || !view.css.shadow) {
return;
}
const box = view.css.shadow.replace(/,\s+/g, ',').split(/\s+/);
if (box.length > 4) {
console.error("shadow don't spread option");
return;
}
this.ctx.shadowOffsetX = parseInt(box[0], 10);
this.ctx.shadowOffsetY = parseInt(box[1], 10);
this.ctx.shadowBlur = parseInt(box[2], 10);
this.ctx.shadowColor = box[3];
}
_getAngle(angle) {
return (Number(angle) * Math.PI) / 180;
}
}

View File

@@ -0,0 +1,784 @@
/* eslint-disable */
!(function () {
// alignment pattern
var adelta = [
0, 11, 15, 19, 23, 27, 31,
16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,
26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28
];
// version block
var vpat = [
0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d,
0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9,
0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75,
0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64,
0x541, 0xc69
];
// final format bits with mask: level << 3 | mask
var fmtword = [
0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, //L
0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, //M
0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, //Q
0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b //H
];
// 4 per version: number of blocks 1,2; data width; ecc width
var eccblocks = [
1, 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, 1, 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, 18, 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, 68, 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, 1, 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, 4, 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, 20, 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, 14, 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, 11, 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, 24, 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, 17, 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, 26, 17, 4, 21, 26, 9, 16, 13, 26,
3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,
4, 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, 24, 30, 34, 0, 13, 24,
4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30,
6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30,
10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30,
8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30,
3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30,
7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30,
5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30,
13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30,
17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30,
17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30,
13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30,
12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30,
6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30,
17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30,
4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30,
20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30,
19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
];
// Galois field log table
var glog = [
0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,
0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,
0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,
0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,
0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,
0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,
0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,
0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,
0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,
0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,
0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,
0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,
0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,
0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,
0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,
0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf
];
// Galios field exponent table
var gexp = [
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,
0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,
0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,
0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,
0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,
0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,
0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,
0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,
0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,
0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,
0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,
0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,
0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,
0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,
0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,
0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00
];
// Working buffers:
// data input and ecc append, image working buffer, fixed part of image, run lengths for badness
var strinbuf = [], eccbuf = [], qrframe = [], framask = [], rlens = [];
// Control values - width is based on version, last 4 are from table.
var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
var ecclevel = 2;
// set bit to indicate cell in qrframe is immutable. symmetric around diagonal
function setmask(x, y) {
var bt;
if (x > y) {
bt = x;
x = y;
y = bt;
}
// y*y = 1+3+5...
bt = y;
bt *= y;
bt += y;
bt >>= 1;
bt += x;
framask[bt] = 1;
}
// enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
function putalign(x, y) {
var j;
qrframe[x + width * y] = 1;
for (j = -2; j < 2; j++) {
qrframe[(x + j) + width * (y - 2)] = 1;
qrframe[(x - 2) + width * (y + j + 1)] = 1;
qrframe[(x + 2) + width * (y + j)] = 1;
qrframe[(x + j + 1) + width * (y + 2)] = 1;
}
for (j = 0; j < 2; j++) {
setmask(x - 1, y + j);
setmask(x + 1, y - j);
setmask(x - j, y - 1);
setmask(x + j, y + 1);
}
}
//========================================================================
// Reed Solomon error correction
// exponentiation mod N
function modnn(x) {
while (x >= 255) {
x -= 255;
x = (x >> 8) + (x & 255);
}
return x;
}
var genpoly = [];
// Calculate and append ECC data to data block. Block is in strinbuf, indexes to buffers given.
function appendrs(data, dlen, ecbuf, eclen) {
var i, j, fb;
for (i = 0; i < eclen; i++)
strinbuf[ecbuf + i] = 0;
for (i = 0; i < dlen; i++) {
fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
if (fb != 255) /* fb term is non-zero */
for (j = 1; j < eclen; j++)
strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
else
for (j = ecbuf; j < ecbuf + eclen; j++)
strinbuf[j] = strinbuf[j + 1];
strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
}
}
//========================================================================
// Frame data insert following the path rules
// check mask - since symmetrical use half.
function ismasked(x, y) {
var bt;
if (x > y) {
bt = x;
x = y;
y = bt;
}
bt = y;
bt += y * y;
bt >>= 1;
bt += x;
return framask[bt];
}
//========================================================================
// Apply the selected mask out of the 8.
function applymask(m) {
var x, y, r3x, r3y;
switch (m) {
case 0:
for (y = 0; y < width; y++)
for (x = 0; x < width; x++)
if (!((x + y) & 1) && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
break;
case 1:
for (y = 0; y < width; y++)
for (x = 0; x < width; x++)
if (!(y & 1) && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
break;
case 2:
for (y = 0; y < width; y++)
for (r3x = 0, x = 0; x < width; x++ , r3x++) {
if (r3x == 3)
r3x = 0;
if (!r3x && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
break;
case 3:
for (r3y = 0, y = 0; y < width; y++ , r3y++) {
if (r3y == 3)
r3y = 0;
for (r3x = r3y, x = 0; x < width; x++ , r3x++) {
if (r3x == 3)
r3x = 0;
if (!r3x && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
}
break;
case 4:
for (y = 0; y < width; y++)
for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++ , r3x++) {
if (r3x == 3) {
r3x = 0;
r3y = !r3y;
}
if (!r3y && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
break;
case 5:
for (r3y = 0, y = 0; y < width; y++ , r3y++) {
if (r3y == 3)
r3y = 0;
for (r3x = 0, x = 0; x < width; x++ , r3x++) {
if (r3x == 3)
r3x = 0;
if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
}
break;
case 6:
for (r3y = 0, y = 0; y < width; y++ , r3y++) {
if (r3y == 3)
r3y = 0;
for (r3x = 0, x = 0; x < width; x++ , r3x++) {
if (r3x == 3)
r3x = 0;
if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
}
break;
case 7:
for (r3y = 0, y = 0; y < width; y++ , r3y++) {
if (r3y == 3)
r3y = 0;
for (r3x = 0, x = 0; x < width; x++ , r3x++) {
if (r3x == 3)
r3x = 0;
if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y))
qrframe[x + y * width] ^= 1;
}
}
break;
}
return;
}
// Badness coefficients.
var N1 = 3, N2 = 3, N3 = 40, N4 = 10;
// Using the table of the length of each run, calculate the amount of bad image
// - long runs or those that look like finders; called twice, once each for X and Y
function badruns(length) {
var i;
var runsbad = 0;
for (i = 0; i <= length; i++)
if (rlens[i] >= 5)
runsbad += N1 + rlens[i] - 5;
// BwBBBwB as in finder
for (i = 3; i < length - 1; i += 2)
if (rlens[i - 2] == rlens[i + 2]
&& rlens[i + 2] == rlens[i - 1]
&& rlens[i - 1] == rlens[i + 1]
&& rlens[i - 1] * 3 == rlens[i]
// white around the black pattern? Not part of spec
&& (rlens[i - 3] == 0 // beginning
|| i + 3 > length // end
|| rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4)
)
runsbad += N3;
return runsbad;
}
// Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
function badcheck() {
var x, y, h, b, b1;
var thisbad = 0;
var bw = 0;
// blocks of same color.
for (y = 0; y < width - 1; y++)
for (x = 0; x < width - 1; x++)
if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y]
&& qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black
|| !(qrframe[x + width * y] || qrframe[(x + 1) + width * y]
|| qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white
thisbad += N2;
// X runs
for (y = 0; y < width; y++) {
rlens[0] = 0;
for (h = b = x = 0; x < width; x++) {
if ((b1 = qrframe[x + width * y]) == b)
rlens[h]++;
else
rlens[++h] = 1;
b = b1;
bw += b ? 1 : -1;
}
thisbad += badruns(h);
}
// black/white imbalance
if (bw < 0)
bw = -bw;
var big = bw;
var count = 0;
big += big << 2;
big <<= 1;
while (big > width * width)
big -= width * width, count++;
thisbad += count * N4;
// Y runs
for (x = 0; x < width; x++) {
rlens[0] = 0;
for (h = b = y = 0; y < width; y++) {
if ((b1 = qrframe[x + width * y]) == b)
rlens[h]++;
else
rlens[++h] = 1;
b = b1;
}
thisbad += badruns(h);
}
return thisbad;
}
function genframe(instring) {
var x, y, k, t, v, i, j, m;
// find the smallest version that fits the string
t = instring.length;
version = 0;
do {
version++;
k = (ecclevel - 1) * 4 + (version - 1) * 16;
neccblk1 = eccblocks[k++];
neccblk2 = eccblocks[k++];
datablkw = eccblocks[k++];
eccblkwid = eccblocks[k];
k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
if (t <= k)
break;
} while (version < 40);
// FIXME - insure that it fits insted of being truncated
width = 17 + 4 * version;
// allocate, clear and setup data structures
v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
for (t = 0; t < v; t++)
eccbuf[t] = 0;
strinbuf = instring.slice(0);
for (t = 0; t < width * width; t++)
qrframe[t] = 0;
for (t = 0; t < (width * (width + 1) + 1) / 2; t++)
framask[t] = 0;
// insert finders - black to frame, white to mask
for (t = 0; t < 3; t++) {
k = 0;
y = 0;
if (t == 1)
k = (width - 7);
if (t == 2)
y = (width - 7);
qrframe[(y + 3) + width * (k + 3)] = 1;
for (x = 0; x < 6; x++) {
qrframe[(y + x) + width * k] = 1;
qrframe[y + width * (k + x + 1)] = 1;
qrframe[(y + 6) + width * (k + x)] = 1;
qrframe[(y + x + 1) + width * (k + 6)] = 1;
}
for (x = 1; x < 5; x++) {
setmask(y + x, k + 1);
setmask(y + 1, k + x + 1);
setmask(y + 5, k + x);
setmask(y + x + 1, k + 5);
}
for (x = 2; x < 4; x++) {
qrframe[(y + x) + width * (k + 2)] = 1;
qrframe[(y + 2) + width * (k + x + 1)] = 1;
qrframe[(y + 4) + width * (k + x)] = 1;
qrframe[(y + x + 1) + width * (k + 4)] = 1;
}
}
// alignment blocks
if (version > 1) {
t = adelta[version];
y = width - 7;
for (; ;) {
x = width - 7;
while (x > t - 3) {
putalign(x, y);
if (x < t)
break;
x -= t;
}
if (y <= t + 9)
break;
y -= t;
putalign(6, y);
putalign(y, 6);
}
}
// single black
qrframe[8 + width * (width - 8)] = 1;
// timing gap - mask only
for (y = 0; y < 7; y++) {
setmask(7, y);
setmask(width - 8, y);
setmask(7, y + width - 7);
}
for (x = 0; x < 8; x++) {
setmask(x, 7);
setmask(x + width - 8, 7);
setmask(x, width - 8);
}
// reserve mask-format area
for (x = 0; x < 9; x++)
setmask(x, 8);
for (x = 0; x < 8; x++) {
setmask(x + width - 8, 8);
setmask(8, x);
}
for (y = 0; y < 7; y++)
setmask(8, y + width - 7);
// timing row/col
for (x = 0; x < width - 14; x++)
if (x & 1) {
setmask(8 + x, 6);
setmask(6, 8 + x);
}
else {
qrframe[(8 + x) + width * 6] = 1;
qrframe[6 + width * (8 + x)] = 1;
}
// version block
if (version > 6) {
t = vpat[version - 7];
k = 17;
for (x = 0; x < 6; x++)
for (y = 0; y < 3; y++ , k--)
if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
qrframe[(5 - x) + width * (2 - y + width - 11)] = 1;
qrframe[(2 - y + width - 11) + width * (5 - x)] = 1;
}
else {
setmask(5 - x, 2 - y + width - 11);
setmask(2 - y + width - 11, 5 - x);
}
}
// sync mask bits - only set above for white spaces, so add in black bits
for (y = 0; y < width; y++)
for (x = 0; x <= y; x++)
if (qrframe[x + width * y])
setmask(x, y);
// convert string to bitstream
// 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
v = strinbuf.length;
// string to array
for (i = 0; i < v; i++)
eccbuf[i] = strinbuf.charCodeAt(i);
strinbuf = eccbuf.slice(0);
// calculate max string length
x = datablkw * (neccblk1 + neccblk2) + neccblk2;
if (v >= x - 2) {
v = x - 2;
if (version > 9)
v--;
}
// shift and repack to insert length prefix
i = v;
if (version > 9) {
strinbuf[i + 2] = 0;
strinbuf[i + 3] = 0;
while (i--) {
t = strinbuf[i];
strinbuf[i + 3] |= 255 & (t << 4);
strinbuf[i + 2] = t >> 4;
}
strinbuf[2] |= 255 & (v << 4);
strinbuf[1] = v >> 4;
strinbuf[0] = 0x40 | (v >> 12);
}
else {
strinbuf[i + 1] = 0;
strinbuf[i + 2] = 0;
while (i--) {
t = strinbuf[i];
strinbuf[i + 2] |= 255 & (t << 4);
strinbuf[i + 1] = t >> 4;
}
strinbuf[1] |= 255 & (v << 4);
strinbuf[0] = 0x40 | (v >> 4);
}
// fill to end with pad pattern
i = v + 3 - (version < 10);
while (i < x) {
strinbuf[i++] = 0xec;
// buffer has room if (i == x) break;
strinbuf[i++] = 0x11;
}
// calculate and append ECC
// calculate generator polynomial
genpoly[0] = 1;
for (i = 0; i < eccblkwid; i++) {
genpoly[i + 1] = 1;
for (j = i; j > 0; j--)
genpoly[j] = genpoly[j]
? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
}
for (i = 0; i <= eccblkwid; i++)
genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step
// append ecc to data buffer
k = x;
y = 0;
for (i = 0; i < neccblk1; i++) {
appendrs(y, datablkw, k, eccblkwid);
y += datablkw;
k += eccblkwid;
}
for (i = 0; i < neccblk2; i++) {
appendrs(y, datablkw + 1, k, eccblkwid);
y += datablkw + 1;
k += eccblkwid;
}
// interleave blocks
y = 0;
for (i = 0; i < datablkw; i++) {
for (j = 0; j < neccblk1; j++)
eccbuf[y++] = strinbuf[i + j * datablkw];
for (j = 0; j < neccblk2; j++)
eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
}
for (j = 0; j < neccblk2; j++)
eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))];
for (i = 0; i < eccblkwid; i++)
for (j = 0; j < neccblk1 + neccblk2; j++)
eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
strinbuf = eccbuf;
// pack bits into frame avoiding masked area.
x = y = width - 1;
k = v = 1; // up, minus
/* inteleaved data and ecc codes */
m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
for (i = 0; i < m; i++) {
t = strinbuf[i];
for (j = 0; j < 8; j++ , t <<= 1) {
if (0x80 & t)
qrframe[x + width * y] = 1;
do { // find next fill position
if (v)
x--;
else {
x++;
if (k) {
if (y != 0)
y--;
else {
x -= 2;
k = !k;
if (x == 6) {
x--;
y = 9;
}
}
}
else {
if (y != width - 1)
y++;
else {
x -= 2;
k = !k;
if (x == 6) {
x--;
y -= 8;
}
}
}
}
v = !v;
} while (ismasked(x, y));
}
}
// save pre-mask copy of frame
strinbuf = qrframe.slice(0);
t = 0; // best
y = 30000; // demerit
// for instead of while since in original arduino code
// if an early mask was "good enough" it wouldn't try for a better one
// since they get more complex and take longer.
for (k = 0; k < 8; k++) {
applymask(k); // returns black-white imbalance
x = badcheck();
if (x < y) { // current mask better than previous best?
y = x;
t = k;
}
if (t == 7)
break; // don't increment i to a void redoing mask
qrframe = strinbuf.slice(0); // reset for next pass
}
if (t != k) // redo best mask - none good enough, last wasn't t
applymask(t);
// add in final mask/ecclevel bytes
y = fmtword[t + ((ecclevel - 1) << 3)];
// low byte
for (k = 0; k < 8; k++ , y >>= 1)
if (y & 1) {
qrframe[(width - 1 - k) + width * 8] = 1;
if (k < 6)
qrframe[8 + width * k] = 1;
else
qrframe[8 + width * (k + 1)] = 1;
}
// high byte
for (k = 0; k < 7; k++ , y >>= 1)
if (y & 1) {
qrframe[8 + width * (width - 7 + k)] = 1;
if (k)
qrframe[(6 - k) + width * 8] = 1;
else
qrframe[7 + width * 8] = 1;
}
return qrframe;
}
var _canvas = null;
var api = {
get ecclevel() {
return ecclevel;
},
set ecclevel(val) {
ecclevel = val;
},
get size() {
return _size;
},
set size(val) {
_size = val
},
get canvas() {
return _canvas;
},
set canvas(el) {
_canvas = el;
},
getFrame: function (string) {
return genframe(string);
},
//这里的utf16to8(str)是对Text中的字符串进行转码让其支持中文
utf16to8: function (str) {
var out, i, len, c;
out = "";
len = str.length;
for (i = 0; i < len; i++) {
c = str.charCodeAt(i);
if ((c >= 0x0001) && (c <= 0x007F)) {
out += str.charAt(i);
} else if (c > 0x07FF) {
out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F));
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
} else {
out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F));
}
}
return out;
},
/**
* 新增$this参数传入组件的this,兼容在组件中生成
* @param bg 目前只能设置颜色值
*/
draw: function (str, ctx, startX, startY, cavW, cavH, bg, color, $this, ecc) {
var that = this;
ecclevel = ecc || ecclevel;
if (!ctx) {
console.warn('No canvas provided to draw QR code in!')
return;
}
var size = Math.min(cavW, cavH);
str = that.utf16to8(str);//增加中文显示
var frame = that.getFrame(str);
var px = size / width;
if (bg) {
ctx.fillStyle = bg;
ctx.fillRect(startX, startY, cavW, cavW);
}
ctx.fillStyle = color || 'black';
for (var i = 0; i < width; i++) {
for (var j = 0; j < width; j++) {
if (frame[j * width + i]) {
ctx.fillRect(startX + px * i, startY + px * j, px, px);
}
}
}
}
}
module.exports = { api }
// exports.draw = api;
})();

View File

@@ -0,0 +1,97 @@
var hexcase = 0;
var chrsz = 8;
function hex_sha1(s) {
return binb2hex(core_sha1(str2binb(s), s.length * chrsz));
}
function core_sha1(x, len) {
x[len >> 5] |= 0x80 << (24 - (len % 32));
x[(((len + 64) >> 9) << 4) + 15] = len;
var w = Array(80);
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
var e = -1009589776;
for (var i = 0; i < x.length; i += 16) {
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
var olde = e;
for (var j = 0; j < 80; j++) {
if (j < 16) w[j] = x[i + j];
else w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
var t = safe_add(
safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
safe_add(safe_add(e, w[j]), sha1_kt(j))
);
e = d;
d = c;
c = rol(b, 30);
b = a;
a = t;
}
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
e = safe_add(e, olde);
}
return Array(a, b, c, d, e);
}
function sha1_ft(t, b, c, d) {
if (t < 20) return (b & c) | (~b & d);
if (t < 40) return b ^ c ^ d;
if (t < 60) return (b & c) | (b & d) | (c & d);
return b ^ c ^ d;
}
function sha1_kt(t) {
return t < 20
? 1518500249
: t < 40
? 1859775393
: t < 60
? -1894007588
: -899497514;
}
function safe_add(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
function rol(num, cnt) {
return (num << cnt) | (num >>> (32 - cnt));
}
function str2binb(str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for (var i = 0; i < str.length * chrsz; i += chrsz)
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - (i % 32));
return bin;
}
function binb2hex(binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str +=
hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8 + 4)) & 0xf) +
hex_tab.charAt((binarray[i >> 2] >> ((3 - (i % 4)) * 8)) & 0xf);
}
return str;
}
module.exports = {
hex_sha1,
}

View File

@@ -0,0 +1,78 @@
function isValidUrl(url) {
return isOnlineUrl(url) || isDataUrl(url);
}
function isOnlineUrl(url) {
return /(ht|f)tp(s?):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url)
}
function isDataUrl(url) {
return /data:image\/(\w+);base64,(.*)/.test(url);
}
/**
* 深度对比两个对象是否一致
* from: https://github.com/epoberezkin/fast-deep-equal
* @param {Object} a 对象a
* @param {Object} b 对象b
* @return {Boolean} 是否相同
*/
/* eslint-disable */
function equal(a, b) {
if (a === b) return true;
if (a && b && typeof a == 'object' && typeof b == 'object') {
var arrA = Array.isArray(a)
, arrB = Array.isArray(b)
, i
, length
, key;
if (arrA && arrB) {
length = a.length;
if (length != b.length) return false;
for (i = length; i-- !== 0;)
if (!equal(a[i], b[i])) return false;
return true;
}
if (arrA != arrB) return false;
var dateA = a instanceof Date
, dateB = b instanceof Date;
if (dateA != dateB) return false;
if (dateA && dateB) return a.getTime() == b.getTime();
var regexpA = a instanceof RegExp
, regexpB = b instanceof RegExp;
if (regexpA != regexpB) return false;
if (regexpA && regexpB) return a.toString() == b.toString();
var keys = Object.keys(a);
length = keys.length;
if (length !== Object.keys(b).length)
return false;
for (i = length; i-- !== 0;)
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
for (i = length; i-- !== 0;) {
key = keys[i];
if (!equal(a[key], b[key])) return false;
}
return true;
}
return a!==a && b!==b;
}
module.exports = {
isValidUrl,
isOnlineUrl,
isDataUrl,
equal
};

View File

@@ -0,0 +1,611 @@
// @ts-check
export default class WxCanvas {
ctx;
type;
canvasId;
canvasNode;
stepList = [];
canvasPrototype = {};
constructor(type, ctx, canvasId, isNew, canvasNode) {
this.ctx = ctx;
this.canvasId = canvasId;
this.type = type;
if (isNew) {
this.canvasNode = canvasNode || {};
}
}
set width(w) {
if (this.canvasNode) this.canvasNode.width = w;
}
get width() {
if (this.canvasNode) return this.canvasNode.width;
return 0;
}
set height(h) {
if (this.canvasNode) this.canvasNode.height = h;
}
get height() {
if (this.canvasNode) return this.canvasNode.height;
return 0;
}
set lineWidth(args) {
this.canvasPrototype.lineWidth = args;
this.stepList.push({
action: "lineWidth",
args,
actionType: "set",
});
}
get lineWidth() {
return this.canvasPrototype.lineWidth;
}
set lineCap(args) {
this.canvasPrototype.lineCap = args;
this.stepList.push({
action: "lineCap",
args,
actionType: "set",
});
}
get lineCap() {
return this.canvasPrototype.lineCap;
}
set lineJoin(args) {
this.canvasPrototype.lineJoin = args;
this.stepList.push({
action: "lineJoin",
args,
actionType: "set",
});
}
get lineJoin() {
return this.canvasPrototype.lineJoin;
}
set miterLimit(args) {
this.canvasPrototype.miterLimit = args;
this.stepList.push({
action: "miterLimit",
args,
actionType: "set",
});
}
get miterLimit() {
return this.canvasPrototype.miterLimit;
}
set lineDashOffset(args) {
this.canvasPrototype.lineDashOffset = args;
this.stepList.push({
action: "lineDashOffset",
args,
actionType: "set",
});
}
get lineDashOffset() {
return this.canvasPrototype.lineDashOffset;
}
set font(args) {
this.canvasPrototype.font = args;
this.ctx.font = args;
this.stepList.push({
action: "font",
args,
actionType: "set",
});
}
get font() {
return this.canvasPrototype.font;
}
set textAlign(args) {
this.canvasPrototype.textAlign = args;
this.stepList.push({
action: "textAlign",
args,
actionType: "set",
});
}
get textAlign() {
return this.canvasPrototype.textAlign;
}
set textBaseline(args) {
this.canvasPrototype.textBaseline = args;
this.stepList.push({
action: "textBaseline",
args,
actionType: "set",
});
}
get textBaseline() {
return this.canvasPrototype.textBaseline;
}
set fillStyle(args) {
this.canvasPrototype.fillStyle = args;
this.stepList.push({
action: "fillStyle",
args,
actionType: "set",
});
}
get fillStyle() {
return this.canvasPrototype.fillStyle;
}
set strokeStyle(args) {
this.canvasPrototype.strokeStyle = args;
this.stepList.push({
action: "strokeStyle",
args,
actionType: "set",
});
}
get strokeStyle() {
return this.canvasPrototype.strokeStyle;
}
set globalAlpha(args) {
this.canvasPrototype.globalAlpha = args;
this.stepList.push({
action: "globalAlpha",
args,
actionType: "set",
});
}
get globalAlpha() {
return this.canvasPrototype.globalAlpha;
}
set globalCompositeOperation(args) {
this.canvasPrototype.globalCompositeOperation = args;
this.stepList.push({
action: "globalCompositeOperation",
args,
actionType: "set",
});
}
get globalCompositeOperation() {
return this.canvasPrototype.globalCompositeOperation;
}
set shadowColor(args) {
this.canvasPrototype.shadowColor = args;
this.stepList.push({
action: "shadowColor",
args,
actionType: "set",
});
}
get shadowColor() {
return this.canvasPrototype.shadowColor;
}
set shadowOffsetX(args) {
this.canvasPrototype.shadowOffsetX = args;
this.stepList.push({
action: "shadowOffsetX",
args,
actionType: "set",
});
}
get shadowOffsetX() {
return this.canvasPrototype.shadowOffsetX;
}
set shadowOffsetY(args) {
this.canvasPrototype.shadowOffsetY = args;
this.stepList.push({
action: "shadowOffsetY",
args,
actionType: "set",
});
}
get shadowOffsetY() {
return this.canvasPrototype.shadowOffsetY;
}
set shadowBlur(args) {
this.canvasPrototype.shadowBlur = args;
this.stepList.push({
action: "shadowBlur",
args,
actionType: "set",
});
}
get shadowBlur() {
return this.canvasPrototype.shadowBlur;
}
save() {
this.stepList.push({
action: "save",
args: null,
actionType: "func",
});
}
restore() {
this.stepList.push({
action: "restore",
args: null,
actionType: "func",
});
}
setLineDash(...args) {
this.canvasPrototype.lineDash = args;
this.stepList.push({
action: "setLineDash",
args,
actionType: "func",
});
}
moveTo(...args) {
this.stepList.push({
action: "moveTo",
args,
actionType: "func",
});
}
closePath() {
this.stepList.push({
action: "closePath",
args: null,
actionType: "func",
});
}
lineTo(...args) {
this.stepList.push({
action: "lineTo",
args,
actionType: "func",
});
}
quadraticCurveTo(...args) {
this.stepList.push({
action: "quadraticCurveTo",
args,
actionType: "func",
});
}
bezierCurveTo(...args) {
this.stepList.push({
action: "bezierCurveTo",
args,
actionType: "func",
});
}
arcTo(...args) {
this.stepList.push({
action: "arcTo",
args,
actionType: "func",
});
}
arc(...args) {
this.stepList.push({
action: "arc",
args,
actionType: "func",
});
}
rect(...args) {
this.stepList.push({
action: "rect",
args,
actionType: "func",
});
}
scale(...args) {
this.stepList.push({
action: "scale",
args,
actionType: "func",
});
}
rotate(...args) {
this.stepList.push({
action: "rotate",
args,
actionType: "func",
});
}
translate(...args) {
this.stepList.push({
action: "translate",
args,
actionType: "func",
});
}
transform(...args) {
this.stepList.push({
action: "transform",
args,
actionType: "func",
});
}
setTransform(...args) {
this.stepList.push({
action: "setTransform",
args,
actionType: "func",
});
}
clearRect(...args) {
this.stepList.push({
action: "clearRect",
args,
actionType: "func",
});
}
fillRect(...args) {
this.stepList.push({
action: "fillRect",
args,
actionType: "func",
});
}
strokeRect(...args) {
this.stepList.push({
action: "strokeRect",
args,
actionType: "func",
});
}
fillText(...args) {
this.stepList.push({
action: "fillText",
args,
actionType: "func",
});
}
strokeText(...args) {
this.stepList.push({
action: "strokeText",
args,
actionType: "func",
});
}
beginPath() {
this.stepList.push({
action: "beginPath",
args: null,
actionType: "func",
});
}
fill() {
this.stepList.push({
action: "fill",
args: null,
actionType: "func",
});
}
stroke() {
this.stepList.push({
action: "stroke",
args: null,
actionType: "func",
});
}
drawFocusIfNeeded(...args) {
this.stepList.push({
action: "drawFocusIfNeeded",
args,
actionType: "func",
});
}
clip() {
this.stepList.push({
action: "clip",
args: null,
actionType: "func",
});
}
isPointInPath(...args) {
this.stepList.push({
action: "isPointInPath",
args,
actionType: "func",
});
}
drawImage(...args) {
this.stepList.push({
action: "drawImage",
args,
actionType: "func",
});
}
addHitRegion(...args) {
this.stepList.push({
action: "addHitRegion",
args,
actionType: "func",
});
}
removeHitRegion(...args) {
this.stepList.push({
action: "removeHitRegion",
args,
actionType: "func",
});
}
clearHitRegions(...args) {
this.stepList.push({
action: "clearHitRegions",
args,
actionType: "func",
});
}
putImageData(...args) {
this.stepList.push({
action: "putImageData",
args,
actionType: "func",
});
}
getLineDash() {
return this.canvasPrototype.lineDash;
}
createLinearGradient(...args) {
return this.ctx.createLinearGradient(...args);
}
createRadialGradient(...args) {
if (this.type === "2d") {
return this.ctx.createRadialGradient(...args);
} else {
return this.ctx.createCircularGradient(...args.slice(3, 6));
}
}
createPattern(...args) {
return this.ctx.createPattern(...args);
}
measureText(...args) {
return this.ctx.measureText(...args);
}
createImageData(...args) {
return this.ctx.createImageData(...args);
}
getImageData(...args) {
return this.ctx.getImageData(...args);
}
async draw(reserve, func) {
const realstepList = this.stepList.slice();
this.stepList.length = 0;
if (this.type === "mina") {
if (realstepList.length > 0) {
for (const step of realstepList) {
this.implementMinaStep(step);
}
realstepList.length = 0;
}
this.ctx.draw(reserve, func);
} else if (this.type === "2d") {
if (!reserve) {
this.ctx.clearRect(0, 0, this.canvasNode.width, this.canvasNode.height);
}
if (realstepList.length > 0) {
for (const step of realstepList) {
await this.implement2DStep(step);
}
realstepList.length = 0;
}
if (func) {
func();
}
}
realstepList.length = 0;
}
implementMinaStep(step) {
switch (step.action) {
case "textAlign": {
this.ctx.setTextAlign(step.args);
break;
}
case "textBaseline": {
this.ctx.setTextBaseline(step.args);
break;
}
default: {
if (step.actionType === "set") {
this.ctx[step.action] = step.args;
} else if (step.actionType === "func") {
if (step.args) {
this.ctx[step.action](...step.args);
} else {
this.ctx[step.action]();
}
}
break;
}
}
}
implement2DStep(step) {
return new Promise((resolve) => {
if (step.action === "drawImage") {
const img = this.canvasNode.createImage();
img.src = step.args[0];
img.onload = () => {
this.ctx.drawImage(img, ...step.args.slice(1));
resolve();
};
} else {
if (step.actionType === "set") {
this.ctx[step.action] = step.args;
} else if (step.actionType === "func") {
if (step.args) {
this.ctx[step.action](...step.args);
} else {
this.ctx[step.action]();
}
}
resolve();
}
});
}
}

View File

@@ -0,0 +1,864 @@
import Pen, { penCache, clearPenCache } from './lib/pen';
import Downloader from './lib/downloader';
import WxCanvas from './lib/wx-canvas';
const util = require('./lib/util');
const calc = require('./lib/calc');
const downloader = new Downloader();
// 最大尝试的绘制次数
const MAX_PAINT_COUNT = 5;
const ACTION_DEFAULT_SIZE = 24;
const ACTION_OFFSET = '2rpx';
Component({
canvasWidthInPx: 0,
canvasHeightInPx: 0,
canvasNode: null,
paintCount: 0,
currentPalette: {},
outterDisabled: false,
isDisabled: false,
needClear: false,
/**
* 组件的属性列表
*/
properties: {
use2D: {
type: Boolean,
},
customStyle: {
type: String,
},
// 运行自定义选择框和删除缩放按钮
customActionStyle: {
type: Object,
},
palette: {
type: Object,
observer: function (newVal, oldVal) {
if (this.isNeedRefresh(newVal, oldVal)) {
this.paintCount = 0;
clearPenCache();
this.startPaint();
}
},
},
dancePalette: {
type: Object,
observer: function (newVal, oldVal) {
if (!this.isEmpty(newVal) && !this.properties.use2D) {
clearPenCache();
this.initDancePalette(newVal);
}
},
},
// 缩放比,会在传入的 palette 中统一乘以该缩放比
scaleRatio: {
type: Number,
value: 1,
},
widthPixels: {
type: Number,
value: 0,
},
// 启用脏检查,默认 false
dirty: {
type: Boolean,
value: false,
},
LRU: {
type: Boolean,
value: false,
},
action: {
type: Object,
observer: function (newVal, oldVal) {
if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
this.doAction(newVal, null, false, true);
}
},
},
disableAction: {
type: Boolean,
observer: function (isDisabled) {
this.outterDisabled = isDisabled;
this.isDisabled = isDisabled;
},
},
clearActionBox: {
type: Boolean,
observer: function (needClear) {
if (needClear && !this.needClear) {
if (this.frontContext) {
setTimeout(() => {
this.frontContext.draw();
}, 100);
this.touchedView = {};
this.prevFindedIndex = this.findedIndex;
this.findedIndex = -1;
}
}
this.needClear = needClear;
},
},
},
data: {
picURL: '',
showCanvas: true,
painterStyle: '',
},
methods: {
/**
* 判断一个 object 是否为 空
* @param {object} object
*/
isEmpty(object) {
for (const i in object) {
return false;
}
return true;
},
isNeedRefresh(newVal, oldVal) {
if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
return false;
}
return true;
},
getBox(rect, type) {
const boxArea = {
type: 'rect',
css: {
height: `${rect.bottom - rect.top}px`,
width: `${rect.right - rect.left}px`,
left: `${rect.left}px`,
top: `${rect.top}px`,
borderWidth: '4rpx',
borderColor: '#1A7AF8',
color: 'transparent',
},
};
if (type === 'text') {
boxArea.css = Object.assign({}, boxArea.css, {
borderStyle: 'dashed',
});
}
if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border);
}
Object.assign(boxArea, {
id: 'box',
});
return boxArea;
},
getScaleIcon(rect, type) {
let scaleArea = {};
const { customActionStyle } = this.properties;
if (customActionStyle && customActionStyle.scale) {
scaleArea = {
type: 'image',
url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
},
};
} else {
scaleArea = {
type: 'rect',
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
color: '#0000ff',
},
};
}
scaleArea.css = Object.assign({}, scaleArea.css, {
align: 'center',
left: `${rect.right + ACTION_OFFSET.toPx()}px`,
top:
type === 'text'
? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
: `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`,
});
Object.assign(scaleArea, {
id: 'scale',
});
return scaleArea;
},
getDeleteIcon(rect) {
let deleteArea = {};
const { customActionStyle } = this.properties;
if (customActionStyle && customActionStyle.scale) {
deleteArea = {
type: 'image',
url: customActionStyle.delete.icon,
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
},
};
} else {
deleteArea = {
type: 'rect',
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
color: '#0000ff',
},
};
}
deleteArea.css = Object.assign({}, deleteArea.css, {
align: 'center',
left: `${rect.left - ACTION_OFFSET.toPx()}px`,
top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`,
});
Object.assign(deleteArea, {
id: 'delete',
});
return deleteArea;
},
doAction(action, callback, isMoving, overwrite) {
if (this.properties.use2D) {
return;
}
let newVal = null;
if (action) {
newVal = action.view;
}
if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
// 带 id 的动作给撤回时使用,不带 id表示对当前选中对象进行操作
const { views } = this.currentPalette;
for (let i = 0; i < views.length; i++) {
if (views[i].id === newVal.id) {
// 跨层回撤,需要重新构建三层关系
this.touchedView = views[i];
this.findedIndex = i;
this.sliceLayers();
break;
}
}
}
const doView = this.touchedView;
if (!doView || this.isEmpty(doView)) {
return;
}
if (newVal && newVal.css) {
if (overwrite) {
doView.css = newVal.css;
} else if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
doView.css = Object.assign({}, ...doView.css, ...newVal.css);
} else if (Array.isArray(doView.css)) {
doView.css = Object.assign({}, ...doView.css, newVal.css);
} else if (Array.isArray(newVal.css)) {
doView.css = Object.assign({}, doView.css, ...newVal.css);
} else {
doView.css = Object.assign({}, doView.css, newVal.css);
}
}
if (newVal && newVal.rect) {
doView.rect = newVal.rect;
}
if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
downloader
.download(newVal.url, this.properties.LRU)
.then(path => {
if (newVal.url.startsWith('https')) {
doView.originUrl = newVal.url;
}
doView.url = path;
wx.getImageInfo({
src: path,
success: res => {
doView.sHeight = res.height;
doView.sWidth = res.width;
this.reDraw(doView, callback, isMoving);
},
fail: () => {
this.reDraw(doView, callback, isMoving);
},
});
})
.catch(error => {
// 未下载成功,直接绘制
console.error(error);
this.reDraw(doView, callback, isMoving);
});
} else {
newVal && newVal.text && doView.text && newVal.text !== doView.text && (doView.text = newVal.text);
newVal &&
newVal.content &&
doView.content &&
newVal.content !== doView.content &&
(doView.content = newVal.content);
this.reDraw(doView, callback, isMoving);
}
},
reDraw(doView, callback, isMoving) {
const draw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(doView) ? [] : [doView],
};
const pen = new Pen(this.globalContext, draw);
pen.paint(callbackInfo => {
callback && callback(callbackInfo);
this.triggerEvent('viewUpdate', {
view: this.touchedView,
});
});
const { rect, css, type } = doView;
this.block = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)],
};
if (css && css.scalable) {
this.block.views.push(this.getScaleIcon(rect, type));
}
if (css && css.deletable) {
this.block.views.push(this.getDeleteIcon(rect));
}
const topBlock = new Pen(this.frontContext, this.block);
topBlock.paint();
},
isInView(x, y, rect) {
return x > rect.left && y > rect.top && x < rect.right && y < rect.bottom;
},
isInDelete(x, y) {
for (const view of this.block.views) {
if (view.id === 'delete') {
return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
}
}
return false;
},
isInScale(x, y) {
for (const view of this.block.views) {
if (view.id === 'scale') {
return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
}
}
return false;
},
touchedView: {},
findedIndex: -1,
onClick() {
const x = this.startX;
const y = this.startY;
const totalLayerCount = this.currentPalette.views.length;
let canBeTouched = [];
let isDelete = false;
let deleteIndex = -1;
for (let i = totalLayerCount - 1; i >= 0; i--) {
const view = this.currentPalette.views[i];
const { rect } = view;
if (this.touchedView && this.touchedView.id && this.touchedView.id === view.id && this.isInDelete(x, y, rect)) {
canBeTouched.length = 0;
deleteIndex = i;
isDelete = true;
break;
}
if (this.isInView(x, y, rect)) {
canBeTouched.push({
view,
index: i,
});
}
}
this.touchedView = {};
if (canBeTouched.length === 0) {
this.findedIndex = -1;
} else {
let i = 0;
const touchAble = canBeTouched.filter(item => Boolean(item.view.id));
if (touchAble.length === 0) {
this.findedIndex = canBeTouched[0].index;
} else {
for (i = 0; i < touchAble.length; i++) {
if (this.findedIndex === touchAble[i].index) {
i++;
break;
}
}
if (i === touchAble.length) {
i = 0;
}
this.touchedView = touchAble[i].view;
this.findedIndex = touchAble[i].index;
this.triggerEvent('viewClicked', {
view: this.touchedView,
});
}
}
if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
// 证明点击了背景 或无法移动的view
this.frontContext.draw();
if (isDelete) {
this.triggerEvent('touchEnd', {
view: this.currentPalette.views[deleteIndex],
index: deleteIndex,
type: 'delete',
});
this.doAction();
} else if (this.findedIndex < 0) {
this.triggerEvent('viewClicked', {});
}
this.findedIndex = -1;
this.prevFindedIndex = -1;
} else if (this.touchedView && this.touchedView.id) {
this.sliceLayers();
}
},
sliceLayers() {
const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex);
const topLayers = this.currentPalette.views.slice(this.findedIndex + 1);
const bottomDraw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
background: this.currentPalette.background,
views: bottomLayers,
};
const topDraw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: topLayers,
};
if (this.prevFindedIndex < this.findedIndex) {
new Pen(this.bottomContext, bottomDraw).paint();
this.doAction();
new Pen(this.topContext, topDraw).paint();
} else {
new Pen(this.topContext, topDraw).paint();
this.doAction();
new Pen(this.bottomContext, bottomDraw).paint();
}
this.prevFindedIndex = this.findedIndex;
},
startX: 0,
startY: 0,
startH: 0,
startW: 0,
isScale: false,
startTimeStamp: 0,
onTouchStart(event) {
if (this.isDisabled) {
return;
}
const { x, y } = event.touches[0];
this.startX = x;
this.startY = y;
this.startTimeStamp = new Date().getTime();
if (this.touchedView && !this.isEmpty(this.touchedView)) {
const { rect } = this.touchedView;
if (this.isInScale(x, y, rect)) {
this.isScale = true;
this.startH = rect.bottom - rect.top;
this.startW = rect.right - rect.left;
} else {
this.isScale = false;
}
} else {
this.isScale = false;
}
},
onTouchEnd(e) {
if (this.isDisabled) {
return;
}
const current = new Date().getTime();
if (current - this.startTimeStamp <= 500 && !this.hasMove) {
!this.isScale && this.onClick(e);
} else if (this.touchedView && !this.isEmpty(this.touchedView)) {
this.triggerEvent('touchEnd', {
view: this.touchedView,
});
}
this.hasMove = false;
},
onTouchCancel(e) {
if (this.isDisabled) {
return;
}
this.onTouchEnd(e);
},
hasMove: false,
onTouchMove(event) {
if (this.isDisabled) {
return;
}
this.hasMove = true;
if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
return;
}
const { x, y } = event.touches[0];
const offsetX = x - this.startX;
const offsetY = y - this.startY;
const { rect, type } = this.touchedView;
let css = {};
if (this.isScale) {
clearPenCache(this.touchedView.id);
const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1;
if (this.touchedView.css && this.touchedView.css.minWidth) {
if (newW < this.touchedView.css.minWidth.toPx()) {
return;
}
}
if (this.touchedView.rect && this.touchedView.rect.minWidth) {
if (newW < this.touchedView.rect.minWidth) {
return;
}
}
const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1;
css = {
width: `${newW}px`,
};
if (type !== 'text') {
if (type === 'image') {
css.height = `${(newW * this.startH) / this.startW}px`;
} else {
css.height = `${newH}px`;
}
}
} else {
this.startX = x;
this.startY = y;
css = {
left: `${rect.x + offsetX}px`,
top: `${rect.y + offsetY}px`,
right: undefined,
bottom: undefined,
};
}
this.doAction(
{
view: {
css,
},
},
null,
!this.isScale,
);
},
initScreenK() {
if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
try {
getApp().systemInfo = wx.getSystemInfoSync();
} catch (e) {
console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
return;
}
}
this.screenK = 0.5;
if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
this.screenK = getApp().systemInfo.screenWidth / 750;
}
setStringPrototype(this.screenK, this.properties.scaleRatio);
},
initDancePalette() {
if (this.properties.use2D) {
return;
}
this.isDisabled = true;
this.initScreenK();
this.downloadImages(this.properties.dancePalette).then(async palette => {
this.currentPalette = palette;
const { width, height } = palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
this.setData({
painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
});
this.frontContext || (this.frontContext = await this.getCanvasContext(this.properties.use2D, 'front'));
this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
this.isDisabled = false;
this.isDisabled = this.outterDisabled;
this.triggerEvent('didShow');
});
this.globalContext.draw();
this.frontContext.draw();
this.topContext.draw();
});
this.touchedView = {};
},
startPaint() {
this.initScreenK();
const { width, height } = this.properties.palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
let needScale = false;
// 生成图片时,根据设置的像素值重新绘制
if (width.toPx() !== this.canvasWidthInPx) {
this.canvasWidthInPx = width.toPx();
needScale = this.properties.use2D;
}
if (this.properties.widthPixels) {
setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx);
this.canvasWidthInPx = this.properties.widthPixels;
}
if (this.canvasHeightInPx !== height.toPx()) {
this.canvasHeightInPx = height.toPx();
needScale = needScale || this.properties.use2D;
}
this.setData(
{
photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
},
function () {
this.downloadImages(this.properties.palette).then(async palette => {
if (!this.photoContext) {
this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
}
if (needScale) {
const scale = getApp().systemInfo.pixelRatio;
this.photoContext.width = this.canvasWidthInPx * scale;
this.photoContext.height = this.canvasHeightInPx * scale;
this.photoContext.scale(scale, scale);
}
new Pen(this.photoContext, palette).paint(() => {
this.saveImgToLocal();
});
setStringPrototype(this.screenK, this.properties.scaleRatio);
});
},
);
},
downloadImages(palette) {
return new Promise((resolve, reject) => {
let preCount = 0;
let completeCount = 0;
const paletteCopy = JSON.parse(JSON.stringify(palette));
if (paletteCopy.background) {
preCount++;
downloader.download(paletteCopy.background, this.properties.LRU).then(
path => {
paletteCopy.background = path;
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
() => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
);
}
if (paletteCopy.views) {
for (const view of paletteCopy.views) {
if (view && view.type === 'image' && view.url) {
preCount++;
/* eslint-disable no-loop-func */
downloader.download(view.url, this.properties.LRU).then(
path => {
view.originUrl = view.url;
view.url = path;
wx.getImageInfo({
src: path,
success: res => {
// 获得一下图片信息,供后续裁减使用
view.sWidth = res.width;
view.sHeight = res.height;
},
fail: error => {
// 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
view.url = '';
},
complete: () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
});
},
() => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
);
}
}
}
if (preCount === 0) {
resolve(paletteCopy);
}
});
},
saveImgToLocal() {
const that = this;
setTimeout(() => {
wx.canvasToTempFilePath(
{
canvasId: 'photo',
canvas: that.properties.use2D ? that.canvasNode : null,
destWidth: that.canvasWidthInPx,
destHeight: that.canvasHeightInPx,
success: function (res) {
that.getImageInfo(res.tempFilePath);
},
fail: function (error) {
console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
that.triggerEvent('imgErr', {
error: error,
});
},
},
this,
);
}, 300);
},
getCanvasContext(use2D, id) {
const that = this;
return new Promise(resolve => {
if (use2D) {
const query = wx.createSelectorQuery().in(that);
const selectId = `#${id}`;
query
.select(selectId)
.fields({ node: true, size: true })
.exec(res => {
that.canvasNode = res[0].node;
const ctx = that.canvasNode.getContext('2d');
const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
resolve(wxCanvas);
});
} else {
const temp = wx.createCanvasContext(id, that);
resolve(new WxCanvas('mina', temp, id, true));
}
});
},
getImageInfo(filePath) {
const that = this;
wx.getImageInfo({
src: filePath,
success: infoRes => {
if (that.paintCount > MAX_PAINT_COUNT) {
const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
console.error(error);
that.triggerEvent('imgErr', {
error: error,
});
return;
}
// 比例相符时才证明绘制成功,否则进行强制重绘制
if (
Math.abs(
(infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) /
(infoRes.height * that.canvasHeightInPx),
) < 0.01
) {
that.triggerEvent('imgOK', {
path: filePath,
});
} else {
that.startPaint();
}
that.paintCount++;
},
fail: error => {
console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
that.triggerEvent('imgErr', {
error: error,
});
},
});
},
},
});
function setStringPrototype(screenK, scale) {
/* eslint-disable no-extend-native */
/**
* string 到对应的 px
* @param {Number} baseSize 当设置了 % 号时,设置的基准值
*/
String.prototype.toPx = function toPx(_, baseSize) {
if (this === '0') {
return 0;
}
const REG = /-?[0-9]+(\.[0-9]+)?(rpx|px|%)/;
const parsePx = origin => {
const results = new RegExp(REG).exec(origin);
if (!origin || !results) {
console.error(`The size: ${origin} is illegal`);
return 0;
}
const unit = results[2];
const value = parseFloat(origin);
let res = 0;
if (unit === 'rpx') {
res = Math.round(value * (screenK || 0.5) * (scale || 1));
} else if (unit === 'px') {
res = Math.round(value * (scale || 1));
} else if (unit === '%') {
res = Math.round((value * baseSize) / 100);
}
return res;
};
const formula = /^calc\((.+)\)$/.exec(this);
if (formula && formula[1]) {
// 进行 calc 计算
const afterOne = formula[1].replace(/([^\s\(\+\-\*\/]+)\.(left|right|bottom|top|width|height)/g, word => {
const [id, attr] = word.split('.');
return penCache.viewRect[id][attr];
});
const afterTwo = afterOne.replace(new RegExp(REG, 'g'), parsePx);
return calc(afterTwo);
} else {
return parsePx(this);
}
};
}

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,21 @@
<view style='position: relative;{{customStyle}};{{painterStyle}}'>
<block wx:if="{{!use2D}}">
<canvas canvas-id="photo" style="{{photoStyle}};position: absolute; left: -9999px; top: -9999rpx;" />
<block wx:if="{{dancePalette}}">
<canvas canvas-id="bottom" style="{{painterStyle}};position: absolute;" />
<canvas canvas-id="k-canvas" style="{{painterStyle}};position: absolute;" />
<canvas canvas-id="top" style="{{painterStyle}};position: absolute;" />
<canvas
canvas-id="front"
style="{{painterStyle}};position: absolute;"
bindtouchstart="onTouchStart"
bindtouchmove="onTouchMove"
bindtouchend="onTouchEnd"
bindtouchcancel="onTouchCancel"
disable-scroll="{{true}}" />
</block>
</block>
<block wx:if="{{use2D}}">
<canvas type="2d" id="photo" style="{{photoStyle}};" />
</block>
</view>