Compare commits

..

2 Commits

Author SHA1 Message Date
yuantao
f8348d80e9 iflow上下文调优 2025-11-05 16:26:01 +08:00
yuantao
65656f1810 新增:全量优化 2025-11-05 16:20:06 +08:00
39 changed files with 2536 additions and 4227 deletions

25
.env
View File

@@ -1,5 +1,20 @@
VITE_BASE_URL= #接口地址 # 接口地址
VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/ #资源地址 VITE_BASE_URL=
VITE_APPID=wx9cb717d8151d8486 #小程序APPID
VITE_UNI_APPID=_UNI_8842336 #UNI-APPID # 资源地址
VITE_LIBVERSION=3.0.0 #微信小程序基础库 VITE_ASSETSURL=https://cdn.vrupup.com/s/1598/assets/
# 小程序APPID
VITE_APPID=wx9cb717d8151d8486
# UNI-APPID
VITE_UNI_APPID=_UNI_8842336
# 调试模式
VITE_DEBUG=false
# API超时时间毫秒
VITE_API_TIMEOUT=10000
# 版本号
VITE_APP_VERSION=1.0.0

6
.eslintignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.hbuilderx
unpackage
.env*
!.env.example

56
.eslintrc.js Normal file
View File

@@ -0,0 +1,56 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es6: true,
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-essential',
'prettier',
'plugin:prettier/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
ecmaVersion: 2020,
sourceType: 'module',
},
plugins: ['vue', 'prettier'],
rules: {
'prettier/prettier': 'error',
'vue/multi-word-component-names': 'off',
'vue/no-unused-vars': 'error',
'vue/html-self-closing': 'off',
'vue/max-attributes-per-line': [
'error',
{
singleline: {
max: 3,
},
multiline: {
max: 1,
},
},
],
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-unused-vars': 'error',
'no-undef': 'error',
'no-var': 'error',
'prefer-const': 'error',
'object-shorthand': 'error',
'quote-props': ['error', 'as-needed'],
'array-callback-return': 'error',
'prefer-arrow-callback': 'error',
'arrow-parens': ['error', 'as-needed'],
'arrow-spacing': 'error',
'no-duplicate-imports': 'error',
},
globals: {
uni: 'readonly',
getApp: 'readonly',
getCurrentPages: 'readonly',
plus: 'readonly',
},
}

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
dist
.hbuilderx
unpackage
.env*
!.env.example

37
.prettierrc.js Normal file
View File

@@ -0,0 +1,37 @@
module.exports = {
// 一行最多 100 字符
printWidth: 100,
// 使用 2 个空格缩进
tabWidth: 2,
// 不使用缩进符,而使用空格
useTabs: false,
// 行尾需要有分号
semi: false,
// 使用单引号
singleQuote: true,
// 对象的 key 仅在必要时用引号
quoteProps: 'as-needed',
// jsx 不使用单引号,而使用双引号
jsxSingleQuote: false,
// 末尾不需要逗号
trailingComma: 'none',
// 大括号内的首尾需要空格
bracketSpacing: true,
// jsx 标签的反尖括号需要换行
bracketSameLine: false,
// 箭头函数,只有一个参数的时候,也需要括号
arrowParens: 'avoid',
// 每个文件格式化的范围是文件的全部内容
rangeStart: 0,
rangeEnd: Infinity,
// 不需要写文件开头的 @prettier
requirePragma: false,
// 不需要自动在文件开头插入 @prettier
insertPragma: false,
// 使用默认的折行标准
proseWrap: 'preserve',
// 根据显示样式决定 html 要不要折行
htmlWhitespaceSensitivity: 'css',
// 换行符使用 lf
endOfLine: 'lf',
}

View File

@@ -12,5 +12,6 @@ export default {
@import "./uni.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 '@/common/styles/dark-mode.scss';
@import '@/uview-plus/index.scss'; @import '@/uview-plus/index.scss';
</style> </style>

353
IFLOW.md
View File

@@ -1,33 +1,41 @@
# 项目概述 - IFLOW 上下文 # 项目概述 - IFLOW 上下文
这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数,方便快速开发微信小程序。 这是一个基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板。它提供了一个基础的项目结构和一些常用的工具函数、自定义Hooks、组件等,方便快速开发微信小程序。
## 技术栈 ## 技术栈
* **UniApp**: 跨平台开发框架,用于构建微信小程序。 * **UniApp**: 跨平台开发框架,用于构建微信小程序。
* **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面。 * **Vue3**: 渐进式 JavaScript 框架,用于构建用户界面,采用 Composition API 和 setup 语法糖
* **uView-Plus**: 基于 UniApp 的 UI 组件库。 * **uView-Plus**: 基于 UniApp 的 UI 组件库。
* **z-paging**: 一个用于处理分页加载的组件库。 * **z-paging**: 一个用于处理分页加载的组件库。
* **Vuex**: 状态管理库,用于统一管理应用状态 * **luch-request**: 网络请求库,用于封装 HTTP 请求
* **Vite**: 现代化构建工具配置了自定义插件用于manifest.json的appid替换 * **ESLint & Prettier**: 代码规范和格式化工具
* **luch-request**: 网络请求库,提供请求封装和拦截器功能。
## 目录结构 ## 目录结构
``` ```
. .
├── api/ # 接口相关 ├── api/ # 接口相关
│ ├── modules/ # 业务接口(目录存在待完善) │ ├── modules/ # 业务接口模块
│ ├── index.js # API统一导出入口
│ └── request.js # 请求封装 │ └── request.js # 请求封装
├── common/ # 公共资源 ├── common/ # 公共资源
│ ├── constants/ # 常量定义
│ ├── styles/ # 全局样式 │ ├── styles/ # 全局样式
│ │ ├── common.css # codefun原子类样式(预留) │ │ ├── common.css # codefun原子类样式
│ │ ── base.scss # 全局样式变量(预留) │ │ ── base.scss # 全局样式变量和工具类
│ │ └── dark-mode.scss # 暗黑模式样式
│ └── utils/ # 工具函数 │ └── utils/ # 工具函数
── tool.js # 常用工具函数 ── tool.js # 常用工具函数
├── components/ # 公共组件(目录存在待完善) │ └── env.js # 环境变量工具
├── wxcomponents/ # 微信原生组件 ├── components/ # 公共组件
│ └── painter/ # 海报绘制组件 │ └── common/ # 通用业务组件
├── directives/ # 自定义指令
├── hooks/ # Vue Composition API Hooks
│ ├── index.js # Hooks统一导出入口
│ ├── useRequest.js # 通用请求Hook
│ ├── useApi.js # API相关Hook
│ └── useState.js # 状态管理Hook
├── uni_modules/ # uni-app 组件 ├── uni_modules/ # uni-app 组件
│ └── z-paging/ # 分页组件库 │ └── z-paging/ # 分页组件库
├── lib/ # 第三方库 ├── lib/ # 第三方库
@@ -36,12 +44,10 @@
├── mixins/ # Vue 混入 ├── mixins/ # Vue 混入
│ └── global.js # 全局混入 │ └── global.js # 全局混入
├── pages/ # 主包页面 ├── pages/ # 主包页面
│ └── index/ # 首页
├── subPages/ # 分包页面
│ └── welcome/ # 欢迎页
├── static/ # 静态资源文件 ├── static/ # 静态资源文件
│ └── assets/ # 静态图片资源 │ └── assets/ # 静态图片资源
├── store/ # 状态管理 ├── store/ # 状态管理
├── subPages/ # 分包页面
├── App.vue # 应用入口 ├── App.vue # 应用入口
├── main.js # 主入口文件 ├── main.js # 主入口文件
├── pages.json # 页面配置 ├── pages.json # 页面配置
@@ -50,258 +56,161 @@
├── vite.config.js # Vite 编译配置 ├── vite.config.js # Vite 编译配置
├── .nvmdrc # Node.js 版本要求 ├── .nvmdrc # Node.js 版本要求
├── .env # 环境变量 ├── .env # 环境变量
├── .iflow/ # iFlow CLI 配置 ├── .eslintrc.js # ESLint 配置
├── package.json # 项目依赖配置 ├── .prettierrc.js # Prettier 配置
── uni.promisify.adaptor.js # uni-app promise 适配器 ── .eslintignore # ESLint 忽略文件
└── .prettierignore # Prettier 忽略文件
``` ```
## 开发环境与运行 # 开发环境与运行
### 环境要求 ## 环境要求
* Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 24.0.1) * Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 20.0.0)
* 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 来运行和调试: ```bash
# 开发环境运行
npm run dev
1. 使用 HBuilderX 打开项目 # 构建生产环境
2. 选择"运行到小程序模拟器"或"运行到手机或模拟器" npm run build
3. 选择微信开发者工具
### 构建项目 # 预览构建结果
npm run preview
同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。 # 代码检查和修复
npm run lint
## 代码规范与开发约定 # 代码格式化
npm run format
## 样式规范
* `uni.scss` 提供了全局样式变量
* **全局样式**: 位于 `common/styles/` 目录
* `common.css`: CodeFun原子类样式用于快速布局
* `base.scss`: SCSS变量、Mixins和常用样式类生成器
* **样式规范**: 遵循项目中已有的样式风格,统一使用原子类
## JavaScript规范
* **ES6+**: 严格遵循ES6+语法规范
* **函数式编程**: 优先使用函数式编程范式
* **函数定义**: 使用 `function` 关键字定义方法函数
* **响应式数据**:
* 少于4个 `ref` 时直接使用 `ref`
* 超过4个时使用 `reactive` 进行封装
* **生命周期**: 通过 `@dcloudio/uni-app` 按需导入,如:
```javascript
import { onLoad, onShow, onHide } from '@dcloudio/uni-app'
``` ```
* **变量命名**:
* 小驼峰命名法: `userName`, `orderList` ## 构建项目
* 常量全大写: `MAX_LENGTH`, `DEFAULT_VALUE`
* 状态类变量: `isLogin`, `isOpen`, `isLoading` 构建项目使用 Vite 工具链:
* 事件方法: `onClick`, `onSelect`, `onSubmit`
* **注释规范**: 变量和方法必须有注释说明和类型说明 ```bash
* **异步处理**: 所有Promise类方法使用 `async/await` 写法,避免 `.then` 嵌套 npm run build
* **字符串拼接**: 使用ES6模板语法 `` `string ${variable}` `` ```
# 代码规范与开发约定
## 样式
* 全局样式文件位于 `common/styles/` 目录下,包括 `common.css``base.scss`
* `common.css` 提供了codefun原子类样式用于快速布局。
* `base.scss` 提供了SCSS变量和mixins以及常用的样式类生成器。
* 支持暗黑模式,通过 `dark-mode.scss` 实现。
* 样式规范应遵循项目中已有的风格。
## JavaScript
* 严格遵循ES6+规范。
* 遵循JavaScript函数式编程范式。
* 方法类函数应该使用 `function` 进行定义。
* 避免出现超过4个以上的 `ref`超过4个则使用 `reactive`
* 页面的生命周期需要通过 `@dcloudio/uni-app` 依赖进行按需导入,如(`import { onLoad } from '@dcloudio/uni-app'`)。
* 全局变量都集中放置于代码顶部。
* 变量名使用小驼峰命名法。
* 常量名使用全大写。
* 状态类变量命名参考 `isLogin``isOpen`
* 事件类方法命名参考 `onClick``onSelect`
* 变量都应该写有注释说明、类型说明。
* 所有 `Promise` 类方法使用 `async` `await` 写法,避免出现 `.then` 嵌套,并进行容错、错误抛出处理。
* 在需要页面跳转、提示、加载、本地存储、或其他功能的时候,优先使用工具函数 `common/utils/tool.js` 中存在的函数。
* 字符串拼接使用ES6的模板语法。
* 组件使用 Vue3 的 Composition API (setup语法糖) 编写。
## 静态资源 ## 静态资源
* **资源路径**: 使用全局混入的 `ASSETSURL` 变量 * 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `<template></template>` 中直接使用。
* **使用方式**: * 所有静态资源URL应该使用 `ASSETSURL` 进行拼接,使用方式为:`${ASSETSURL}simple.png``background-image: url($ASSETSURL + 'b23bbf0c4c8e59f88f8fd883cb5d6b27.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`
* `alert()`: 文字轻提示 * **页面跳转**: `navigateTo`, `redirectTo`, `reLaunch`, `switchTab`, `navigateBack`
* `loading()`: 显示加载状态 * **本地存储**: `storage`, `removeStorage`, `getStorageInfo`
* `hideLoading()`: 隐藏加载状态 * **其他功能**: `copy` (复制文本), `saveImageToPhotos` (保存图片), `requestPayment` (微信支付), `upload` (文件上传), `loadFont` (加载字体)
* **日期处理**: `formatDate` (日期格式化)
* **工具函数**: `deepClone` (深拷贝), `debounce/throttle` (防抖节流)
* **表单验证**: `getValidator` (表单验证工具)
* **主题切换**: `toggleDarkMode`, `getCurrentTheme` (暗黑模式切换)
* **权限管理**: `hasPermission`, `setPermissions`, `getUserPermissions` (权限检查和管理)
### 页面跳转 ## 网络请求
* `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` 下,并将单个接口进行导出以便页面按需导入。
* 提供 `useGet``usePost` 等 Hooks 用于在组件中便捷地发起请求。
### 状态管理 (Vuex) ## 自定义Hooks
* 项目集成了Vuex进行全局状态管理。 项目提供了多个可复用的 Vue Composition API Hooks位于 `hooks/` 目录下:
* 状态管理文件位于 `store/index.js`。
* 使用 `createStore` 创建store实例。
* 目前处于基础状态包含空的state、mutations、actions和getters。
## 组件规范 * `useRequest`: 通用请求Hook支持自动执行、手动执行、loading状态等
* `useGet`: GET请求Hook
* `usePost`: POST请求Hook
* `useState`: 状态管理Hook
* `useStorageState`: 本地存储状态Hook
* `useFormState`: 表单状态Hook
* `uni_modules` 目录中的组件无需导入直接可以进行使用。 ## 组件
* **组件库**: 集成 `uView-Plus` 和 `z-paging`
* **自动导入**: `uView-Plus` 通过 `easycom` 自动导入
```javascript
// 直接使用无需import
<u-button>按钮</u-button>
<u-icon name="home" />
```
* **组件分类**:
* **全局组件**: 放在 `components/` 目录
* **页面组件**: 放在页面根目录的 `components/` 目录
* **微信原生组件**: 放在页面根目录的 `wxcomponents/` 目录
## 分页功能 * 项目集成了 `uView-Plus``z-paging` 两个组件库。
* 所有 `uni_modules` 目录中的组件无需导入直接可以进行使用。
* `uView-Plus` 组件已通过 `easycom` 自动导入,可以直接使用,如:`<u-button>``<u-icon>`
* 自定义公共组件放在 `components/common/` 目录下,采用 Vue3 的 setup 语法糖编写。
* 页面独立组件放在页面根目录下的 `components/`
* 微信的原生组件放在页面根目录下的 `wxcomponents/`,并在使用了组件的对应页面路由配置中添加组件的引用属性 `"usingComponents": { "components": "/wxcomponents/components/components" }`
* 组件编写应遵循项目中已有的风格。
* **分页组件**: 使用 `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> * 项目支持自定义指令,位于 `directives/` 目录下
import { ref } from 'vue' * 包括防重复点击、长按、权限控制、拖拽等指令
*`main.js` 中已全局注册
const paging = ref(null) ## 常量管理
const dataList = ref([])
// 分页查询函数 * 项目常量统一管理在 `common/constants/` 目录下
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>
```
### 路由配置 * 环境变量通过 `.env` 文件配置
* **主包页面**: 放在 `pages/` 目录 * 提供 `common/utils/env.js` 工具进行环境变量验证和获取
* **分包页面**: 放在 `subPages/` 目录
* **TabBar配置**: 在 `pages.json` 中配置底部导航
### Vite 配置 ## 页面
* `vite.config.js` 包含了自定义插件用于在编译时替换 `manifest.json` 中的 appid * 页面配置在 `pages.json` 中管理。
* 仅在首次编译时执行,避免热重载时重复执行导致内存占用高 * 主包页面放在 `pages/` 目录下,分包页面放在 `subPages/` 目录下,如果页面不属于一级页面且没有包含在 `pages.json` 中的 `tabbar`,则应该放置在分包目录下。
* 支持环境变量配置: * 页面使用 Composition API (setup语法糖) 编写。
* `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="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进行列表优化
* 合理使用图片懒加载
* 避免不必要的重复渲染

205
README.md
View File

@@ -1,80 +1,159 @@
# <EFBFBD><EFBFBD>Ŀģ<EFBFBD><EFBFBD>ʹ<EFBFBD><EFBFBD>˵<EFBFBD><EFBFBD> # UniApp 微信小程序快速开发模板
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģ<EFBFBD><EFBFBD><EFBFBD>ǻ<EFBFBD><EFBFBD><EFBFBD> UniApp + Vue3 + uView-Plus <EFBFBD><EFBFBD>С<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀģ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һЩ<EFBFBD><EFBFBD><EFBFBD>õ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>á<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>͹<EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 本模板是基于 UniApp + Vue3 + uView-Plus 的微信小程序项目模板,提供了一些常用的工具函数、组件和最佳实践。
## Ŀ¼<EFBFBD> ## 目录结构
``` ```
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> api/ # <EFBFBD>ӿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── api/ # 接口相关
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> modules/ # ҵ<EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD> │ ├── modules/ # 业务接口
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> request.js # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ │ └── request.js # 请求封装
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ ├── common/ # 公共资源
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> styles/ # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ │ ├── styles/ # 全局样式
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> common.css # codefunԭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ ├── common.css # codefun原子类样式
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> base.scss # ȫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> └── base.scss # 全局样式变量
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> utils/ # <EFBFBD><EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD> │ └── utils/ # 工具函数
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> tool.ts # <EFBFBD><EFBFBD><EFBFBD>ù<EFBFBD><EFBFBD>ߺ<EFBFBD><EFBFBD><EFBFBD> └── tool.js # 常用工具函数
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> components/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── components/ # 公共组件
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni_modules/ # uni-app <EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── uni_modules/ # uni-app 组件
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> z-paging/ # <EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> │ └── z-paging/ # 分页组件库
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> lib/ # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── lib/ # 第三方库
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> luch-request/ # luch-request <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> │ └── luch-request/ # luch-request 网络请求库
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uview-plus/ # uView-Plus <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── uview-plus/ # uView-Plus 组件库
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> mixins/ # Vue <EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── mixins/ # Vue 混入
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> global.ts # ȫ<EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD> │ └── global.js # 全局混入
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> store/ # ״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── store/ # 状态管理
<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> index.ts # Vuex store ├── pages/ # 主包页面
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages/ # <20><><EFBFBD><EFBFBD>ҳ<EFBFBD><D2B3> ├── subPages/ # 分包页面
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> subPages/ # <EFBFBD>ְ<EFBFBD>ҳ<EFBFBD><EFBFBD> ├── App.vue # 应用入口
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> App.vue # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── main.js # 主入口文件
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> main.js # <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD> ├── pages.json # 页面配置
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> pages.json # ҳ<><D2B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── manifest.json # 应用配置
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> manifest.json # Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── uni.scss # 全局样式变量
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> uni.scss # ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD> ├── vite.config.js # Vite 编译配置
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> vite.config.js # Vite <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ├── .nvmdrc # Node.js 版本要求
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .nvmdrc # Node.js <20>汾Ҫ<E6B1BE><D2AA> └── .env # 环境变量
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .env # <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
``` ```
## ʹ<EFBFBD>÷<EFBFBD><EFBFBD><EFBFBD> ## 使用方法
1. <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģ<EFBFBD><EFBFBD>Ŀ¼<EFBFBD><EFBFBD><EFBFBD>Ƶ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ĿĿ¼<EFBFBD><EFBFBD> 1. 将模板目录复制到你的项目目录中
2. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>޸<EFBFBD> package.json <EFBFBD>е<EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><EFBFBD><EFBFBD>ƺ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 2. 根据需要修改 package.json 中的项目名称和描述
3. ʹ<EFBFBD><EFBFBD> npm install <EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> 3. 使用 npm install 安装依赖
4. <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD>޸<EFBFBD> pages.json <EFBFBD>е<EFBFBD>ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 4. 根据需要修改 pages.json 中的页面配置
5. <EFBFBD><EFBFBD>ʼ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¹<EFBFBD><EFBFBD><EFBFBD> 5. 开始构建你的新项目
## <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD> ## 开发环境与运行
### <EFBFBD><EFBFBD>Ҫ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ### 环境要求
- **dotenv** - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>
### <20><>ʽ * Node.js (版本信息在 `.nvmdrc` 文件中指定,当前为 20.0.0)
* npm 或 yarn
- common.css: ȫ<>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD>ʽ ### 安装依赖
- base.scss: <20><><EFBFBD>õ<EFBFBD> SCSS <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
### <20><><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD> (tool.js) ```bash
npm install
```
- alert: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ ### 运行项目
- loading/hideLoading: <20><>ʾ/<2F><><EFBFBD>ؼ<EFBFBD><D8BC><EFBFBD><EFBFBD><EFBFBD>ʾ
- ҳ<><D2B3><EFBFBD><EFBFBD>ת<EFBFBD><D7AA><EFBFBD>ط<EFBFBD><D8B7><EFBFBD>: navigateTo, redirectTo, reLaunch, switchTab, navigateBack
- <20><><EFBFBD>ػ<EFBFBD><D8BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: storage, removeStorage, getStorageInfo
- copy: <20><><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- saveImageToPhotos: <20><><EFBFBD><EFBFBD>ͼƬ<CDBC><C6AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- requestPayment: ΢<><CEA2>֧<EFBFBD><D6A7>
- upload: <20>ļ<EFBFBD><C4BC>ϴ<EFBFBD>
### <20><><EFBFBD><EFBFBD> 由于这是一个 UniApp 项目,通常需要使用 HBuilderX 或其他支持 UniApp 的 IDE 来运行和调试。具体的运行命令取决于你的开发环境。
- App.vue: ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ### 构建项目
- main.js: Vue Ӧ<>ó<EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>ȫ<EFBFBD>ֲ<EFBFBD><D6B2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- pages.json: ҳ<><D2B3>·<EFBFBD>ɺʹ<C9BA><CDB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
- uni.scss: ȫ<><C8AB><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>
## ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 同样,构建项目也需要使用 HBuilderX 或相应的 CLI 工具。
1. <20><><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ## 项目特性
2. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD>޸Ļ<DEB8><C4BB><EFBFBD>չ<EFBFBD><D5B9><EFBFBD>ߺ<EFBFBD><DFBA><EFBFBD>
3. <20><><EFBFBD><EFBFBD><EFBFBD>ɸ<EFBFBD><C9B8><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD><EFBFBD>޸Ļ<DEB8><C4BB> ### 核心依赖
4. <20><>ʽ<EFBFBD>ļ<EFBFBD><C4BC>ɸ<EFBFBD><C9B8><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD>ƹ淶<C6B9><E6B7B6><EFBFBD>е<EFBFBD><D0B5><EFBFBD> - **dotenv** - 环境变量注入
### 样式系统
- common.css: 全局原子类样式
- base.scss: 实用的 SCSS 工具类
### 工具函数 (tool.js)
- alert: 文字轻提示
- loading/hideLoading: 显示/隐藏加载提示
- 页面跳转方法: navigateTo, redirectTo, reLaunch, switchTab, navigateBack
- 本地存储: storage, removeStorage, getStorageInfo
- copy: 复制文本到剪贴板
- saveImageToPhotos: 保存图片到相册
- requestPayment: 微信支付
- upload: 文件上传
- formatDate: 日期格式化
- deepClone: 深拷贝
- debounce/throttle: 防抖节流
- getValidator: 表单验证工具
- toggleDarkMode/getCurrentTheme: 暗黑模式切换
### 网络请求
- 基于 luch-request 的封装
- 自动添加 token
- 统一错误处理
- 请求缓存机制
- 请求重试机制
### 组件
- uView-Plus 组件库
- z-paging 分页组件
## 代码规范与开发约定
### 样式
- 全局样式文件位于 `common/styles/` 目录下
- 遵循项目中已有的风格
### JavaScript
- 严格遵循ES6规范
- 遵循JavaScript函数式编程范式
- 方法类函数应该使用 `function` 进行定义
- 页面的生命周期需要通过 `@dcloudio/uni-app` 依赖进行按需导入
- 全局变量都集中放置于代码顶部
- 变量名使用小驼峰命名法
- 常量名使用全大写
- 所有 `Promise` 类方法使用 `async` `await` 写法
- 在需要页面跳转、提示、加载、本地存储等功能的时候,优先使用工具函数
- 字符串拼接使用ES6的模板语法
### 静态资源
- 静态资源变量 `ASSETSURL` 已进行全局混入,可以在 `<template></template>` 中直接使用
### 网络请求
- 网络请求使用 `lib/luch-request` 库进行封装
- 包含请求和响应拦截器,用于处理通用逻辑
- 各业务板块的接口都应存放在 `api/modules`
### 组件
- 所有 `uni_modules` 目录中的组件无需导入直接可以进行使用
- `uView-Plus` 组件已通过 `easycom` 自动导入
### 页面
- 页面配置在 `pages.json` 中管理
- 页面使用 Composition API (setup语法糖) 编写
## 代码提交规范
- 提交信息应清晰描述变更内容
- 对于功能性的新增或修改,使用 `新增` 前缀
- 对于错误修复,使用 `修复` 前缀
- 对于性能优化、代码重构,使用 `优化` 前缀
- 对于文档更新,使用 `文档` 前缀
- 提交信息应使用中文
## 注意事项
1. 请根据实际项目需求进行修改和扩展功能
2. 代码文件可按项目规范进行修改替换
3. 样式文件可按项目规范进行修改

15
api/index.js Normal file
View File

@@ -0,0 +1,15 @@
/**
* API统一导出入口
*/
// 导入网络请求实例
import http from './request.js'
// 导入各业务模块
import user from './modules/user.js'
// 导出网络请求实例
export { http }
// 导出各业务模块
export { user }
// 默认导出包含所有模块的对象
export default {
user,
}

85
api/modules/user.js Normal file
View File

@@ -0,0 +1,85 @@
import http from '@/api/request.js'
/**
* 用户相关API模块
*/
export default {
/**
* 用户登录
* @param {Object} data 登录数据
* @returns {Promise}
*/
login(data) {
return http.post('/user/login', data)
},
/**
* 用户注册
* @param {Object} data 注册数据
* @returns {Promise}
*/
register(data) {
return http.post('/user/register', data)
},
/**
* 获取用户信息
* @returns {Promise}
*/
getUserInfo() {
return http.get('/user/info')
},
/**
* 更新用户信息
* @param {Object} data 用户信息数据
* @returns {Promise}
*/
updateUserInfo(data) {
return http.put('/user/info', data)
},
/**
* 修改密码
* @param {Object} data 密码数据
* @returns {Promise}
*/
changePassword(data) {
return http.post('/user/change-password', data)
},
/**
* 退出登录
* @returns {Promise}
*/
logout() {
return http.post('/user/logout')
},
/**
* 获取用户列表
* @param {Object} params 查询参数
* @returns {Promise}
*/
getUserList(params) {
return http.get('/user/list', { params })
},
/**
* 获取用户详情
* @param {number} userId 用户ID
* @returns {Promise}
*/
getUserDetail(userId) {
return http.get(`/user/${userId}`)
},
/**
* 删除用户
* @param {number} userId 用户ID
* @returns {Promise}
*/
deleteUser(userId) {
return http.delete(`/user/${userId}`)
},
}

View File

@@ -1,9 +1,46 @@
import Request from '@/lib/luch-request/index.js' import Request from '@/lib/luch-request/index.js'
import tool from '@/common/utils/tool.js'
const baseUrl = import.meta.env.VITE_BASE_URL import EnvConfig from '@/common/utils/env.js'
const baseUrl = EnvConfig.BASE_URL
const http = new Request()
// 缓存存储
const cache = new Map()
// 缓存过期时间毫秒默认5分钟
const CACHE_EXPIRATION = 5 * 60 * 1000
// 缓存工具方法
const cacheUtils = {
// 生成缓存key
generateKey(config) {
return `${config.method}:${config.url}:${JSON.stringify(config.data || {})}:${JSON.stringify(config.params || {})}`
},
// 获取缓存
get(key) {
const cached = cache.get(key)
if (!cached) return null
// 检查是否过期
if (Date.now() - cached.timestamp > CACHE_EXPIRATION) {
cache.delete(key)
return null
}
return cached.data
},
// 设置缓存
set(key, data) {
cache.set(key, {
data,
timestamp: Date.now(),
})
},
// 清除缓存
clear() {
cache.clear()
},
// 清除指定缓存 // 清除指定缓存
delete(key) { delete(key) {
cache.delete(key) cache.delete(key)
@@ -11,31 +48,115 @@ http.setConfig(config => {
} }
/* 设置全局配置 */ /* 设置全局配置 */
http.setConfig(config => { http.setConfig(config => {
config.header = { ...config.header } config.header = { ...config.header }
config.sslVerify = false config.sslVerify = false
config.baseURL = baseUrl config.baseURL = baseUrl
return config
})
http.interceptors.request.use(
async config => {
config.header = { ...config.header }
// 自动添加token
const token = uni.getStorageSync('token')
if (token) {
config.header.Authorization = `Bearer ${token}`
}
// 添加通用请求头
config.header['Content-Type'] = 'application/json'
// 检查缓存
if (config.method === 'GET' && config.cache !== false) {
const cacheKey = cacheUtils.generateKey(config)
const cachedData = cacheUtils.get(cacheKey)
if (cachedData) {
// 如果有缓存,直接返回缓存数据
return Promise.resolve({
data: cachedData, data: cachedData,
statusCode: 200, statusCode: 200,
}) })
} }
} }
return config return config
},
http.interceptors.response.use(response => { config => {
if (response.statusCode == 500 || response.statusCode == 404 || response.statusCode == 403) { return Promise.reject(config)
console.error(response) }
return tool.alert('网络错误,请稍后重试') )
http.interceptors.response.use(
response => {
// 网络错误处理 // 网络错误处理
if (response.statusCode >= 500) {
if (response.statusCode == 401 || response.data.code == 401) { console.error('服务器错误:', response)
tool.alert('服务器错误,请稍后重试')
return Promise.reject(response)
}
if (response.statusCode === 404) {
console.error('接口不存在:', response)
tool.alert('接口不存在')
return Promise.reject(response)
}
if (response.statusCode === 403) {
console.error('无权限访问:', response)
tool.alert('无权限访问')
return Promise.reject(response)
}
// 未授权处理
if (response.statusCode === 401 || response.data?.code === 401) {
console.error('未授权或登录过期:', response)
tool.alert('登录已过期,请重新登录')
// 清除本地token
uni.removeStorageSync('token')
// 跳转到登录页
setTimeout(() => {
uni.reLaunch({ url: '/pages/login/login' }) // 根据实际登录页路径调整
}, 1500)
return Promise.reject(response)
}
// 业务错误处理
if (response.data && response.data.code !== undefined && response.data.code !== 200) {
const message = response.data.message || response.data.msg || '业务错误'
console.error('业务错误:', message)
tool.alert(message)
return Promise.reject(response)
}
// 成功响应
if (response.statusCode === 200) { if (response.statusCode === 200) {
if (response.statusCode == 200) {
// 缓存GET请求的响应数据 // 缓存GET请求的响应数据
if (response.config && response.config.method === 'GET' && response.config.cache !== false) { if (response.config && response.config.method === 'GET' && response.config.cache !== false) {
const cacheKey = cacheUtils.generateKey(response.config) const cacheKey = cacheUtils.generateKey(response.config)
cacheUtils.set(cacheKey, response.data) cacheUtils.set(cacheKey, response.data)
}
return Promise.resolve(response)
} else {
return Promise.reject(response)
}
},
error => {
console.error('请求失败:', error)
if (error.errMsg) {
// 网络错误
tool.alert('网络连接失败,请检查网络')
} else {
tool.alert('请求失败,请稍后重试')
}
return Promise.reject(error)
}
)
// 添加缓存功能到http实例
http.cache = cacheUtils
// 添加重试方法
http.retry = async function (config, retries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
const attempt = async retryCount => {
try {
const response = await this.request(config)
resolve(response)
} catch (error) {
if (retryCount <= 0) {
reject(error)
} else {
// 延迟后重试
setTimeout(() => {
attempt(retryCount - 1)
}, delay) }, delay)
}
} }

30
common/constants/app.js Normal file
View File

@@ -0,0 +1,30 @@
/**
* 应用相关常量
*/
// 应用信息
export const APP_NAME = '小程序模板'
export const APP_VERSION = '1.0.0'
// 页面路径常量
export const PAGE_PATHS = {
INDEX: '/pages/index/index',
WELCOME: '/subPages/welcome/index',
LOGIN: '/pages/login/login', // 示例路径
REGISTER: '/pages/register/register' // 示例路径
}
// 存储键名常量
export const STORAGE_KEYS = {
TOKEN: 'token',
USER_INFO: 'userInfo',
THEME: 'theme',
LANG: 'language'
}
// 事件常量
export const EVENTS = {
USER_LOGIN: 'userLogin',
USER_LOGOUT: 'userLogout',
THEME_CHANGE: 'themeChange'
}

View File

@@ -1,8 +1,6 @@
// 定义内外边距,历遍1-100 // 定义内外边距,只生成常用的数值
@for $i from 0 through 100 { $spacing-sizes: (0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 100);
// 只要双数和能被5除尽的数 @each $i in $spacing-sizes {
@if $i % 2==0 or $i % 5==0 {
// 得出u-margin-30或者u-m-30
.w-#{$i} { .w-#{$i} {
width: calc($i * 1%) !important; width: calc($i * 1%) !important;
} }
@@ -10,76 +8,60 @@
padding-left: $i + rpx !important; padding-left: $i + rpx !important;
padding-right: $i + rpx !important; padding-right: $i + rpx !important;
} }
.p-y-#{$i} { .p-y-#{$i} {
padding-top: $i + rpx !important; padding-top: $i + rpx !important;
padding-bottom: $i + rpx !important; padding-bottom: $i + rpx !important;
} }
.m-x-#{$i} { .m-x-#{$i} {
margin-left: $i + rpx !important; margin-left: $i + rpx !important;
margin-right: $i + rpx !important; margin-right: $i + rpx !important;
} }
.m-y-#{$i} { .m-y-#{$i} {
margin-top: $i + rpx !important; margin-top: $i + rpx !important;
margin-bottom: $i + rpx !important; margin-bottom: $i + rpx !important;
} }
.m-#{$i} { .m-#{$i} {
margin-left: $i + rpx !important; margin-left: $i + rpx !important;
margin-right: $i + rpx !important; margin-right: $i + rpx !important;
margin-top: $i + rpx !important; margin-top: $i + rpx !important;
margin-bottom: $i + rpx !important; margin-bottom: $i + rpx !important;
} }
.p-#{$i} { .p-#{$i} {
padding-left: $i + rpx !important; padding-left: $i + rpx !important;
padding-right: $i + rpx !important; padding-right: $i + rpx !important;
padding-top: $i + rpx !important; padding-top: $i + rpx !important;
padding-bottom: $i + rpx !important; padding-bottom: $i + rpx !important;
} }
.m-l-#{$i} { .m-l-#{$i} {
margin-left: $i + rpx !important; margin-left: $i + rpx !important;
} }
.m-t-#{$i} { .m-t-#{$i} {
margin-top: $i + rpx !important; margin-top: $i + rpx !important;
} }
.m-r-#{$i} { .m-r-#{$i} {
margin-right: $i + rpx !important; margin-right: $i + rpx !important;
} }
.m-b-#{$i} { .m-b-#{$i} {
margin-bottom: $i + rpx !important; margin-bottom: $i + rpx !important;
} }
.p-l-#{$i} { .p-l-#{$i} {
padding-left: $i + rpx !important; padding-left: $i + rpx !important;
} }
.p-t-#{$i} { .p-t-#{$i} {
padding-top: $i + rpx !important; padding-top: $i + rpx !important;
} }
.p-r-#{$i} { .p-r-#{$i} {
padding-right: $i + rpx !important; padding-right: $i + rpx !important;
} }
.p-b-#{$i} { .p-b-#{$i} {
padding-bottom: $i + rpx !important; padding-bottom: $i + rpx !important;
} }
.l-p-#{$i} { .l-p-#{$i} {
letter-spacing: $i + rpx !important; letter-spacing: $i + rpx !important;
} }
.z-i-#{$i} { .z-i-#{$i} {
z-index: $i; z-index: $i;
} }
.l-h-#{$i} { .l-h-#{$i} {
line-height: $i + rpx !important; line-height: $i + rpx !important;
} }
@@ -89,7 +71,6 @@
.l-#{$i} { .l-#{$i} {
left: $i + rpx !important; left: $i + rpx !important;
} }
.t-#{$i} { .t-#{$i} {
top: $i + rpx !important; top: $i + rpx !important;
} }
@@ -97,22 +78,20 @@
bottom: $i + rpx !important; bottom: $i + rpx !important;
} }
} }
} // 定义字体(rpx)单位,只生成常用的字体大小
$font-sizes: (9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 52, 56, 60);
// 定义字体(rpx)单位大于或等于20的都为rpx单位字体 @each $i in $font-sizes {
@for $i from 9 through 60 {
.font-#{$i} { .font-#{$i} {
font-size: $i + rpx !important; font-size: $i + rpx !important;
} }
} }
// 圆角 // 圆角
@for $i from 4 through 60 { $rounded-sizes: (4, 6, 8, 10, 12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60);
@each $i in $rounded-sizes {
.rounded-#{$i} { .rounded-#{$i} {
border-radius: $i + rpx; border-radius: $i + rpx;
} }
} }
// 多行文本溢出 // 多行文本溢出
@for $i from 1 through 5 { @for $i from 1 through 5 {
.over-line-#{$i} { .over-line-#{$i} {
@@ -125,3 +104,120 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
} }
} }
// 显示相关工具类
.display-none {
display: none !important;
}
.display-block {
display: block !important;
}
.display-inline {
display: inline !important;
}
.display-inline-block {
display: inline-block !important;
}
.display-flex {
display: flex !important;
}
// 浮动相关
.float-left {
float: left !important;
}
.float-right {
float: right !important;
}
.float-none {
float: none !important;
}
.clearfix::after {
content: '';
display: table;
clear: both;
}
// 文本对齐
.text-left {
text-align: left !important;
}
.text-center {
text-align: center !important;
}
.text-right {
text-align: right !important;
}
.text-justify {
text-align: justify !important;
}
// 文本装饰
.text-underline {
text-decoration: underline !important;
}
.text-line-through {
text-decoration: line-through !important;
}
.text-no-underline {
text-decoration: none !important;
}
// 字体粗细
.font-normal {
font-weight: normal !important;
}
.font-bold {
font-weight: bold !important;
}
// 文本大小写
.text-uppercase {
text-transform: uppercase !important;
}
.text-lowercase {
text-transform: lowercase !important;
}
.text-capitalize {
text-transform: capitalize !important;
}
// 垂直对齐
.align-baseline {
vertical-align: baseline !important;
}
.align-top {
vertical-align: top !important;
}
.align-middle {
vertical-align: middle !important;
}
.align-bottom {
vertical-align: bottom !important;
}
// 位置相关
.position-relative {
position: relative !important;
}
.position-absolute {
position: absolute !important;
}
.position-fixed {
position: fixed !important;
}
.position-static {
position: static !important;
}
// 溢出处理
.overflow-auto {
overflow: auto !important;
}
.overflow-hidden {
overflow: hidden !important;
}
.overflow-visible {
overflow: visible !important;
}
.overflow-scroll {
overflow: scroll !important;
}
// 可见性
.visible {
visibility: visible !important;
}
.invisible {
visibility: hidden !important;
}

View File

@@ -0,0 +1,88 @@
/* 暗黑模式样式 */
:root {
// 默认使用亮色模式变量
--uni-color-primary: #007aff;
--uni-color-success: #4cd964;
--uni-color-warning: #f0ad4e;
--uni-color-error: #dd524d;
--uni-text-color: #333;
--uni-text-color-inverse: #fff;
--uni-text-color-grey: #999;
--uni-text-color-placeholder: #808080;
--uni-text-color-disable: #c0c0c0;
--uni-bg-color: #ffffff;
--uni-bg-color-grey: #f8f8f8;
--uni-bg-color-hover: #f1f1f1;
--uni-bg-color-mask: rgba(0, 0, 0, 0.4);
--uni-border-color: #c8c7cc;
}
/* 暗黑模式 */
[data-theme='dark'] {
--uni-color-primary: #0a84ff;
--uni-color-success: #34c759;
--uni-color-warning: #ff9500;
--uni-color-error: #ff3b30;
--uni-text-color: #f2f2f7;
--uni-text-color-inverse: #000000;
--uni-text-color-grey: #8e8e93;
--uni-text-color-placeholder: #4c4c4c;
--uni-text-color-disable: #636366;
--uni-bg-color: #1c1c1e;
--uni-bg-color-grey: #2c2c2e;
--uni-bg-color-hover: #3a3a3c;
--uni-bg-color-mask: rgba(0, 0, 0, 0.6);
--uni-border-color: #434346;
}
/* 系统级暗黑模式 */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--uni-color-primary: #0a84ff;
--uni-color-success: #34c759;
--uni-color-warning: #ff9500;
--uni-color-error: #ff3b30;
--uni-text-color: #f2f2f7;
--uni-text-color-inverse: #000000;
--uni-text-color-grey: #8e8e93;
--uni-text-color-placeholder: #4c4c4c;
--uni-text-color-disable: #636366;
--uni-bg-color: #1c1c1e;
--uni-bg-color-grey: #2c2c2e;
--uni-bg-color-hover: #3a3a3c;
--uni-bg-color-mask: rgba(0, 0, 0, 0.6);
--uni-border-color: #434346;
}
}
/* 应用暗黑模式的通用类 */
.dark-mode {
background-color: var(--uni-bg-color);
color: var(--uni-text-color);
}
.dark-mode .dark-invert {
background-color: var(--uni-bg-color-inverse);
color: var(--uni-text-color-inverse);
}
/* 通用暗黑模式切换类 */
.light-mode {
background-color: var(--uni-bg-color);
color: var(--uni-text-color);
}
/* 通用暗黑模式切换类 */
.dark-mode {
background-color: var(--uni-bg-color);
color: var(--uni-text-color);
}

80
common/utils/env.js Normal file
View File

@@ -0,0 +1,80 @@
/**
* 环境变量验证工具
*/
/**
* 验证必需的环境变量
* @param {Array<string>} requiredVars 必需的环境变量键名数组
* @throws {Error} 如果有任何必需的环境变量缺失则抛出错误
*/
export function validateEnv(requiredVars = []) {
const missingVars = []
for (const key of requiredVars) {
if (!import.meta.env[key]) {
missingVars.push(key)
}
}
if (missingVars.length > 0) {
throw new Error(`缺少必需的环境变量: ${missingVars.join(', ')}`)
}
}
/**
* 获取环境变量,带默认值和类型转换
* @param {string} key 环境变量键名
* @param {any} defaultValue 默认值
* @param {string} type 类型 ('string' | 'number' | 'boolean' | 'array')
* @returns {any} 环境变量值
*/
export function getEnv(key, defaultValue = null, type = 'string') {
const value = import.meta.env[key]
if (value === undefined || value === null || value === '') {
return defaultValue
}
switch (type) {
case 'number':
const numValue = Number(value)
return isNaN(numValue) ? defaultValue : numValue
case 'boolean':
return value === 'true' || value === '1'
case 'array':
try {
return JSON.parse(value)
} catch (e) {
return Array.isArray(defaultValue) ? defaultValue : []
}
case 'string':
default:
return value
}
}
/**
* 环境变量配置
*/
export const EnvConfig = {
// API基础URL
BASE_URL: getEnv('VITE_BASE_URL', '', 'string'),
// 静态资源URL
ASSETS_URL: getEnv('VITE_ASSETSURL', '', 'string'),
// 小程序APPID
APP_ID: getEnv('VITE_APPID', '', 'string'),
// UNI-APPID
UNI_APP_ID: getEnv('VITE_UNI_APPID', '', 'string'),
// 是否为开发环境
IS_DEV: getEnv('NODE_ENV', 'development', 'string') === 'development',
// 是否为生产环境
IS_PROD: getEnv('NODE_ENV', 'development', 'string') === 'production',
// 调试模式
DEBUG: getEnv('VITE_DEBUG', false, 'boolean'),
// API超时时间毫秒
API_TIMEOUT: getEnv('VITE_API_TIMEOUT', 10000, 'number'),
}
// 验证必需的环境变量
try {
validateEnv(['VITE_BASE_URL', 'VITE_APPID'])
} catch (error) {
console.error('环境变量验证失败:', error.message)
// 在开发环境中抛出错误
if (EnvConfig.IS_DEV) {
throw error
}
}
export default EnvConfig

View File

@@ -1,6 +1,5 @@
const baseUrl = import.meta.env.VITE_BASE_URL const baseUrl = import.meta.env.VITE_BASE_URL
const assetsUrl = import.meta.env.VITE_ASSETSURL const assetsUrl = import.meta.env.VITE_ASSETSURL
/** /**
* 工具类 - 提供常用的工具方法 * 工具类 - 提供常用的工具方法
* @class Tool * @class Tool
@@ -13,11 +12,9 @@ class Tool {
SUCCESS: 1, SUCCESS: 1,
LOADING: 2, LOADING: 2,
} }
// 字体加载状态缓存 // 字体加载状态缓存
this.loadedFonts = new Set() this.loadedFonts = new Set()
} }
/** /**
* 文字轻提示 * 文字轻提示
* @param {string} str 提示文字 * @param {string} str 提示文字
@@ -30,13 +27,11 @@ class Tool {
console.warn('alert方法需要提供提示文字') console.warn('alert方法需要提供提示文字')
return return
} }
const iconMap = { const iconMap = {
[this.ICON_TYPES.NONE]: 'none', [this.ICON_TYPES.NONE]: 'none',
[this.ICON_TYPES.SUCCESS]: 'success', [this.ICON_TYPES.SUCCESS]: 'success',
[this.ICON_TYPES.LOADING]: 'loading', [this.ICON_TYPES.LOADING]: 'loading',
} }
uni.showToast({ uni.showToast({
title: String(str), title: String(str),
icon: iconMap[icon] || 'none', icon: iconMap[icon] || 'none',
@@ -49,7 +44,6 @@ class Tool {
}) })
}) })
} }
/** /**
* 显示loading加载 * 显示loading加载
* @param {string} [title=' '] 加载文案 * @param {string} [title=' '] 加载文案
@@ -58,14 +52,12 @@ class Tool {
loading(title = ' ', mask = true) { loading(title = ' ', mask = true) {
uni.showLoading({ title, mask }) uni.showLoading({ title, mask })
} }
/** /**
* 关闭loading提示框 * 关闭loading提示框
*/ */
hideLoading() { hideLoading() {
uni.hideLoading() uni.hideLoading()
} }
/** /**
* 统一处理URL格式确保以/开头 * 统一处理URL格式确保以/开头
* @param {string} url 页面地址 * @param {string} url 页面地址
@@ -76,17 +68,14 @@ class Tool {
if (!url || typeof url !== 'string') { if (!url || typeof url !== 'string') {
throw new Error('URL必须是字符串') throw new Error('URL必须是字符串')
} }
return url.startsWith('/') ? url : `/${url}` return url.startsWith('/') ? url : `/${url}`
} }
/** /**
* 可返回跳转(导航到新页面) * 可返回跳转(导航到新页面)
* @param {string} url 页面地址 * @param {string} url 页面地址
*/ */
navigateTo(url) { navigateTo(url) {
const formattedUrl = this._formatUrl(url) const formattedUrl = this._formatUrl(url)
uni.navigateTo({ uni.navigateTo({
url: formattedUrl, url: formattedUrl,
fail: err => { fail: err => {
@@ -95,7 +84,6 @@ class Tool {
}, },
}) })
} }
/** /**
* 不可返回跳转(重定向到新页面) * 不可返回跳转(重定向到新页面)
* @param {string} url 页面地址 * @param {string} url 页面地址
@@ -103,7 +91,6 @@ class Tool {
redirectTo(url) { redirectTo(url) {
uni.redirectTo({ url: this._formatUrl(url) }) uni.redirectTo({ url: this._formatUrl(url) })
} }
/** /**
* 清除页面栈跳转(重新启动到新页面) * 清除页面栈跳转(重新启动到新页面)
* @param {string} url 页面地址 * @param {string} url 页面地址
@@ -111,7 +98,6 @@ class Tool {
reLaunch(url) { reLaunch(url) {
uni.reLaunch({ url: this._formatUrl(url) }) uni.reLaunch({ url: this._formatUrl(url) })
} }
/** /**
* 跳转tabBar页 * 跳转tabBar页
* @param {string} url 页面地址 * @param {string} url 页面地址
@@ -119,7 +105,6 @@ class Tool {
switchTab(url) { switchTab(url) {
uni.switchTab({ url: this._formatUrl(url) }) uni.switchTab({ url: this._formatUrl(url) })
} }
/** /**
* 返回上一页面或指定页面 * 返回上一页面或指定页面
* @param {number} [delta=1] 返回的页面数 * @param {number} [delta=1] 返回的页面数
@@ -127,7 +112,6 @@ class Tool {
*/ */
navigateBack(delta = 1, fallbackUrl = '/pages/index/index') { navigateBack(delta = 1, fallbackUrl = '/pages/index/index') {
const pages = getCurrentPages() const pages = getCurrentPages()
if (pages.length <= 1) { if (pages.length <= 1) {
console.warn('无上一页,使用回退地址') console.warn('无上一页,使用回退地址')
uni.reLaunch({ url: fallbackUrl }) uni.reLaunch({ url: fallbackUrl })
@@ -135,7 +119,6 @@ class Tool {
uni.navigateBack({ delta }) uni.navigateBack({ delta })
} }
} }
/** /**
* 操作本地缓存 * 操作本地缓存
* @param {string} key 缓存键值 * @param {string} key 缓存键值
@@ -146,24 +129,20 @@ class Tool {
if (typeof key !== 'string') { if (typeof key !== 'string') {
throw new Error('key必须是字符串') throw new Error('key必须是字符串')
} }
// 设置操作 // 设置操作
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
uni.setStorageSync(key, value) uni.setStorageSync(key, value)
return return
} }
// 读取操作 // 读取操作
if (key !== '#') { if (key !== '#') {
return uni.getStorageSync(key) return uni.getStorageSync(key)
} }
// 特殊操作 // 特殊操作
if (key === '#') { if (key === '#') {
uni.clearStorageSync() uni.clearStorageSync()
} }
} }
/** /**
* 删除指定缓存 * 删除指定缓存
* @param {string} key 要删除的缓存键 * @param {string} key 要删除的缓存键
@@ -172,10 +151,8 @@ class Tool {
if (typeof key !== 'string') { if (typeof key !== 'string') {
throw new Error('key必须是字符串') throw new Error('key必须是字符串')
} }
uni.removeStorageSync(key) uni.removeStorageSync(key)
} }
/** /**
* 获取缓存信息 * 获取缓存信息
* @returns {Object} 缓存信息 * @returns {Object} 缓存信息
@@ -183,7 +160,6 @@ class Tool {
getStorageInfo() { getStorageInfo() {
return uni.getStorageInfoSync() return uni.getStorageInfoSync()
} }
/** /**
* 复制文本到剪贴板 * 复制文本到剪贴板
* @param {string} data 要复制的文本 * @param {string} data 要复制的文本
@@ -194,7 +170,6 @@ class Tool {
this.alert('暂无内容') this.alert('暂无内容')
return false return false
} }
try { try {
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
uni.setClipboardData({ uni.setClipboardData({
@@ -203,7 +178,6 @@ class Tool {
fail: reject, fail: reject,
}) })
}) })
this.alert('复制成功') this.alert('复制成功')
return true return true
} catch (error) { } catch (error) {
@@ -212,7 +186,6 @@ class Tool {
return false return false
} }
} }
/** /**
* 导入外部字体 * 导入外部字体
* @param {string} fontName 字体文件名(不含路径) * @param {string} fontName 字体文件名(不含路径)
@@ -222,15 +195,12 @@ class Tool {
if (!fontName || typeof fontName !== 'string') { if (!fontName || typeof fontName !== 'string') {
throw new Error('字体名称必须是字符串') throw new Error('字体名称必须是字符串')
} }
// 检查是否已加载过 // 检查是否已加载过
if (this.loadedFonts.has(fontName)) { if (this.loadedFonts.has(fontName)) {
return true return true
} }
try { try {
const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名 const fontFamily = fontName.replace(/\.[^/.]+$/, '') // 移除文件扩展名
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
uni.loadFontFace({ uni.loadFontFace({
family: fontFamily, family: fontFamily,
@@ -240,7 +210,6 @@ class Tool {
fail: reject, fail: reject,
}) })
}) })
this.loadedFonts.add(fontName) this.loadedFonts.add(fontName)
return true return true
} catch (error) { } catch (error) {
@@ -248,7 +217,6 @@ class Tool {
return false return false
} }
} }
/** /**
* 保存图片到相册 * 保存图片到相册
* @param {string} url 图片URL * @param {string} url 图片URL
@@ -259,7 +227,6 @@ class Tool {
this.alert('图片地址不能为空') this.alert('图片地址不能为空')
return false return false
} }
try { try {
// 检查权限 // 检查权限
const { authSetting } = await new Promise((resolve, reject) => { const { authSetting } = await new Promise((resolve, reject) => {
@@ -268,7 +235,6 @@ class Tool {
fail: reject, fail: reject,
}) })
}) })
if (!authSetting['scope.writePhotosAlbum']) { if (!authSetting['scope.writePhotosAlbum']) {
// 请求权限 // 请求权限
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
@@ -279,7 +245,6 @@ class Tool {
}) })
}) })
} }
// 获取图片信息 // 获取图片信息
const { path } = await new Promise((resolve, reject) => { const { path } = await new Promise((resolve, reject) => {
uni.getImageInfo({ uni.getImageInfo({
@@ -288,7 +253,6 @@ class Tool {
fail: reject, fail: reject,
}) })
}) })
// 保存到相册 // 保存到相册
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
uni.saveImageToPhotosAlbum({ uni.saveImageToPhotosAlbum({
@@ -297,12 +261,10 @@ class Tool {
fail: reject, fail: reject,
}) })
}) })
this.alert('已保存到相册') this.alert('已保存到相册')
return true return true
} catch (error) { } catch (error) {
console.error('保存图片失败:', error) console.error('保存图片失败:', error)
if (error.errMsg && error.errMsg.includes('auth')) { if (error.errMsg && error.errMsg.includes('auth')) {
// 权限相关错误 // 权限相关错误
await new Promise(resolve => { await new Promise(resolve => {
@@ -313,16 +275,240 @@ class Tool {
success: resolve, success: resolve,
}) })
}) })
uni.openSetting() uni.openSetting()
} else { } else {
this.alert('保存失败,请重试') this.alert('保存失败,请重试')
} }
return false return false
} }
} }
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 延迟时间(ms)
* @param {boolean} immediate 是否立即执行
* @returns {Function} 防抖后的函数
*/
debounce(func, wait, immediate = false) {
let timeout
return function (...args) {
const later = () => {
timeout = null
if (!immediate) func.apply(this, args)
}
const callNow = immediate && !timeout
clearTimeout(timeout)
timeout = setTimeout(later, wait)
if (callNow) func.apply(this, args)
}
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} wait 延迟时间(ms)
* @returns {Function} 节流后的函数
*/
throttle(func, wait) {
let timeout
let previous = 0
return function (...args) {
const now = Date.now()
const remaining = wait - (now - previous)
const context = this
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(context, args)
} else if (!timeout) {
timeout = setTimeout(() => {
previous = Date.now()
timeout = null
func.apply(context, args)
}, remaining)
}
}
}
/**
* 日期格式化
* @param {Date|string|number} date 日期对象、字符串或时间戳
* @param {string} fmt 格式字符串, 默认为 'YYYY-MM-DD HH:mm:ss'
* @returns {string} 格式化后的日期字符串
*/
formatDate(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
// 如果传入的是时间戳转换为Date对象
if (typeof date === 'number') {
date = new Date(date)
} else if (typeof date === 'string') {
// 如果传入的是字符串尝试转换为Date对象
date = new Date(date)
}
if (!(date instanceof Date) || isNaN(date.getTime())) {
throw new Error('Invalid date')
}
const o = {
'Y+': date.getFullYear(),
'M+': date.getMonth() + 1,
'D+': date.getDate(),
'H+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
S: date.getMilliseconds(),
}
for (let k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
let str = o[k] + ''
if (k === 'S') {
fmt = fmt.replace(RegExp.$1, str.padStart(3, '0'))
} else {
fmt = fmt.replace(RegExp.$1, str.length === 1 ? '0' + str : str)
}
}
}
return fmt
}
/**
* 深拷贝函数
* @param {*} obj 要深拷贝的对象
* @returns {*} 拷贝后的对象
*/
deepClone(obj) {
// 处理null、undefined和原始类型
if (obj === null || typeof obj !== 'object') {
return obj
}
// 处理日期对象
if (obj instanceof Date) {
return new Date(obj.getTime())
}
// 处理数组和对象
if (obj instanceof Array) {
return obj.map(item => this.deepClone(item))
}
// 处理普通对象
const clonedObj = {}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = this.deepClone(obj[key])
}
}
return clonedObj
}
/**
* 表单验证工具
* @class Validator
*/
getValidator() {
const rules = {
/**
* 必填验证
* @param {*} value 值
* @param {boolean} required 是否必填
* @returns {boolean} 验证结果
*/
required(value, required = true) {
if (!required) return true
return value !== undefined && value !== null && value !== ''
},
/**
* 邮箱验证
* @param {string} value 值
* @returns {boolean} 验证结果
*/
email(value) {
if (!value) return true
const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return reg.test(value)
},
/**
* 手机号验证
* @param {string} value 值
* @returns {boolean} 验证结果
*/
mobile(value) {
if (!value) return true
const reg = /^1[3-9]\d{9}$/
return reg.test(value)
},
/**
* 最小长度验证
* @param {string} value 值
* @param {number} length 最小长度
* @returns {boolean} 验证结果
*/
minLength(value, length) {
if (!value) return true
return value.length >= length
},
/**
* 最大长度验证
* @param {string} value 值
* @param {number} length 最大长度
* @returns {boolean} 验证结果
*/
maxLength(value, length) {
if (!value) return true
return value.length <= length
},
/**
* 数值范围验证
* @param {number} value 值
* @param {number} min 最小值
* @param {number} max 最大值
* @returns {boolean} 验证结果
*/
range(value, min, max) {
if (value === undefined || value === null) return true
return value >= min && value <= max
},
}
return {
/**
* 验证单个字段
* @param {*} value 值
* @param {Object} rule 规则
* @returns {string|null} 错误信息
*/
validateField(value, rule) {
// 必填验证
if (rule.required !== undefined && !rules.required(value, rule.required)) {
return rule.message || '此字段为必填项'
}
// 如果值为空且非必填,跳过其他验证
if (value === undefined || value === null || value === '') {
return null
}
// 其他验证规则
if (rule.type && rules[rule.type]) {
if (!rules[rule.type](value, rule.param1, rule.param2)) {
return rule.message || '字段格式不正确'
}
}
return null
},
/**
* 验证整个表单
* @param {Object} data 表单数据
* @param {Object} rules 验证规则
* @returns {Object} 验证结果 { isValid: boolean, errors: Object }
*/
validate(data, rules) {
const errors = {}
let isValid = true
for (let field in rules) {
const error = this.validateField(data[field], rules[field])
if (error) {
errors[field] = error
isValid = false
}
}
return { isValid, errors }
},
}
}
/** /**
* 微信支付 * 微信支付
* @param {Object} paymentData 支付参数 * @param {Object} paymentData 支付参数
@@ -338,6 +524,11 @@ class Tool {
}) })
}) })
} }
/**
* 文件上传
* @param {string} filePath 文件路径
* @returns {Promise<Object>} 上传结果
*/
upload(filePath) { upload(filePath) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
uni.uploadFile({ uni.uploadFile({
@@ -357,7 +548,120 @@ class Tool {
}) })
}) })
} }
/**
* 暗黑模式切换
* @param {boolean} isDark 是否启用暗黑模式
*/
toggleDarkMode(isDark) {
try {
if (isDark) {
// 启用暗黑模式
document.documentElement.setAttribute('data-theme', 'dark')
uni.setStorageSync('theme', 'dark')
} else {
// 启用亮色模式
document.documentElement.setAttribute('data-theme', 'light')
uni.setStorageSync('theme', 'light')
}
} catch (error) {
console.error('切换主题失败:', error)
}
}
/**
* 获取当前主题模式
* @returns {string} 当前主题 ('light' | 'dark')
*/
getCurrentTheme() {
try {
// 优先使用用户设置的主题
const savedTheme = uni.getStorageSync('theme')
if (savedTheme) {
return savedTheme
}
// 检查系统主题
const systemInfo = uni.getSystemInfoSync()
if (systemInfo.theme) {
return systemInfo.theme
}
// 默认返回亮色主题
return 'light'
} catch (error) {
console.error('获取主题失败:', error)
return 'light'
}
}
/**
* 权限检查
* @param {string} permission 权限标识
* @returns {boolean} 是否拥有权限
*/
hasPermission(permission) {
try {
// 获取用户权限列表
const permissions = uni.getStorageSync('permissions') || []
return permissions.includes(permission)
} catch (error) {
console.error('权限检查失败:', error)
return false
}
}
/**
* 设置用户权限
* @param {Array<string>} permissions 权限列表
*/
setPermissions(permissions) {
try {
uni.setStorageSync('permissions', Array.isArray(permissions) ? permissions : [])
} catch (error) {
console.error('设置权限失败:', error)
}
}
/**
* 获取用户所有权限
* @returns {Array<string>} 权限列表
*/
getUserPermissions() {
try {
return uni.getStorageSync('permissions') || []
} catch (error) {
console.error('获取权限失败:', error)
return []
}
}
/**
* 检查用户是否有任意权限
* @param {Array<string>} permissions 权限列表
* @returns {boolean} 是否有任意权限
*/
hasAnyPermission(permissions) {
if (!Array.isArray(permissions)) {
return false
}
const userPermissions = this.getUserPermissions()
return permissions.some(permission => userPermissions.includes(permission))
}
/**
* 检查用户是否拥有所有权限
* @param {Array<string>} permissions 权限列表
* @returns {boolean} 是否拥有所有权限
*/
hasAllPermissions(permissions) {
if (!Array.isArray(permissions)) {
return false
}
const userPermissions = this.getUserPermissions()
return permissions.every(permission => userPermissions.includes(permission))
}
/**
* 清除用户权限
*/
clearPermissions() {
try {
uni.removeStorageSync('permissions')
} catch (error) {
console.error('清除权限失败:', error)
}
}
} }
// 创建单例并导出 // 创建单例并导出
export default new Tool() export default new Tool()

View File

@@ -0,0 +1,74 @@
<template>
<view class="card" :class="{ 'card--shadow': shadow, 'card--border': border }">
<view class="card__header" v-if="title || $slots.header">
<slot name="header">
<view class="card__title">{{ title }}</view>
</slot>
</view>
<view class="card__body">
<slot></slot>
</view>
<view class="card__footer" v-if="$slots.footer">
<slot name="footer"></slot>
</view>
</view>
</template>
<script setup>
// 定义props
defineProps({
// 卡片标题
title: {
type: String,
default: ''
},
// 是否显示阴影
shadow: {
type: Boolean,
default: false
},
// 是否显示边框
border: {
type: Boolean,
default: true
}
})
</script>
<style lang="scss" scoped>
.card {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
// 阴影样式
&--shadow {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
// 边框样式
&--border {
border: 1rpx solid #f0f0f0;
}
&__header {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
&__title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
&__body {
padding: 32rpx;
}
&__footer {
padding: 24rpx 32rpx;
border-top: 1rpx solid #f0f0f0;
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<view
class="custom-button"
:class="[`custom-button--${type}`, `custom-button--${size}`, { 'custom-button--disabled': disabled, 'custom-button--loading': loading }]"
@click="handleClick"
>
<view class="custom-button__loading" v-if="loading">
<u-loading-icon mode="circle" size="20" color="#fff" />
</view>
<view class="custom-button__content">
<slot></slot>
</view>
</view>
</template>
<script setup>
import { ref, onBeforeUnmount } from 'vue'
// 定义props
const props = defineProps({
// 按钮类型
type: {
type: String,
default: 'primary', // primary, secondary, danger, ghost
validator: (value) => ['primary', 'secondary', 'danger', 'ghost'].includes(value)
},
// 按钮大小
size: {
type: String,
default: 'medium', // small, medium, large
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
},
// 防抖时间(毫秒)
debounce: {
type: Number,
default: 0
}
})
// 定义emits
const emit = defineEmits(['click'])
// 防抖定时器
const debounceTimer = ref(null)
/**
* 处理点击事件
* @param {Event} e 点击事件对象
*/
function handleClick(e) {
// 如果禁用或加载中,不处理点击
if (props.disabled || props.loading) {
return
}
// 如果设置了防抖
if (props.debounce > 0) {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
}
debounceTimer.value = setTimeout(() => {
emit('click', e)
debounceTimer.value = null
}, props.debounce)
} else {
emit('click', e)
}
}
// 组件卸载前清除定时器
onBeforeUnmount(() => {
if (debounceTimer.value) {
clearTimeout(debounceTimer.value)
debounceTimer.value = null
}
})
</script>
<style lang="scss" scoped>
.custom-button {
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8rpx;
font-weight: 500;
text-align: center;
vertical-align: middle;
cursor: pointer;
transition: all 0.2s;
user-select: none;
border: 1rpx solid transparent;
position: relative;
overflow: hidden;
// 主要按钮
&--primary {
color: #fff;
background-color: #1890ff;
border-color: #1890ff;
&:not(.custom-button--disabled):active {
background-color: #40a9ff;
border-color: #40a9ff;
}
}
// 次要按钮
&--secondary {
color: #333;
background-color: #f5f5f5;
border-color: #d9d9d9;
&:not(.custom-button--disabled):active {
background-color: #e6e6e6;
border-color: #bfbfbf;
}
}
// 危险按钮
&--danger {
color: #fff;
background-color: #ff4d4f;
border-color: #ff4d4f;
&:not(.custom-button--disabled):active {
background-color: #ff7875;
border-color: #ff7875;
}
}
// 幽灵按钮
&--ghost {
color: #1890ff;
background-color: transparent;
border-color: #1890ff;
&:not(.custom-button--disabled):active {
color: #40a9ff;
border-color: #40a9ff;
}
}
// 禁用状态
&--disabled {
cursor: not-allowed;
opacity: 0.6;
}
// 加载状态
&--loading {
cursor: not-allowed;
}
// 小尺寸
&--small {
height: 56rpx;
padding: 0 24rpx;
font-size: 24rpx;
}
// 中等尺寸
&--medium {
height: 72rpx;
padding: 0 32rpx;
font-size: 28rpx;
}
// 大尺寸
&--large {
height: 88rpx;
padding: 0 40rpx;
font-size: 32rpx;
}
&__loading {
margin-right: 16rpx;
}
&__content {
display: flex;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<view class="form-input">
<view class="form-input__label" v-if="label">
{{ label }}
<text class="form-input__required" v-if="required">*</text>
</view>
<view class="form-input__wrapper">
<input
class="form-input__field"
:type="type"
:placeholder="placeholder"
:value="modelValue"
:disabled="disabled"
:maxlength="maxlength"
@input="handleInput"
@blur="handleBlur"
@focus="handleFocus"
/>
<view class="form-input__suffix" v-if="$slots.suffix">
<slot name="suffix"></slot>
</view>
</view>
<view class="form-input__error" v-if="error">{{ error }}</view>
</view>
</template>
<script setup>
// 定义props
const props = defineProps({
// 输入框的值
modelValue: {
type: [String, Number],
default: ''
},
// 标签
label: {
type: String,
default: ''
},
// 占位符
placeholder: {
type: String,
default: ''
},
// 输入框类型
type: {
type: String,
default: 'text'
},
// 是否必填
required: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 最大长度
maxlength: {
type: Number,
default: 140
},
// 错误信息
error: {
type: String,
default: ''
}
})
// 定义emits
const emit = defineEmits(['update:modelValue', 'blur', 'focus'])
/**
* 处理输入事件
* @param {Event} e 输入事件对象
*/
function handleInput(e) {
emit('update:modelValue', e.detail.value)
}
/**
* 处理失去焦点事件
* @param {Event} e 失去焦点事件对象
*/
function handleBlur(e) {
emit('blur', e.detail.value)
}
/**
* 处理获得焦点事件
* @param {Event} e 获得焦点事件对象
*/
function handleFocus(e) {
emit('focus', e.detail.value)
}
</script>
<style lang="scss" scoped>
.form-input {
margin-bottom: 32rpx;
&__label {
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
&__required {
color: #ff4d4f;
margin-left: 4rpx;
}
&__wrapper {
display: flex;
align-items: center;
border: 1rpx solid #d9d9d9;
border-radius: 8rpx;
padding: 0 24rpx;
background-color: #fff;
}
&__field {
flex: 1;
height: 80rpx;
font-size: 28rpx;
color: #333;
}
&__suffix {
margin-left: 16rpx;
}
&__error {
font-size: 24rpx;
color: #ff4d4f;
margin-top: 8rpx;
}
}
</style>

141
directives/index.js Normal file
View File

@@ -0,0 +1,141 @@
/**
* 自定义指令入口文件
*/
// 防止重复点击指令
export const preventReClick = {
mounted(el, binding) {
el.addEventListener('click', () => {
if (el.disabled) return
el.disabled = true
setTimeout(() => {
el.disabled = false
}, binding.value || 1000)
})
},
}
// 长按指令
export const longPress = {
mounted(el, binding) {
let timer = null
let startTime = 0
const handler = binding.value
const duration = binding.arg || 1000
// 取消定时器
const cancel = () => {
if (timer) {
clearTimeout(timer)
timer = null
}
}
// 开始计时
const start = () => {
startTime = Date.now()
timer = setTimeout(() => {
handler(el)
}, duration)
}
// 绑定事件
el.addEventListener('touchstart', start)
el.addEventListener('touchend', cancel)
el.addEventListener('touchcancel', cancel)
},
}
// 权限控制指令
export const permission = {
mounted(el, binding) {
const { value } = binding
const permissions = uni.getStorageSync('permissions') || []
if (value && !permissions.includes(value)) {
// 隐藏元素
el.style.display = 'none'
el.__vue__ && (el.__vue__.isDisplay = false)
}
},
updated(el, binding) {
const { value } = binding
const permissions = uni.getStorageSync('permissions') || []
if (value && !permissions.includes(value)) {
// 隐藏元素
el.style.display = 'none'
el.__vue__ && (el.__vue__.isDisplay = false)
} else {
// 显示元素
el.style.display = ''
el.__vue__ && (el.__vue__.isDisplay = true)
}
},
}
// 拖拽指令
export const drag = {
mounted(el) {
let startX = 0
let startY = 0
let initialX = 0
let initialY = 0
let isDragging = false
// 获取元素初始位置
const getInitialPosition = () => {
const rect = el.getBoundingClientRect()
initialX = rect.left
initialY = rect.top
}
// 鼠标按下事件
const handleMouseDown = e => {
isDragging = true
startX = e.clientX
startY = e.clientY
getInitialPosition()
el.style.position = 'absolute'
el.style.left = initialX + 'px'
el.style.top = initialY + 'px'
el.style.zIndex = 9999
}
// 鼠标移动事件
const handleMouseMove = e => {
if (!isDragging) return
const dx = e.clientX - startX
const dy = e.clientY - startY
el.style.left = initialX + dx + 'px'
el.style.top = initialY + dy + 'px'
}
// 鼠标松开事件
const handleMouseUp = () => {
isDragging = false
}
// 绑定事件
el.addEventListener('mousedown', handleMouseDown)
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
// 在元素上存储事件处理函数,以便在指令解绑时移除
el._dragHandlers = {
handleMouseDown,
handleMouseMove,
handleMouseUp,
}
},
unmounted(el) {
// 移除事件监听器
if (el._dragHandlers) {
const { handleMouseDown, handleMouseMove, handleMouseUp } = el._dragHandlers
el.removeEventListener('mousedown', handleMouseDown)
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
delete el._dragHandlers
}
},
}

7
hooks/index.js Normal file
View File

@@ -0,0 +1,7 @@
/**
* Hooks统一导出入口
*/
export { useRequest } from './useRequest.js'
export { useState, useStorageState, useFormState } from './useState.js'
export { useGet, usePost } from './useApi.js'

157
hooks/useApi.js Normal file
View File

@@ -0,0 +1,157 @@
import { ref, reactive } from 'vue'
import { http } from '@/api/index.js'
import tool from '@/common/utils/tool.js'
/**
* GET请求Hook
* @param {string} url 请求URL
* @param {Object} options 配置选项
* @returns {Object} 请求相关状态和方法
*/
export function useGet(url, options = {}) {
const {
manual = false, // 是否手动触发
params = {}, // 请求参数
cache = false, // 是否使用缓存
loadingDelay = 0, // loading延迟时间
onSuccess = () => {}, // 成功回调
onError = () => {}, // 失败回调
} = options
// 状态
const data = ref(null)
const loading = ref(false)
const error = ref(null)
// loading延迟定时器
let loadingTimer = null
/**
* 执行GET请求
* @param {Object} requestParams 请求参数
*/
async function run(requestParams = {}) {
// 清除之前的loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
// 如果有loading延迟先启动定时器
if (loadingDelay > 0) {
loadingTimer = setTimeout(() => {
loading.value = true
}, loadingDelay)
} else {
loading.value = true
}
try {
const response = await http.get(url, {
params: { ...params, ...requestParams },
cache,
})
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
data.value = response.data
error.value = null
onSuccess(response.data)
return response.data
} catch (err) {
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
error.value = err
data.value = null
console.error(`GET请求失败: ${url}`, err)
onError(err)
throw err
} finally {
loading.value = false
}
}
/**
* 取消请求
*/
function cancel() {
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
loading.value = false
}
// 如果不是手动触发,立即执行
if (!manual) {
Promise.resolve().then(() => {
run(params)
})
}
return {
data,
loading,
error,
run,
cancel,
}
}
/**
* POST请求Hook
* @param {string} url 请求URL
* @returns {Object} 请求相关状态和方法
*/
export function usePost(url) {
/**
* 执行POST请求
* @param {Object} data 请求数据
* @param {Object} options 配置选项
*/
async function run(data, options = {}) {
const { loadingDelay = 0, onSuccess = () => {}, onError = () => {} } = options
let loadingTimer = null
// 如果有loading延迟先启动定时器
if (loadingDelay > 0) {
loadingTimer = setTimeout(() => {
tool.loading()
}, loadingDelay)
} else {
tool.loading()
}
try {
const response = await http.post(url, data)
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
tool.hideLoading()
onSuccess(response.data)
return response.data
} catch (err) {
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
tool.hideLoading()
console.error(`POST请求失败: ${url}`, err)
onError(err)
throw err
}
}
return {
run,
}
}

113
hooks/useRequest.js Normal file
View File

@@ -0,0 +1,113 @@
import { ref, reactive } from 'vue'
import tool from '@/common/utils/tool.js'
/**
* 通用请求Hook
* @param {Function} apiFunction API函数
* @param {Object} options 配置选项
* @returns {Object} 请求相关状态和方法
*/
export function useRequest(apiFunction, options = {}) {
const {
manual = false, // 是否手动触发
defaultParams = [], // 默认参数
onSuccess = () => {}, // 成功回调
onError = () => {}, // 失败回调
loadingDelay = 0, // loading延迟时间
} = options
// 状态
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const params = ref([])
// loading延迟定时器
let loadingTimer = null
/**
* 执行请求
* @param {...any} args 请求参数
*/
async function run(...args) {
// 清除之前的loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
// 设置参数
params.value = args
// 如果有loading延迟先启动定时器
if (loadingDelay > 0) {
loadingTimer = setTimeout(() => {
loading.value = true
}, loadingDelay)
} else {
loading.value = true
}
try {
const result = await apiFunction(...args)
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
data.value = result
error.value = null
onSuccess(result, args)
return result
} catch (err) {
// 清除loading延迟定时器
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
error.value = err
data.value = null
console.error('请求失败:', err)
onError(err, args)
throw err
} finally {
loading.value = false
}
}
/**
* 取消请求
*/
function cancel() {
if (loadingTimer) {
clearTimeout(loadingTimer)
loadingTimer = null
}
loading.value = false
}
/**
* 重新执行请求
*/
function refresh() {
return run(...params.value)
}
// 如果不是手动触发,立即执行
if (!manual) {
// 使用nextTick确保在组件挂载后执行
Promise.resolve().then(() => {
run(...defaultParams)
})
}
return {
data,
loading,
error,
params,
run,
cancel,
refresh,
}
}

140
hooks/useState.js Normal file
View File

@@ -0,0 +1,140 @@
import { ref, computed } from 'vue'
import tool from '@/common/utils/tool.js'
/**
* 状态管理Hook
* @param {any} initialState 初始状态
* @returns {Object} 状态和操作方法
*/
export function useState(initialState) {
const state = ref(initialState)
/**
* 设置状态
* @param {any} newState 新状态
*/
function setState(newState) {
if (typeof newState === 'function') {
state.value = newState(state.value)
} else {
state.value = newState
}
}
/**
* 重置状态到初始值
*/
function resetState() {
state.value = typeof initialState === 'function' ? initialState() : initialState
}
return {
state: computed(() => state.value),
setState,
resetState,
}
}
/**
* 本地存储状态Hook
* @param {string} key 存储键名
* @param {any} defaultValue 默认值
* @returns {Object} 状态和操作方法
*/
export function useStorageState(key, defaultValue = null) {
const storageValue = tool.storage(key) ?? defaultValue
const { state, setState, resetState } = useState(storageValue)
/**
* 设置状态并同步到本地存储
* @param {any} newState 新状态
*/
function setStorageState(newState) {
if (typeof newState === 'function') {
newState = newState(state.value)
}
setState(newState)
tool.storage(key, newState)
}
/**
* 清除本地存储中的值
*/
function clearStorage() {
tool.removeStorage(key)
resetState()
}
return {
state,
setState: setStorageState,
resetState: () => {
clearStorage()
},
clearStorage,
}
}
/**
* 表单状态Hook
* @param {Object} initialFormData 初始表单数据
* @returns {Object} 表单状态和操作方法
*/
export function useFormState(initialFormData = {}) {
const { state, setState, resetState } = useState({ ...initialFormData })
/**
* 设置表单项
* @param {string} field 字段名
* @param {any} value 字段值
*/
function setField(field, value) {
setState(prev => ({
...prev,
[field]: value,
}))
}
/**
* 重置表单到初始状态
*/
function resetForm() {
resetState()
}
/**
* 清空表单
*/
function clearForm() {
setState({})
}
/**
* 批量设置表单值
* @param {Object} values 表单值对象
*/
function setFields(values) {
setState(prev => ({
...prev,
...values,
}))
}
/**
* 获取表单验证器
* @returns {Object} 验证器实例
*/
function getValidator() {
return tool.getValidator()
}
return {
form: state,
setField,
resetForm,
clearForm,
setFields,
getValidator,
}
}

10
main.js
View File

@@ -1,21 +1,23 @@
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 { preventReClick, longPress, permission, drag } from './directives/index'
import './uni.promisify.adaptor' import './uni.promisify.adaptor'
uni.$zp = { uni.$zp = {
config: { config: {
'empty-view-text': '空空如也~~', 'empty-view-text': '空空如也~~',
'refresher-enabled': true, 'refresher-enabled': true,
}, },
} }
export function createApp() { 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) // 注册自定义指令
app.directive('prevent-re-click', preventReClick)
app.directive('long-press', longPress)
app.directive('permission', permission)
app.directive('drag', drag)
return { app } return { app }
} }

View File

@@ -1,7 +1,33 @@
{ {
"name": "uniapp-template",
"version": "1.0.0",
"description": "基于 UniApp + Vue3 + uView-Plus 的微信小程序快速开发模板",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint src/**/*.{js,jsx,ts,tsx,vue} --fix",
"format": "prettier --write src/**/*.{js,jsx,ts,tsx,vue,json,css,scss}",
"prepare": "husky install"
},
"dependencies": { "dependencies": {
"dotenv": "^17.2.2", "dotenv": "^17.2.2",
"dayjs": "*", "dayjs": "*",
"vue": "^3.5.21" "vue": "^3.5.21"
},
"devDependencies": {
"eslint": "^8.0.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^3.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.0",
"lint-staged": "^15.0.0"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx,vue}": [
"eslint --fix",
"prettier --write"
]
} }
} }

View File

@@ -68,12 +68,69 @@ $uni-spacing-col-lg: 12px;
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 $uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */ /* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色 $uni-color-title: #2c405a; // 文章标题颜色
$uni-font-size-title: 20px; $uni-font-size-title: 20px;
$uni-color-subtitle: #555555; // 二级标题颜色 $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变量 */ /* 全局资源URL变量 */
$ASSETSURL: 'https://cdn.vrupup.com/s/1732/assets/'; $ASSETSURL: 'https://cdn.vrupup.com/s/1732/assets/';
/* 亮色模式颜色变量 */
$uni-color-primary-light: #007aff;
$uni-color-success-light: #4cd964;
$uni-color-warning-light: #f0ad4e;
$uni-color-error-light: #dd524d;
$uni-text-color-light: #333;
$uni-text-color-inverse-light: #fff;
$uni-text-color-grey-light: #999;
$uni-text-color-placeholder-light: #808080;
$uni-text-color-disable-light: #c0c0c0;
$uni-bg-color-light: #ffffff;
$uni-bg-color-grey-light: #f8f8f8;
$uni-bg-color-hover-light: #f1f1f1;
$uni-bg-color-mask-light: rgba(0, 0, 0, 0.4);
$uni-border-color-light: #c8c7cc;
/* 暗色模式颜色变量 */
$uni-color-primary-dark: #0a84ff;
$uni-color-success-dark: #34c759;
$uni-color-warning-dark: #ff9500;
$uni-color-error-dark: #ff3b30;
$uni-text-color-dark: #f2f2f7;
$uni-text-color-inverse-dark: #000000;
$uni-text-color-grey-dark: #8e8e93;
$uni-text-color-placeholder-dark: #4c4c4c;
$uni-text-color-disable-dark: #636366;
$uni-bg-color-dark: #1c1c1e;
$uni-bg-color-grey-dark: #2c2c2e;
$uni-bg-color-hover-dark: #3a3a3c;
$uni-bg-color-mask-dark: rgba(0, 0, 0, 0.6);
$uni-border-color-dark: #434346;
/* 默认使用亮色模式颜色 */
$uni-color-primary: $uni-color-primary-light;
$uni-color-success: $uni-color-success-light;
$uni-color-warning: $uni-color-warning-light;
$uni-color-error: $uni-color-error-light;
$uni-text-color: $uni-text-color-light;
$uni-text-color-inverse: $uni-text-color-inverse-light;
$uni-text-color-grey: $uni-text-color-grey-light;
$uni-text-color-placeholder: $uni-text-color-placeholder-light;
$uni-text-color-disable: $uni-text-color-disable-light;
$uni-bg-color: $uni-bg-color-light;
$uni-bg-color-grey: $uni-bg-color-grey-light;
$uni-bg-color-hover: $uni-bg-color-hover-light;
$uni-bg-color-mask: $uni-bg-color-mask-light;
$uni-border-color: $uni-border-color-light;

View File

@@ -3,7 +3,6 @@ import uni from '@dcloudio/vite-plugin-uni'
import { resolve } from 'path' import { resolve } from 'path'
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs' import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import dotenv from 'dotenv' import dotenv from 'dotenv'
// 自定义插件:替换 manifest.json 中的 appid // 自定义插件:替换 manifest.json 中的 appid
// 仅在通过HBuilder首次编译时执行避免热重载时重复执行导致内存占用高 // 仅在通过HBuilder首次编译时执行避免热重载时重复执行导致内存占用高
function replaceManifestAppid() { function replaceManifestAppid() {
@@ -14,19 +13,15 @@ function replaceManifestAppid() {
// 检查是否已经执行过插件 // 检查是否已经执行过插件
const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated') const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated')
const isFirstCompile = !existsSync(manifestUpdatedFlag) const isFirstCompile = !existsSync(manifestUpdatedFlag)
// 仅首次编译时执行 // 仅首次编译时执行
if (!isFirstCompile) { if (!isFirstCompile) {
console.log('跳过 manifest appid 更新(已执行过首次编译)') console.log('跳过 manifest appid 更新(已执行过首次编译)')
return return
} }
// 获取环境变量,明确指定路径为项目根目录 // 获取环境变量,明确指定路径为项目根目录
dotenv.config({ path: resolve(__dirname, '.env') }) dotenv.config({ path: resolve(__dirname, '.env') })
const appid = process.env.VITE_APPID const appid = process.env.VITE_APPID
const uni_appId = process.env.VITE_UNI_APPID const uni_appId = process.env.VITE_UNI_APPID
const libVersion = process.env.VITE_LIBVERSION || '3.0.0'
if (appid && uni_appId) { if (appid && uni_appId) {
// 读取 manifest.json 文件 // 读取 manifest.json 文件
const manifestPath = resolve(__dirname, 'manifest.json') const manifestPath = resolve(__dirname, 'manifest.json')
@@ -35,12 +30,9 @@ function replaceManifestAppid() {
const manifest = JSON.parse(manifestContent) const manifest = JSON.parse(manifestContent)
// 替换 // 替换
manifest.appid = uni_appId manifest.appid = uni_appId
if (manifest['mp-weixin']) { if (manifest['mp-weixin']) {
manifest['mp-weixin'].appid = appid manifest['mp-weixin'].appid = appid
manifest['mp-weixin'].libVersion = libVersion
} }
// 写回文件 // 写回文件
writeFileSync(manifestPath, JSON.stringify(manifest, null, 4)) writeFileSync(manifestPath, JSON.stringify(manifest, null, 4))
// 创建标记文件,表示已执行过插件 // 创建标记文件,表示已执行过插件
@@ -53,21 +45,18 @@ function replaceManifestAppid() {
}, },
} }
} }
// 检查是否通过HBuilder编译决定是否启用插件 // 检查是否通过HBuilder编译决定是否启用插件
const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated') const manifestUpdatedFlag = resolve(__dirname, '.manifest-updated')
if (existsSync(manifestUpdatedFlag)) { if (existsSync(manifestUpdatedFlag)) {
unlinkSync(manifestUpdatedFlag) unlinkSync(manifestUpdatedFlag)
console.log('已清理 manifest 更新标记文件') console.log('已清理 manifest 更新标记文件')
} }
const plugins = manifestUpdatedFlag ? [replaceManifestAppid(), uni()] : [uni()] const plugins = manifestUpdatedFlag ? [replaceManifestAppid(), uni()] : [uni()]
export default defineConfig({ export default defineConfig({
plugins, plugins,
resolve: { resolve: {
alias: { alias: {
'@': '/src', '@': resolve(__dirname, 'src'),
}, },
}, },
build: { build: {
@@ -75,6 +64,55 @@ export default defineConfig({
terserOptions: { terserOptions: {
compress: { compress: {
drop_console: true, drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log'], // 移除console.log
},
},
rollupOptions: {
output: {
// 静态资源分类打包
assetFileNames: (assetInfo) => {
let extType = assetInfo.name.split('.').at(1)
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
extType = 'img'
} else if (/woff|woff2|eot|ttf|otf/i.test(extType)) {
extType = 'font'
}
return `static/${extType}/[name]-[hash].[ext]`
},
// 分包
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
manualChunks: {
// 将第三方库单独打包
vue: ['vue'],
'uview-plus': ['uview-plus'],
'luch-request': ['luch-request'],
},
},
},
// 启用 CSS 代码分割
cssCodeSplit: true,
// 小于此阈值的导入或引用资源将内联为 base64 编码
assetsInlineLimit: 4096,
// 启用 gzip 压缩
brotliSize: true,
},
// 优化选项
optimizeDeps: {
include: ['vue', 'uview-plus', 'luch-request'],
},
// 开发服务器配置
server: {
host: '0.0.0.0',
port: 3000,
open: true,
// 代理配置
proxy: {
'/api': {
target: process.env.VITE_BASE_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}, },
}, },
}, },

View File

@@ -1,54 +0,0 @@
/* 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

@@ -1,361 +0,0 @@
/**
* 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

@@ -1,102 +0,0 @@
/* 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

@@ -1,734 +0,0 @@
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

@@ -1,784 +0,0 @@
/* 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

@@ -1,97 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,611 +0,0 @@
// @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

@@ -1,864 +0,0 @@
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

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

View File

@@ -1,21 +0,0 @@
<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>