diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e8105f2bbf5568eabd38e62486fe01ef564a466b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# 依赖和包管理
+node_modules/
+package-lock.json
+miniprogram_npm/
+
+# 微信小程序私有配置文件
+project.private.config.json
+
+# 系统文件
+.DS_Store
+*.log
+.vscode/
+.idea/
+
+# 敏感配置文件
+config/api-config.local.js
+config/secrets.js
+*.env
+.env*
+
+# 临时文件
+*.tmp
+*.temp
+.cache/
+
+# 开发和调试文件
+debug.log
+error.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# 微信开发者工具生成的文件
+.tea/
\ No newline at end of file
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
new file mode 100644
index 0000000000000000000000000000000000000000..a79c408d11d0a4b7cc6bcdfdd897e8b8997d0b31
--- /dev/null
+++ b/DEPLOYMENT.md
@@ -0,0 +1,125 @@
+# 部署指南
+
+## 🚀 快速部署
+
+### 1. 配置小程序AppID
+
+在 `project.config.json` 文件中,将 `appid` 字段替换为您自己的小程序AppID:
+
+```json
+{
+ "appid": "您的小程序AppID"
+}
+```
+
+### 2. 配置API服务器
+
+1. 复制 `config/api-config.example.js` 为 `config/api-config.js`
+2. 修改其中的配置信息:
+
+```javascript
+const ApiConfig = {
+ server: {
+ baseUrl: 'https://您的API服务器地址.com',
+ // 其他配置...
+ },
+ auth: {
+ corpCode: '您的企业代码',
+ storageKeys: {
+ // ⚠️ 重要:以下键名必须根据您的服务器API规范进行调整
+ accessToken: 'your_server_access_token_key',
+ userCode: 'your_server_user_code_key',
+ userInfo: 'user_info',
+ isLoggedIn: 'is_logged_in'
+ }
+ // 其他配置...
+ }
+};
+```
+
+> **重要说明**:`storageKeys` 中的键名是根据不同服务器的API规范来设定的。不同的后端服务可能使用不同的命名约定,请根据您的服务器API文档进行相应调整。
+
+### 3. 环境变量配置(可选)
+
+如果您使用环境变量管理配置,可以创建 `.env` 文件:
+
+```env
+API_BASE_URL=https://your-api-server.com
+CORP_CODE=your_corp_code
+WECHAT_APP_ID=your_wechat_app_id
+```
+
+### 4. 私有配置
+
+`project.private.config.json` 文件包含开发者工具的私有配置,请根据需要调整:
+
+- 删除或修改其中的个人配置
+- 该文件已被 `.gitignore` 忽略,不会被提交到版本控制
+
+## ⚙️ 详细配置说明
+
+### StorageKeys 配置
+
+`storageKeys` 配置是与您的后端服务密切相关的,不同的服务器可能使用完全不同的命名规范:
+
+**示例对比:**
+
+```javascript
+// 某些服务器使用带前缀的格式
+storageKeys: {
+ accessToken: 'S-Access-Token',
+ userCode: 'S-Code'
+}
+
+// 另一些服务器使用标准格式
+storageKeys: {
+ accessToken: 'access_token',
+ userCode: 'user_id'
+}
+
+// 还有些服务器使用驼峰命名
+storageKeys: {
+ accessToken: 'accessToken',
+ userCode: 'userCode'
+}
+```
+
+**如何确定正确的键名:**
+1. 查看您的服务器API文档
+2. 检查登录接口返回的Header字段名
+3. 咨询后端开发人员确认存储键的命名规范
+
+## 📋 部署检查清单
+
+在发布到生产环境前,请确认:
+
+- [ ] 已替换所有示例配置为真实配置
+- [ ] **已根据服务器API规范调整storageKeys配置**
+- [ ] 移除或替换测试数据
+- [ ] 检查所有API端点是否正确
+- [ ] 验证小程序AppID是否正确
+- [ ] 测试登录和认证功能是否正常
+- [ ] 测试所有主要功能是否正常
+- [ ] 确认权限配置是否完整
+
+## 🔒 安全建议
+
+1. **永远不要**将真实的AppID、密钥等敏感信息提交到公开仓库
+2. 使用环境变量或私有配置文件管理敏感信息
+3. 定期检查和更新访问令牌
+4. 在生产环境中禁用测试和调试功能
+
+## 🛠️ 开发环境
+
+如果您需要在开发环境中使用不同的配置,可以:
+
+1. 创建 `config/api-config.local.js` 文件(已被gitignore忽略)
+2. 在代码中根据环境加载不同的配置文件
+
+```javascript
+// 示例:根据环境加载配置
+const isDev = wx.getSystemInfoSync().platform === 'devtools';
+const config = isDev ?
+ require('./api-config.local.js') :
+ require('./api-config.js');
+```
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index b257232e5540fe57c3be48c39d9aa72a408fda78..211bd9a1af5a646087687382a13318320ea321b0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 嬲爆爆
+Copyright (c) 2024 小籽二维码
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+SOFTWARE.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2e52546c9ee81a63f037f1d3742a034a5b252025
--- /dev/null
+++ b/README.md
@@ -0,0 +1,187 @@
+# 小籽二维码 - 微信小程序
+
+
+

+
扫码生成 · 简单便捷
+
一款功能完整的微信小程序二维码工具,支持扫码识别、生成二维码和条形码等功能
+
+
+## ✨ 主要功能
+
+### 🔍 扫码识别
+- **智能识别**:支持二维码、条形码等多种格式
+- **相册扫码**:可从相册选择图片进行识别
+- **多种模式**:自定义摄像头和微信API两种扫码模式
+- **结果处理**:自动识别URL、WiFi、联系人、电话等信息类型
+
+### 📱 二维码生成
+- **多类型支持**:文本、链接、WiFi、联系人信息
+- **自定义样式**:支持多种样式和颜色设置
+- **高清输出**:生成高质量二维码图片
+- **保存分享**:可保存到相册或分享给好友
+
+### 📊 条形码生成
+- **多格式支持**:Code128、EAN13、Code39等标准格式
+- **商品条码**:支持商品标准条形码生成
+- **自定义配置**:可调整尺寸、颜色等参数
+
+### 📝 扫码记录
+- **历史记录**:自动保存所有扫码和生成记录
+- **分类管理**:按类型和时间分类显示
+- **快速操作**:支持复制、分享、删除等操作
+- **数据统计**:显示使用统计和记录数量
+
+## 🛠️ 技术特点
+
+- **iOS风格界面**:采用现代化iOS设计风格,用户体验友好
+- **响应式布局**:适配不同屏幕尺寸的设备
+- **离线支持**:核心功能支持离线使用
+- **性能优化**:采用懒加载和组件化开发
+- **数据持久化**:使用本地存储保存用户数据
+
+## 📁 项目结构
+
+```
+QrCode/
+├── app.js # 应用入口文件
+├── app.json # 应用配置文件
+├── app.wxss # 全局样式文件
+├── assets/ # 静态资源目录
+│ ├── icons/ # 图标文件
+│ └── js/ # 第三方JS库
+├── config/ # 配置文件目录
+│ └── api-config.js # API配置
+├── pages/ # 页面目录
+│ ├── index/ # 首页
+│ ├── custom-scan/ # 自定义扫码页面
+│ ├── generate-qr/ # 二维码生成页面
+│ ├── generate-barcode/ # 条形码生成页面
+│ ├── generate-result/ # 生成结果页面
+│ └── history/ # 历史记录页面
+└── utils/ # 工具类目录
+ ├── api-service.js # API服务
+ ├── history-manager.js # 历史记录管理
+ ├── qrcode.js # 二维码工具
+ └── util.js # 通用工具函数
+```
+
+## 🚀 快速开始
+
+### 环境要求
+- 微信开发者工具
+- Node.js (可选,用于开发工具)
+
+### 安装步骤
+
+1. **克隆项目**
+ ```bash
+ git clone [项目地址]
+ cd QrCode
+ ```
+
+2. **配置项目**
+ - 复制 `config/api-config.js` 为 `config/api-config.local.js`
+ - 在 `config/api-config.local.js` 中填入您的API服务器地址和配置
+ - 在 `project.config.json` 中填入您的小程序AppID
+
+ > 💡 **配置说明**:项目会优先使用 `api-config.local.js`(不会上传到Git),如果不存在则使用默认的 `api-config.js`
+
+3. **微信开发者工具导入**
+ - 打开微信开发者工具
+ - 选择"导入项目"
+ - 选择项目目录
+ - 确认AppID配置正确
+
+4. **预览和调试**
+ - 点击"编译"按钮
+ - 使用"真机调试"测试摄像头功能
+ - 可使用"预览"功能在手机上体验
+
+> ⚠️ **重要提醒**:首次使用前请仔细阅读 [DEPLOYMENT.md](DEPLOYMENT.md) 文件了解如何正确配置敏感信息。
+
+## 📋 功能详情
+
+### 扫码功能
+- 支持QR码、Data Matrix、PDF417等二维码格式
+- 支持EAN、UPC、Code128等条形码格式
+- 自动识别内容类型(URL、文本、WiFi配置等)
+- 提供历史记录和快速操作
+
+### 生成功能
+- **二维码生成**:支持文本、URL、WiFi、联系人信息
+- **条形码生成**:支持商品码、自定义文本编码
+- **样式自定义**:颜色、尺寸、边距等参数调整
+- **批量操作**:支持批量生成和导出
+
+### 历史管理
+- 自动保存所有操作记录
+- 按日期、类型分类展示
+- 支持搜索和筛选
+- 数据导出和备份功能
+
+## 🎨 界面预览
+
+- **首页**:功能导航和快速操作
+- **扫码页**:实时摄像头预览和识别
+- **生成页**:参数配置和实时预览
+- **历史页**:记录列表和详情查看
+
+## 🔧 自定义配置
+
+### API配置
+修改 `config/api-config.js` 文件来配置后端服务地址:
+
+```javascript
+module.exports = {
+ baseUrl: 'https://your-api-domain.com',
+ timeout: 10000,
+ // 其他配置...
+}
+```
+
+### 功能开关
+在 `app.js` 中可以配置功能开关:
+
+```javascript
+globalData: {
+ enableOfflineMode: true, // 离线模式
+ enableHistory: true, // 历史记录
+ enableShare: true, // 分享功能
+}
+```
+
+## 📱 兼容性
+
+- **微信版本**:要求微信7.0.0及以上版本
+- **系统支持**:iOS 10.0+、Android 5.0+
+- **功能权限**:需要摄像头、相册访问权限
+
+## 🤝 贡献指南
+
+欢迎提交Issue和Pull Request来帮助改进项目!
+
+1. Fork 项目
+2. 创建功能分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 打开 Pull Request
+
+## 📄 开源协议
+
+本项目采用 MIT 协议开源 - 查看 [LICENSE](LICENSE) 文件了解详情。
+
+## 👨💻 作者
+
+- 如有疑问或建议,欢迎提交Issue
+
+## 🙏 致谢
+
+- 感谢微信小程序平台提供的开发框架
+- 感谢开源社区提供的各种工具库和组件
+- 感谢所有贡献者和用户的支持
+
+---
+
+
+
⭐ 如果这个项目对你有帮助,请给它一个星标!
+
\ No newline at end of file
diff --git a/app.js b/app.js
new file mode 100644
index 0000000000000000000000000000000000000000..4fad964e993b0331a312f13e2d66b21c59eed4c8
--- /dev/null
+++ b/app.js
@@ -0,0 +1,205 @@
+// app.js
+const apiService = require('./utils/api-service.js');
+const MessageUtil = require('./utils/message-util.js');
+
+App({
+ onLaunch() {
+ console.log('小程序启动');
+
+ // 展示本地存储能力
+ const logs = wx.getStorageSync('logs') || []
+ logs.unshift(Date.now())
+ wx.setStorageSync('logs', logs)
+
+ // 设置初始化状态
+ this.globalData.isInitializing = true;
+ this.globalData.initializationError = null;
+
+ // 静默登录
+ this.performSilentLogin();
+ },
+
+ // 执行静默登录
+ async performSilentLogin() {
+ try {
+ console.log('开始静默登录...');
+
+ // 检查是否已经登录
+ if (apiService.isUserLoggedIn()) {
+ console.log('用户已登录,跳过静默登录');
+ this.globalData.userInfo = apiService.userInfo;
+ this.globalData.isLoggedIn = true;
+ this.globalData.isOfflineMode = false;
+ this.finishInitialization(true);
+ return;
+ }
+
+ // 执行静默登录
+ const result = await apiService.silentLogin();
+
+ if (result.success) {
+ console.log('静默登录成功:', result.userInfo);
+ this.globalData.userInfo = result.userInfo;
+ this.globalData.isLoggedIn = true;
+
+ // 可以在这里触发其他需要登录状态的操作
+ this.onLoginSuccess();
+ this.finishInitialization(true);
+ }
+ } catch (error) {
+ console.error('静默登录失败:', error);
+ this.globalData.isLoggedIn = false;
+ this.globalData.initializationError = error;
+
+ // 可以在这里处理登录失败的逻辑
+ this.onLoginFailed(error);
+ this.finishInitialization(false);
+ }
+ },
+
+ // 完成初始化
+ finishInitialization(success) {
+ console.log('初始化完成,成功:', success);
+ this.globalData.isInitializing = false;
+
+ // 通知所有页面初始化完成
+ this.notifyPagesInitComplete(success);
+ },
+
+ // 通知页面初始化完成
+ notifyPagesInitComplete(success) {
+ // 获取当前页面栈
+ const pages = getCurrentPages();
+ if (pages.length > 0) {
+ const currentPage = pages[pages.length - 1];
+
+ // 如果当前页面有初始化完成的回调,则调用
+ if (typeof currentPage.onAppInitComplete === 'function') {
+ currentPage.onAppInitComplete(success);
+ }
+ }
+ },
+
+ // 检查是否正在初始化
+ isInitializing() {
+ return this.globalData.isInitializing;
+ },
+
+ // 获取初始化错误
+ getInitializationError() {
+ return this.globalData.initializationError;
+ },
+
+ // 登录成功回调
+ onLoginSuccess() {
+ console.log('登录成功,可以进行后续操作');
+
+ // 检查是否为测试模式,如果是测试模式则不清除手动设置
+ const isTestMode = this.isTestMode();
+ if (isTestMode) {
+ console.log('🧪 测试模式:保持当前手动设置的离线状态');
+ return;
+ }
+
+ // 清除离线模式状态
+ this.globalData.isOfflineMode = false;
+
+ // 清除离线状态存储
+ try {
+ wx.removeStorageSync('app_offline_mode');
+ } catch (e) {
+ console.error('清除离线状态失败:', e);
+ }
+ },
+
+ // 登录失败回调
+ onLoginFailed(error) {
+ console.log('登录失败,进入离线模式');
+
+ // 检查是否为测试模式,如果是测试模式则不覆盖手动设置
+ const isTestMode = this.isTestMode();
+ if (isTestMode) {
+ console.log('🧪 测试模式:保持当前手动设置的离线状态');
+ return;
+ }
+
+ // 设置离线模式状态
+ this.globalData.isOfflineMode = true;
+
+ // 保存离线状态到本地存储
+ try {
+ wx.setStorageSync('app_offline_mode', true);
+ } catch (e) {
+ console.error('保存离线状态失败:', e);
+ }
+ },
+
+ // 获取登录状态
+ isLoggedIn() {
+ return this.globalData.isLoggedIn && apiService.isUserLoggedIn();
+ },
+
+ // 检查是否为离线模式
+ isOfflineMode() {
+ // 先检查内存状态
+ if (this.globalData.isOfflineMode !== undefined) {
+ return this.globalData.isOfflineMode;
+ }
+
+ // 从本地存储检查
+ try {
+ const offlineMode = wx.getStorageSync('app_offline_mode');
+ const testMode = wx.getStorageSync('test_offline_mode');
+
+ // 如果是测试模式,显示额外日志
+ if (testMode) {
+ console.log('🧪 测试模式:手动设置的离线状态 =', !!offlineMode);
+ }
+
+ this.globalData.isOfflineMode = !!offlineMode;
+ return this.globalData.isOfflineMode;
+ } catch (e) {
+ console.error('读取离线状态失败:', e);
+ return false;
+ }
+ },
+
+ // 检查是否为测试模式
+ isTestMode() {
+ try {
+ return !!wx.getStorageSync('test_offline_mode');
+ } catch (e) {
+ return false;
+ }
+ },
+
+ // 获取用户信息
+ getUserInfo() {
+ return this.globalData.userInfo;
+ },
+
+ // 手动重新登录
+ async reLogin() {
+ try {
+ const result = await apiService.silentLogin();
+ if (result.success) {
+ this.globalData.userInfo = result.userInfo;
+ this.globalData.isLoggedIn = true;
+ this.onLoginSuccess();
+ return true;
+ }
+ } catch (error) {
+ console.error('重新登录失败:', error);
+ MessageUtil.showServiceError();
+ }
+ return false;
+ },
+
+ globalData: {
+ userInfo: null,
+ isLoggedIn: false,
+ isOfflineMode: false,
+ isInitializing: false,
+ initializationError: null
+ }
+})
diff --git a/app.json b/app.json
new file mode 100644
index 0000000000000000000000000000000000000000..6a2a19bc63eb83983c3f9ec82d7ffbfc7097d757
--- /dev/null
+++ b/app.json
@@ -0,0 +1,34 @@
+{
+ "pages": [
+ "pages/index/index",
+ "pages/custom-scan/custom-scan",
+ "pages/generate-qr/generate-qr",
+ "pages/generate-barcode/generate-barcode",
+ "pages/generate-result/generate-result",
+ "pages/history/history",
+ "pages/logs/logs"
+ ],
+ "window": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "小籽二维码",
+ "navigationBarBackgroundColor": "#ffffff",
+ "backgroundColor": "#f8f9fa",
+ "backgroundTextStyle": "dark",
+ "enablePullDownRefresh": true
+ },
+ "permission": {
+ "scope.camera": {
+ "desc": "用于扫描二维码和条形码,以及摄像头功能测试"
+ },
+ "scope.writePhotosAlbum": {
+ "desc": "用于保存生成的二维码和拍摄的照片到相册"
+ }
+ },
+ "darkmode": true,
+ "themeLocation": "theme.json",
+ "requiredBackgroundModes": ["audio"],
+ "style": "v2",
+ "componentFramework": "glass-easel",
+ "sitemapLocation": "sitemap.json",
+ "lazyCodeLoading": "requiredComponents"
+}
diff --git a/app.wxss b/app.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..022143203a716bb1c8cfd5d067cc8cf030d072a9
--- /dev/null
+++ b/app.wxss
@@ -0,0 +1,14 @@
+/**app.wxss**/
+
+/* 引入全局图标样式 */
+@import "./assets/icons/icons.wxss";
+
+.container {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: space-between;
+ padding: 200rpx 0, 0, 0;
+ box-sizing: border-box;
+}
diff --git a/assets/icons/album.png b/assets/icons/album.png
new file mode 100644
index 0000000000000000000000000000000000000000..88e212df79117b0417c7c22d0f1910f6f243a82e
Binary files /dev/null and b/assets/icons/album.png differ
diff --git a/assets/icons/back.png b/assets/icons/back.png
new file mode 100644
index 0000000000000000000000000000000000000000..c4714b19151ca8b8dae796f168fc40329b091c16
Binary files /dev/null and b/assets/icons/back.png differ
diff --git a/assets/icons/bar-code.png b/assets/icons/bar-code.png
new file mode 100644
index 0000000000000000000000000000000000000000..f500f2525c63d5122149b7781be823af158e19af
Binary files /dev/null and b/assets/icons/bar-code.png differ
diff --git a/assets/icons/bar_code_b.png b/assets/icons/bar_code_b.png
new file mode 100644
index 0000000000000000000000000000000000000000..f26045e53ce840798b1d6d4e9a94c3dcb4f460d9
Binary files /dev/null and b/assets/icons/bar_code_b.png differ
diff --git a/assets/icons/camera.png b/assets/icons/camera.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd3e0ed4470d8c78706a2c13c6cfaabf00f4e870
Binary files /dev/null and b/assets/icons/camera.png differ
diff --git a/assets/icons/camera_switch.png b/assets/icons/camera_switch.png
new file mode 100644
index 0000000000000000000000000000000000000000..2344a9a98b5bb82bdbf4cb2678ad69cf71253713
Binary files /dev/null and b/assets/icons/camera_switch.png differ
diff --git a/assets/icons/cloud-offline.png b/assets/icons/cloud-offline.png
new file mode 100644
index 0000000000000000000000000000000000000000..85ca54545af29687743c463c40fea450db2f2498
Binary files /dev/null and b/assets/icons/cloud-offline.png differ
diff --git a/assets/icons/contact.png b/assets/icons/contact.png
new file mode 100644
index 0000000000000000000000000000000000000000..c05ca4590667e03869b2f3f9b576c3b3a3e141e2
Binary files /dev/null and b/assets/icons/contact.png differ
diff --git a/assets/icons/email.png b/assets/icons/email.png
new file mode 100644
index 0000000000000000000000000000000000000000..722a89a61aee51e392b75f60ad21b10140d147dc
Binary files /dev/null and b/assets/icons/email.png differ
diff --git a/assets/icons/flash_off.png b/assets/icons/flash_off.png
new file mode 100644
index 0000000000000000000000000000000000000000..629569209d5240f538e22d0b4cedd68a95bbf187
Binary files /dev/null and b/assets/icons/flash_off.png differ
diff --git a/assets/icons/flash_on.png b/assets/icons/flash_on.png
new file mode 100644
index 0000000000000000000000000000000000000000..85b905f8ba9acd58aa54420404b7f1085ffd39b4
Binary files /dev/null and b/assets/icons/flash_on.png differ
diff --git a/assets/icons/history.png b/assets/icons/history.png
new file mode 100644
index 0000000000000000000000000000000000000000..31d77f5d05163e040d76fe118406ca815722fbbb
Binary files /dev/null and b/assets/icons/history.png differ
diff --git a/assets/icons/history_list.png b/assets/icons/history_list.png
new file mode 100644
index 0000000000000000000000000000000000000000..2bed379ea2b7b71ee0c82444770467ee24ac9ecf
Binary files /dev/null and b/assets/icons/history_list.png differ
diff --git a/assets/icons/icons.wxss b/assets/icons/icons.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..6f0c64624700505031f77a89687a5e595115c43e
--- /dev/null
+++ b/assets/icons/icons.wxss
@@ -0,0 +1,61 @@
+/* 本地图标样式文件 */
+
+/* 图标基础样式 */
+.icon {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Symbols', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
+ font-weight: normal;
+ font-style: normal;
+ display: inline-block;
+ line-height: 1;
+ text-decoration: none;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* ionicons替代方案 - 使用专业Unicode字符 */
+.ion-scan-outline::before {
+ content: "⧇"; /* 扫描图标 */
+ font-size: 1.1em;
+}
+
+.ion-qr-code-outline::before {
+ content: "▦"; /* 二维码图标 */
+}
+
+.ion-barcode-outline::before {
+ content: "⦀"; /* 条形码图标 */
+}
+
+.ion-time-outline::before {
+ content: "⏰"; /* 时间图标 */
+}
+
+.ion-settings-outline::before {
+ content: "⚙"; /* 设置图标 */
+}
+
+.ion-gift-outline::before {
+ content: "🎁"; /* 礼品图标 */
+}
+
+.ion-close-outline::before {
+ content: "✕"; /* 关闭图标 */
+ font-size: 0.9em;
+}
+
+.ion-chevron-forward-outline::before {
+ content: "❯"; /* 前进箭头 */
+ font-size: 0.8em;
+}
+
+/* 备用方案 - 如果上面的字符显示有问题 */
+.ion-scan-outline.fallback::before {
+ content: "📷";
+}
+
+.ion-qr-code-outline.fallback::before {
+ content: "⬛";
+}
+
+.ion-barcode-outline.fallback::before {
+ content: "📊";
+}
\ No newline at end of file
diff --git a/assets/icons/link.png b/assets/icons/link.png
new file mode 100644
index 0000000000000000000000000000000000000000..47f264aaec427c37ad655d5793994d729f674a0d
Binary files /dev/null and b/assets/icons/link.png differ
diff --git a/assets/icons/location.png b/assets/icons/location.png
new file mode 100644
index 0000000000000000000000000000000000000000..1ec0fc9291d7cbe10857405a3078dd9d254773f4
Binary files /dev/null and b/assets/icons/location.png differ
diff --git a/assets/icons/micro_qrcode.png b/assets/icons/micro_qrcode.png
new file mode 100644
index 0000000000000000000000000000000000000000..9f6961030aae97c773281387605194a106dde1e3
Binary files /dev/null and b/assets/icons/micro_qrcode.png differ
diff --git a/assets/icons/phone.png b/assets/icons/phone.png
new file mode 100644
index 0000000000000000000000000000000000000000..20fc9349dd39e93317159bde1eafef7879e303f2
Binary files /dev/null and b/assets/icons/phone.png differ
diff --git a/assets/icons/qrcode.png b/assets/icons/qrcode.png
new file mode 100644
index 0000000000000000000000000000000000000000..6f9c78a8604adb4c95723d9757504a6e6e0c61fc
Binary files /dev/null and b/assets/icons/qrcode.png differ
diff --git a/assets/icons/qrcode12.png b/assets/icons/qrcode12.png
new file mode 100644
index 0000000000000000000000000000000000000000..58970436ff1b6e68f16f2be19da22db30dc35be7
Binary files /dev/null and b/assets/icons/qrcode12.png differ
diff --git a/assets/icons/qrcode_b.png b/assets/icons/qrcode_b.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d6bb3a7488418f3674c8a6835c71f09cf48b75d
Binary files /dev/null and b/assets/icons/qrcode_b.png differ
diff --git a/assets/icons/qrcode_logo.png b/assets/icons/qrcode_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a7a03805cebe91830a2e4e5e11e89ce98d713c6
Binary files /dev/null and b/assets/icons/qrcode_logo.png differ
diff --git a/assets/icons/scan.png b/assets/icons/scan.png
new file mode 100644
index 0000000000000000000000000000000000000000..a90ca78e59a28a19f65dda6287515b84c182bdf7
Binary files /dev/null and b/assets/icons/scan.png differ
diff --git a/assets/icons/setting.png b/assets/icons/setting.png
new file mode 100644
index 0000000000000000000000000000000000000000..acca5330203cd60556bd7687279c6095d4a00b31
Binary files /dev/null and b/assets/icons/setting.png differ
diff --git a/assets/icons/text.png b/assets/icons/text.png
new file mode 100644
index 0000000000000000000000000000000000000000..e32e8f6bab568b9b20543e481bef09fa7c764ea9
Binary files /dev/null and b/assets/icons/text.png differ
diff --git a/assets/icons/tick_b.png b/assets/icons/tick_b.png
new file mode 100644
index 0000000000000000000000000000000000000000..23e6cfd3e4362a1de4b3190a06371c1bbeddebca
Binary files /dev/null and b/assets/icons/tick_b.png differ
diff --git a/assets/icons/tick_w.png b/assets/icons/tick_w.png
new file mode 100644
index 0000000000000000000000000000000000000000..72a1ae7eac3239bf2b84731971fa164ee9fbf506
Binary files /dev/null and b/assets/icons/tick_w.png differ
diff --git a/assets/icons/wifi.png b/assets/icons/wifi.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a7d76800bad5fa5d9e51f1685019b6f01ebc7ae
Binary files /dev/null and b/assets/icons/wifi.png differ
diff --git a/assets/js/weapp.qrcode.esm.js b/assets/js/weapp.qrcode.esm.js
new file mode 100644
index 0000000000000000000000000000000000000000..723127029dba0f282ef8dc53600390581f7d92a0
--- /dev/null
+++ b/assets/js/weapp.qrcode.esm.js
@@ -0,0 +1,5 @@
+/**
+ * weapp.qrcode.js v1.0.0 (https://github.com/yingye/weapp-qrcode#readme)
+ */
+
+var hasOwn=Object.prototype.hasOwnProperty,toStr=Object.prototype.toString,defineProperty=Object.defineProperty,gOPD=Object.getOwnPropertyDescriptor,isArray=function(t){return"function"==typeof Array.isArray?Array.isArray(t):"[object Array]"===toStr.call(t)},isPlainObject=function(t){if(!t||"[object Object]"!==toStr.call(t))return!1;var e,r=hasOwn.call(t,"constructor"),o=t.constructor&&t.constructor.prototype&&hasOwn.call(t.constructor.prototype,"isPrototypeOf");if(t.constructor&&!r&&!o)return!1;for(e in t);return void 0===e||hasOwn.call(t,e)},setProperty=function(t,e){defineProperty&&"__proto__"===e.name?defineProperty(t,e.name,{enumerable:!0,configurable:!0,value:e.newValue,writable:!0}):t[e.name]=e.newValue},getProperty=function(t,e){if("__proto__"===e){if(!hasOwn.call(t,e))return;if(gOPD)return gOPD(t,e).value}return t[e]},extend=function t(){var e,r,o,n,i,a,s=arguments[0],u=1,l=arguments.length,h=!1;for("boolean"==typeof s&&(h=s,s=arguments[1]||{},u=2),(null==s||"object"!=typeof s&&"function"!=typeof s)&&(s={});u=7&&this.setupTypeNumber(t),null==this.dataCache&&(this.dataCache=QRCode.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,e)},setupPositionProbePattern:function(t,e){for(var r=-1;r<=7;r++)if(!(t+r<=-1||this.moduleCount<=t+r))for(var o=-1;o<=7;o++)e+o<=-1||this.moduleCount<=e+o||(this.modules[t+r][e+o]=0<=r&&r<=6&&(0==o||6==o)||0<=o&&o<=6&&(0==r||6==r)||2<=r&&r<=4&&2<=o&&o<=4)},getBestMaskPattern:function(){for(var t=0,e=0,r=0;r<8;r++){this.makeImpl(!0,r);var o=QRUtil.getLostPoint(this);(0==r||t>o)&&(t=o,e=r)}return e},createMovieClip:function(t,e,r){var o=t.createEmptyMovieClip(e,r);this.make();for(var n=0;n>r&1);this.modules[Math.floor(r/3)][r%3+this.moduleCount-8-3]=o}for(r=0;r<18;r++){o=!t&&1==(e>>r&1);this.modules[r%3+this.moduleCount-8-3][Math.floor(r/3)]=o}},setupTypeInfo:function(t,e){for(var r=this.errorCorrectLevel<<3|e,o=QRUtil.getBCHTypeInfo(r),n=0;n<15;n++){var i=!t&&1==(o>>n&1);n<6?this.modules[n][8]=i:n<8?this.modules[n+1][8]=i:this.modules[this.moduleCount-15+n][8]=i}for(n=0;n<15;n++){i=!t&&1==(o>>n&1);n<8?this.modules[8][this.moduleCount-n-1]=i:n<9?this.modules[8][15-n-1+1]=i:this.modules[8][15-n-1]=i}this.modules[this.moduleCount-8][8]=!t},mapData:function(t,e){for(var r=-1,o=this.moduleCount-1,n=7,i=0,a=this.moduleCount-1;a>0;a-=2)for(6==a&&a--;;){for(var s=0;s<2;s++)if(null==this.modules[o][a-s]){var u=!1;i>>n&1)),QRUtil.getMask(e,o,a-s)&&(u=!u),this.modules[o][a-s]=u,-1==--n&&(i++,n=7)}if((o+=r)<0||this.moduleCount<=o){o-=r,r=-r;break}}}},QRCode.PAD0=236,QRCode.PAD1=17,QRCode.createData=function(t,e,r){for(var o=QRRSBlock.getRSBlocks(t,e),n=new QRBitBuffer,i=0;i8*s)throw new Error("code length overflow. ("+n.getLengthInBits()+">"+8*s+")");for(n.getLengthInBits()+4<=8*s&&n.put(0,4);n.getLengthInBits()%8!=0;)n.putBit(!1);for(;!(n.getLengthInBits()>=8*s||(n.put(QRCode.PAD0,8),n.getLengthInBits()>=8*s));)n.put(QRCode.PAD1,8);return QRCode.createBytes(n,o)},QRCode.createBytes=function(t,e){for(var r=0,o=0,n=0,i=new Array(e.length),a=new Array(e.length),s=0;s=0?g.get(c):0}}var d=0;for(h=0;h=0;)e^=QRUtil.G15<=0;)e^=QRUtil.G18<>>=1;return e},getPatternPosition:function(t){return QRUtil.PATTERN_POSITION_TABLE[t-1]},getMask:function(t,e,r){switch(t){case QRMaskPattern.PATTERN000:return(e+r)%2==0;case QRMaskPattern.PATTERN001:return e%2==0;case QRMaskPattern.PATTERN010:return r%3==0;case QRMaskPattern.PATTERN011:return(e+r)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(e/2)+Math.floor(r/3))%2==0;case QRMaskPattern.PATTERN101:return e*r%2+e*r%3==0;case QRMaskPattern.PATTERN110:return(e*r%2+e*r%3)%2==0;case QRMaskPattern.PATTERN111:return(e*r%3+(e+r)%2)%2==0;default:throw new Error("bad maskPattern:"+t)}},getErrorCorrectPolynomial:function(t){for(var e=new QRPolynomial([1],0),r=0;r5&&(r+=3+i-5)}for(o=0;o=256;)t-=255;return QRMath.EXP_TABLE[t]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},i=0;i<8;i++)QRMath.EXP_TABLE[i]=1<=1&&n<=127?e+=t.charAt(r):n>2047?(e+=String.fromCharCode(224|n>>12&15),e+=String.fromCharCode(128|n>>6&63),e+=String.fromCharCode(128|n>>0&63)):(e+=String.fromCharCode(192|n>>6&31),e+=String.fromCharCode(128|n>>0&63));return e}function drawQrcode(t){t=t||{},(t=extend(!0,{width:256,height:256,x:0,y:0,typeNumber:-1,correctLevel:QRErrorCorrectLevel.H,background:"#ffffff",foreground:"#000000",image:{imageResource:"",dx:0,dy:0,dWidth:100,dHeight:100}},t)).canvasId||t.ctx?function(){var e,r=new QRCode(t.typeNumber,t.correctLevel);r.addData(utf16to8(t.text)),r.make(),e=t.ctx?t.ctx:t._this?wx.createCanvasContext&&wx.createCanvasContext(t.canvasId,t._this):wx.createCanvasContext&&wx.createCanvasContext(t.canvasId);for(var o=t.width/r.getModuleCount(),n=t.height/r.getModuleCount(),i=0;i>>7-t%8&1)},put:function(t,e){for(var r=0;r>>e-r-1&1))},getLengthInBits:function(){return this.length},putBit:function(t){var e=Math.floor(this.length/8);this.buffer.length<=e&&this.buffer.push(0),t&&(this.buffer[e]|=128>>>this.length%8),this.length++}};export default drawQrcode;
\ No newline at end of file
diff --git a/config/api-config.js b/config/api-config.js
new file mode 100644
index 0000000000000000000000000000000000000000..497e5f7148bba82186b21ce7a384876c5f2cd0f3
--- /dev/null
+++ b/config/api-config.js
@@ -0,0 +1,88 @@
+// API配置文件
+// 如需自定义配置,请复制此文件为 api-config.local.js 并修改配置信息
+// 项目会优先使用 api-config.local.js,如不存在则使用此默认配置
+
+const ApiConfig = {
+ // 服务器配置
+ server: {
+ baseUrl: 'https://your-api-server.com', // 替换为您的API服务器地址
+ timeout: 30000, // 30秒超时
+ retryCount: 3, // 重试次数
+ },
+
+ // API路径配置
+ endpoints: {
+ // 微信登录接口
+ wxLogin: '/wx/login',
+ // 二维码/条形码生成接口
+ codeGeneration: '/api/code/gen',
+ },
+
+ // 历史记录配置
+ history: {
+ maxRecords: 20, // 历史记录最大条数
+ storageKeys: {
+ scanHistory: 'scanHistory', // 扫码历史
+ generateHistory: 'generateHistory', // 生成历史
+ barcodeHistory: 'barcodeHistory' // 条形码历史
+ },
+ // 记录类型
+ recordTypes: {
+ scan: 'scan', // 扫码记录
+ qrGenerate: 'qr', // 二维码生成记录
+ barcodeGenerate: 'barcode' // 条形码生成记录
+ }
+ },
+
+ // 频率控制配置
+ rateLimit: {
+ // 本地频率控制 (毫秒)
+ singleGenInterval: 2000, // 单个生成间隔 2秒
+ batchGenInterval: 10000, // 批量生成间隔 10秒
+ maxRequestsPerMinute: 20, // 每分钟最多请求数
+ maxBatchSize: 50, // 批量生成最大数量
+
+ // 频率控制存储key
+ storageKeys: {
+ lastSingleGen: 'last_single_gen_time',
+ lastBatchGen: 'last_batch_gen_time',
+ requestHistory: 'request_history_minute'
+ }
+ },
+
+ // 用户标识配置
+ userIdentity: {
+ storageKey: 'user_unique_id',
+ prefix: 'wx_user_'
+ },
+
+ // 登录配置
+ auth: {
+ corpCode: 'your_corp_code', // 替换为您的企业代码
+ storageKeys: {
+ // 以下键名需要根据您的服务器API规范进行调整
+ accessToken: 'access_token', // 访问令牌存储键
+ userCode: 'user_code', // 用户代码存储键
+ userInfo: 'user_info', // 用户信息存储键
+ isLoggedIn: 'is_logged_in' // 登录状态存储键
+ },
+ tokenExpiryTime: 30 * 60 * 1000, // 30分钟,毫秒
+ },
+
+ // 二维码/条形码类型配置
+ codeTypes: {
+ qrcode: {
+ standard: 'qr_standard',
+ micro: 'qr_micro'
+ },
+ barcode: {
+ 'Code 128': 'barcode_128',
+ 'Code 39': 'barcode_39',
+ 'EAN-13': 'barcode_ean13',
+ 'EAN-8': 'barcode_ean8',
+ 'ITF': 'barcode_itf'
+ }
+ }
+};
+
+module.exports = ApiConfig;
\ No newline at end of file
diff --git a/config/config-manager.js b/config/config-manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..92b11f01823efed7491a84282a29868446376935
--- /dev/null
+++ b/config/config-manager.js
@@ -0,0 +1,108 @@
+// 配置管理器
+// 自动处理配置文件的加载,支持开发和生产环境
+
+const ConfigManager = {
+
+ /**
+ * 获取API配置
+ * 优先级:api-config.local.js > api-config.js
+ */
+ getApiConfig() {
+ let config = null;
+ let configSource = '';
+
+ try {
+ // 首先尝试加载本地配置文件
+ config = require('./api-config.local.js');
+ configSource = 'api-config.local.js';
+ console.log('✅ 已加载本地配置 api-config.local.js');
+ } catch (error) {
+ try {
+ // 如果本地配置不存在,加载默认配置
+ config = require('./api-config.js');
+ configSource = 'api-config.js';
+ console.log('📋 已加载默认配置 api-config.js');
+
+ // 在开发工具中显示提示
+ if (wx.getSystemInfoSync().platform === 'devtools') {
+ this.showConfigTip();
+ }
+ } catch (defaultError) {
+ console.error('❌ 配置文件加载失败:', defaultError);
+ throw new Error('无法加载配置文件,请检查 config 目录');
+ }
+ }
+
+ // 验证必要的配置项
+ this.validateConfig(config, configSource);
+
+ return config;
+ },
+
+ /**
+ * 验证配置文件的必要字段
+ */
+ validateConfig(config, configSource = '') {
+ const requiredFields = [
+ 'server.baseUrl',
+ 'auth.corpCode',
+ 'auth.storageKeys.accessToken'
+ ];
+
+ for (const field of requiredFields) {
+ const value = this.getNestedValue(config, field);
+ if (!value || value.includes('your-') || value.includes('example')) {
+ if (configSource === 'api-config.js') {
+ console.warn(`⚠️ 配置项 ${field} 是示例值,建议创建 api-config.local.js 文件并配置真实参数`);
+ } else {
+ console.warn(`⚠️ 配置项 ${field} 似乎还是示例值,请检查配置`);
+ }
+ }
+ }
+ },
+
+ /**
+ * 获取嵌套对象的值
+ */
+ getNestedValue(obj, path) {
+ return path.split('.').reduce((current, key) => current?.[key], obj);
+ },
+
+ /**
+ * 显示配置提示
+ */
+ showConfigTip() {
+ // 延迟显示,避免影响启动
+ setTimeout(() => {
+ wx.showModal({
+ title: '配置提示',
+ content: '当前使用默认配置文件。\n\n如需自定义配置,请复制 api-config.js 为 api-config.local.js 并修改您的配置参数。',
+ showCancel: false,
+ confirmText: '我知道了'
+ });
+ }, 2000);
+ },
+
+ /**
+ * 检查是否为开发环境
+ */
+ isDevelopment() {
+ return wx.getSystemInfoSync().platform === 'devtools';
+ },
+
+ /**
+ * 获取环境特定的配置
+ */
+ getEnvironmentConfig() {
+ const baseConfig = this.getApiConfig();
+
+ if (this.isDevelopment()) {
+ // 开发环境的特殊处理
+ console.log('🔧 当前为开发环境');
+ }
+
+ return baseConfig;
+ }
+};
+
+module.exports = ConfigManager;
\ No newline at end of file
diff --git a/images/README.md b/images/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b3488d21346689f1a0ce72e09c0a55063615fd29
--- /dev/null
+++ b/images/README.md
@@ -0,0 +1,31 @@
+# 图标资源说明
+
+## 所需图标列表
+
+### 主要功能图标
+- `setting.png` - 设置图标 (36rpx × 36rpx)
+- `scan.png` - 扫码图标 (48rpx × 48rpx)
+- `qr-code.png` - 二维码图标 (48rpx × 48rpx)
+- `barcode.png` - 条形码图标 (48rpx × 48rpx)
+
+### 箭头图标
+- `arrow-down.png` - 下拉箭头 (24rpx × 24rpx)
+- `arrow-up.png` - 上拉箭头 (24rpx × 24rpx)
+
+### 广告相关
+- `ad-close.png` - 广告关闭按钮 (32rpx × 32rpx)
+
+### 历史记录图标
+- `scan-record.png` - 扫码记录图标 (48rpx × 48rpx)
+
+### 分享相关
+- `share-cover.jpg` - 分享封面图 (520px × 416px)
+
+## 图标设计要求
+- 采用线性图标风格
+- 主色调:#4A90E2
+- 背景透明PNG格式
+- 支持高清屏幕显示
+
+## 临时占位
+在开发阶段,可以使用微信小程序默认图标或者在线图标库的图标进行占位。
\ No newline at end of file
diff --git a/pages/custom-scan/custom-scan.js b/pages/custom-scan/custom-scan.js
new file mode 100644
index 0000000000000000000000000000000000000000..ed41199364b024eac54410f22b6b9c95f9a920e8
--- /dev/null
+++ b/pages/custom-scan/custom-scan.js
@@ -0,0 +1,562 @@
+// custom-scan.js - iOS风格版本
+Page({
+ data: {
+ showCamera: false,
+ devicePosition: 'back',
+ flash: 'off',
+ useCustomScan: true,
+ loading: false,
+ loadingText: '处理中...',
+ showResult: false,
+ scanResult: '',
+ // 按钮按下状态
+ testPressed: false,
+ customSwitchPressed: false,
+ wechatSwitchPressed: false,
+ takePhotoPressed: false,
+ flashPressed: false,
+ switchCameraPressed: false,
+ albumPressed: false,
+ backPressed: false,
+ closePressed: false,
+ copyPressed: false,
+ handlePressed: false
+ },
+
+ onLoad: function (options) {
+ console.log('扫码页面加载');
+
+ // 设置页面标题和样式
+ wx.setNavigationBarTitle({
+ title: '扫一扫'
+ });
+ wx.setNavigationBarColor({
+ frontColor: '#ffffff',
+ backgroundColor: '#000000'
+ });
+
+ // 延迟一点启动摄像头,确保页面完全加载
+ setTimeout(() => {
+ this.initCamera();
+ }, 500);
+ },
+
+ onShow: function () {
+ console.log('扫码页面显示 - 当前摄像头状态:', this.data.showCamera);
+
+ // 重置页面状态
+ this.setData({
+ loading: false,
+ showResult: false,
+ useCustomScan: true
+ });
+
+ // 当页面重新显示时,检查摄像头状态
+ // 如果摄像头没有显示,重新初始化
+ if (!this.data.showCamera) {
+ console.log('摄像头未显示,重新初始化');
+ setTimeout(() => {
+ this.initCamera();
+ }, 300);
+ } else {
+ console.log('摄像头已显示,状态正常');
+ }
+ },
+
+ onHide: function () {
+ console.log('扫码页面隐藏');
+
+ // 隐藏加载状态和结果弹窗
+ this.setData({
+ loading: false,
+ showResult: false
+ });
+ },
+
+ onUnload: function () {
+ console.log('扫码页面卸载');
+ },
+
+ // 测试按钮 - 验证点击功能(虽然界面隐藏但功能保留)
+ testButton: function () {
+ console.log('✅ 按钮功能正常 - 摄像头状态:', this.data.showCamera);
+
+ // iOS风格提示
+ wx.showToast({
+ title: '功能测试正常',
+ icon: 'success',
+ duration: 1500
+ });
+ },
+
+ // 初始化摄像头
+ initCamera: function () {
+ console.log('初始化摄像头');
+
+ // 检查权限
+ wx.getSetting({
+ success: (res) => {
+ if (res.authSetting['scope.camera'] === false) {
+ // 用户拒绝过摄像头权限
+ wx.showModal({
+ title: '需要摄像头权限',
+ content: '请在设置中开启摄像头权限来使用扫码功能',
+ confirmText: '去设置',
+ cancelText: '返回',
+ success: (modalRes) => {
+ if (modalRes.confirm) {
+ wx.openSetting();
+ } else {
+ this.goBack();
+ }
+ }
+ });
+ } else if (res.authSetting['scope.camera'] === undefined) {
+ // 还没有请求过权限
+ wx.authorize({
+ scope: 'scope.camera',
+ success: () => {
+ this.setData({
+ showCamera: true
+ });
+ console.log('摄像头启动成功');
+ },
+ fail: () => {
+ wx.showToast({
+ title: '需要摄像头权限才能扫码',
+ icon: 'none',
+ duration: 2000
+ });
+ this.goBack();
+ }
+ });
+ } else {
+ // 启动摄像头
+ this.setData({
+ showCamera: true
+ });
+ console.log('摄像头启动成功');
+ }
+ },
+ fail: (err) => {
+ console.error('获取设置失败:', err);
+ wx.showToast({
+ title: '权限检查失败',
+ icon: 'none'
+ });
+ }
+ });
+ },
+
+ // 切换到自定义扫码
+ switchToCustom: function () {
+ console.log('切换到自定义扫码');
+ if (!this.data.useCustomScan) {
+ this.setData({
+ useCustomScan: true
+ });
+ wx.showToast({
+ title: '已切换到自定义扫码',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 切换到微信API扫码
+ switchToWechat: function () {
+ console.log('切换到微信API扫码');
+ if (this.data.useCustomScan) {
+ this.setData({
+ useCustomScan: false
+ });
+ // 直接调用微信API
+ this.callWechatScanAPI();
+ }
+ },
+
+ // 调用微信扫码API
+ callWechatScanAPI: function () {
+ console.log('调用微信扫码API');
+ wx.scanCode({
+ onlyFromCamera: false, // 支持摄像头拍照和从相册选择
+ scanType: ['qrCode', 'barCode', 'datamatrix', 'pdf417'],
+ success: (res) => {
+ console.log('微信API扫码成功:', res);
+ this.handleScanSuccess(res.result);
+ },
+ fail: (err) => {
+ console.error('微信API扫码失败:', err);
+
+ // 无论什么原因失败,都要恢复到自定义扫码模式
+ this.setData({
+ useCustomScan: true
+ });
+
+ if (err.errMsg && err.errMsg.includes('cancel')) {
+ console.log('用户取消微信扫码,返回自定义模式');
+ } else {
+ wx.showModal({
+ title: '扫码失败',
+ content: '识别失败,请确保图片清晰且包含可识别的二维码或条形码。\n\n提示:可以选择拍照或从相册选择图片进行识别。',
+ confirmText: '知道了',
+ showCancel: false
+ });
+ }
+
+ // 确保摄像头状态正确
+ if (!this.data.showCamera) {
+ setTimeout(() => {
+ this.initCamera();
+ }, 300);
+ }
+ }
+ });
+ },
+
+ // 拍照扫码
+ takePhotoToScan: function () {
+ console.log('开始拍照识别');
+
+ if (!this.data.showCamera) {
+ wx.showToast({
+ title: '摄像头未就绪,请稍候',
+ icon: 'none'
+ });
+ return;
+ }
+
+ if (!this.data.useCustomScan) {
+ // 如果不是自定义模式,调用微信API
+ this.callWechatScanAPI();
+ return;
+ }
+
+ // 显示加载状态
+ this.setData({
+ loading: true,
+ loadingText: '正在识别...'
+ });
+
+ // 创建摄像头上下文
+ const ctx = wx.createCameraContext();
+
+ ctx.takePhoto({
+ quality: 'high',
+ success: (res) => {
+ console.log('拍照成功,开始识别');
+ this.recognizeQRCode(res.tempImagePath);
+ },
+ fail: (err) => {
+ console.error('拍照失败:', err);
+ this.setData({
+ loading: false
+ });
+
+ wx.showToast({
+ title: '拍照失败,请重试',
+ icon: 'none'
+ });
+ }
+ });
+ },
+
+ // 识别二维码
+ recognizeQRCode: function (imagePath) {
+ // 这里使用模拟识别,实际项目中需要接入真实的识别服务
+
+ this.setData({
+ loadingText: '识别中...'
+ });
+
+ // 模拟识别过程
+ setTimeout(() => {
+ // 模拟识别结果
+ const mockResults = [
+ 'https://www.apple.com',
+ 'https://developer.apple.com',
+ '这是一个测试二维码内容 - iOS风格界面',
+ 'WIFI:T:WPA;S:iPhone的WiFi;P:password123;H:;',
+ 'tel:10086',
+ 'mailto:example@icloud.com',
+ null // 表示未识别到
+ ];
+
+ const randomResult = mockResults[Math.floor(Math.random() * mockResults.length)];
+
+ if (randomResult) {
+ // 识别成功
+ console.log('识别成功:', randomResult);
+ this.handleScanSuccess(randomResult);
+ } else {
+ // 未识别到内容
+ this.setData({
+ loading: false
+ });
+ wx.showToast({
+ title: '未识别到二维码,请重试',
+ icon: 'none'
+ });
+ }
+
+ }, 1200); // 稍微缩短识别时间
+ },
+
+ // 处理扫码成功
+ handleScanSuccess: function (result) {
+ console.log('扫码成功:', result);
+
+ // 保存扫码记录
+ this.saveScanRecord(result);
+
+ this.setData({
+ loading: false,
+ showResult: true,
+ scanResult: result
+ });
+ },
+
+ // 保存扫码记录
+ saveScanRecord: function(content) {
+ const historyManager = require('../../utils/history-manager.js');
+ const success = historyManager.saveScanRecord(content);
+
+ if (success) {
+ console.log('扫码记录已保存');
+ }
+ },
+
+ // 切换闪光灯
+ toggleFlash: function () {
+ console.log('切换闪光灯');
+
+ const newFlash = this.data.flash === 'torch' ? 'off' : 'torch';
+ this.setData({
+ flash: newFlash
+ });
+
+ wx.showToast({
+ title: newFlash === 'torch' ? '闪光灯已开启' : '闪光灯已关闭',
+ icon: 'none',
+ duration: 1500
+ });
+ },
+
+ // 切换摄像头
+ switchCamera: function () {
+ console.log('切换摄像头');
+ const newPosition = this.data.devicePosition === 'back' ? 'front' : 'back';
+ this.setData({
+ devicePosition: newPosition
+ });
+
+ wx.showToast({
+ title: `已切换到${newPosition === 'back' ? '后置' : '前置'}摄像头`,
+ icon: 'none',
+ duration: 1500
+ });
+ },
+
+ // 从相册选择图片识别
+ chooseFromAlbum: function () {
+ console.log('从相册选择图片');
+ wx.chooseImage({
+ count: 1,
+ sizeType: ['original'],
+ sourceType: ['album'],
+ success: (res) => {
+ const imagePath = res.tempFilePaths[0];
+ this.setData({
+ loading: true,
+ loadingText: '正在识别相册图片...'
+ });
+ this.recognizeQRCode(imagePath);
+ },
+ fail: (err) => {
+ console.error('选择图片失败:', err);
+ }
+ });
+ },
+
+ // 返回按钮
+ goBack: function () {
+ console.log('返回上级页面');
+ wx.navigateBack();
+ },
+
+ // 隐藏结果弹窗
+ hideResult: function () {
+ console.log('关闭结果弹窗');
+ this.setData({
+ showResult: false,
+ scanResult: ''
+ });
+ },
+
+ // 复制结果
+ copyResult: function () {
+ wx.setClipboardData({
+ data: this.data.scanResult,
+ success: () => {
+ wx.showToast({
+ title: '已复制到剪贴板',
+ icon: 'success'
+ });
+ }
+ });
+ },
+
+ // 处理结果(返回首页处理)
+ handleResult: function () {
+ const result = this.data.scanResult;
+
+ // 隐藏结果弹窗
+ this.hideResult();
+
+ // 返回到首页并处理结果
+ wx.navigateBack({
+ success: () => {
+ // 获取首页实例并调用处理方法
+ const pages = getCurrentPages();
+ if (pages.length > 1) {
+ const prevPage = pages[pages.length - 2];
+ if (prevPage.handleScanResult) {
+ // 直接传递字符串,与首页的handleScanResult方法兼容
+ prevPage.handleScanResult(result);
+ }
+ }
+ }
+ });
+ },
+
+ // 停止事件冒泡
+ stopPropagation: function () {
+ // 阻止事件冒泡
+ },
+
+ // 摄像头准备就绪
+ onCameraReady: function (e) {
+ console.log('摄像头已准备就绪');
+ },
+
+ // 摄像头错误
+ onCameraError: function (e) {
+ console.error('摄像头错误:', e.detail);
+
+ let errorMsg = '摄像头启动失败';
+ if (e.detail.errMsg) {
+ if (e.detail.errMsg.includes('user deny')) {
+ errorMsg = '用户拒绝了摄像头权限';
+ } else if (e.detail.errMsg.includes('not available')) {
+ errorMsg = '摄像头设备不可用';
+ } else {
+ errorMsg = '摄像头出现错误: ' + e.detail.errMsg;
+ }
+ }
+
+ wx.showModal({
+ title: '摄像头错误',
+ content: errorMsg,
+ showCancel: false,
+ confirmText: '返回',
+ success: () => {
+ this.goBack();
+ }
+ });
+ },
+
+ // 摄像头停止
+ onCameraStop: function (e) {
+ console.log('摄像头已停止');
+ this.setData({
+ showCamera: false
+ });
+ },
+
+ // 测试按钮触摸事件
+ onTestTouchStart: function() {
+ this.setData({ testPressed: true });
+ },
+ onTestTouchEnd: function() {
+ this.setData({ testPressed: false });
+ },
+
+ // 自定义切换按钮触摸事件
+ onCustomSwitchTouchStart: function() {
+ this.setData({ customSwitchPressed: true });
+ },
+ onCustomSwitchTouchEnd: function() {
+ this.setData({ customSwitchPressed: false });
+ },
+
+ // 微信切换按钮触摸事件
+ onWechatSwitchTouchStart: function() {
+ this.setData({ wechatSwitchPressed: true });
+ },
+ onWechatSwitchTouchEnd: function() {
+ this.setData({ wechatSwitchPressed: false });
+ },
+
+ // 拍照按钮触摸事件
+ onTakePhotoTouchStart: function() {
+ this.setData({ takePhotoPressed: true });
+ },
+ onTakePhotoTouchEnd: function() {
+ this.setData({ takePhotoPressed: false });
+ },
+
+ // 闪光灯按钮触摸事件
+ onFlashTouchStart: function() {
+ this.setData({ flashPressed: true });
+ },
+ onFlashTouchEnd: function() {
+ this.setData({ flashPressed: false });
+ },
+
+ // 切换摄像头按钮触摸事件
+ onSwitchCameraTouchStart: function() {
+ this.setData({ switchCameraPressed: true });
+ },
+ onSwitchCameraTouchEnd: function() {
+ this.setData({ switchCameraPressed: false });
+ },
+
+ // 相册按钮触摸事件
+ onAlbumTouchStart: function() {
+ this.setData({ albumPressed: true });
+ },
+ onAlbumTouchEnd: function() {
+ this.setData({ albumPressed: false });
+ },
+
+ // 返回按钮触摸事件
+ onBackTouchStart: function() {
+ this.setData({ backPressed: true });
+ },
+ onBackTouchEnd: function() {
+ this.setData({ backPressed: false });
+ },
+
+ // 关闭按钮触摸事件
+ onCloseTouchStart: function() {
+ this.setData({ closePressed: true });
+ },
+ onCloseTouchEnd: function() {
+ this.setData({ closePressed: false });
+ },
+
+ // 复制按钮触摸事件
+ onCopyTouchStart: function() {
+ this.setData({ copyPressed: true });
+ },
+ onCopyTouchEnd: function() {
+ this.setData({ copyPressed: false });
+ },
+
+ // 处理按钮触摸事件
+ onHandleTouchStart: function() {
+ this.setData({ handlePressed: true });
+ },
+ onHandleTouchEnd: function() {
+ this.setData({ handlePressed: false });
+ },
+});
\ No newline at end of file
diff --git a/pages/custom-scan/custom-scan.json b/pages/custom-scan/custom-scan.json
new file mode 100644
index 0000000000000000000000000000000000000000..469bbe1d95698b8fa667dc597b93d8c4d5554c01
--- /dev/null
+++ b/pages/custom-scan/custom-scan.json
@@ -0,0 +1,6 @@
+{
+ "navigationStyle": "custom",
+ "backgroundColor": "#000000",
+ "backgroundTextStyle": "light",
+ "enablePullDownRefresh": false
+}
\ No newline at end of file
diff --git a/pages/custom-scan/custom-scan.wxml b/pages/custom-scan/custom-scan.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..75275b5baf18a6f036e34bcc1b5436d88d97ee5e
--- /dev/null
+++ b/pages/custom-scan/custom-scan.wxml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 测试按钮 (验证点击)
+
+
+
+
+
+
+ 自定义
+
+
+ 微信摄像头
+
+
+
+
+
+ 自定义兼容强,微信性能高,标准码建议使用微信摄像头
+
+
+
+
+
+
+
+ 拍照
+
+
+
+
+
+ {{flash === 'torch' ? '关闭' : '闪光'}}
+
+
+
+
+
+ 翻转
+
+
+
+
+
+ 相册
+
+
+
+
+
+ ←
+ 返回
+
+
+
+
+ 摄像头状态: {{showCamera ? '已启动' : '未启动'}}
+ 当前模式: {{useCustomScan ? '自定义' : '微信API'}}
+
+
+
+
+
+
+
+ 正在启动摄像头...
+
+
+
+
+
+
+
+
+
+
+
+
+ {{loadingText}}
+
+
+
+
+
+
+
+
+
+
+ 扫码方式:
+ {{useCustomScan ? '自定义摄像头' : '微信摄像头'}}
+
+
+ 识别内容:
+ {{scanResult}}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/custom-scan/custom-scan.wxss b/pages/custom-scan/custom-scan.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..cd33f6af8eb1d08be35907fa1d492431b11c9ebd
--- /dev/null
+++ b/pages/custom-scan/custom-scan.wxss
@@ -0,0 +1,580 @@
+/* custom-scan.wxss - iOS风格设计 */
+
+.container {
+ width: 100vw;
+ height: 100vh;
+ background: #000;
+ position: relative;
+ overflow: hidden;
+}
+
+
+
+/* 摄像头区域 - 全屏占满,避开状态栏 */
+.camera-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: #000;
+ padding-top: env(safe-area-inset-top);
+}
+
+.camera-preview {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.camera-placeholder {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: #000;
+ color: #ffffff;
+}
+
+.placeholder-icon {
+ width: 80rpx;
+ height: 80rpx;
+ margin-bottom: 20rpx;
+ opacity: 0.6;
+}
+
+.placeholder-text {
+ font-size: 32rpx;
+ color: #ffffff;
+ opacity: 0.8;
+ font-weight: 500;
+}
+
+
+
+/* 扫描线容器 - 全屏范围,与摄像头平级 */
+.scan-line-container {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 800; /* 高于摄像头但低于控制栏 */
+ overflow: hidden;
+ pointer-events: none; /* 确保不会拦截点击事件 */
+}
+
+/* 扫描线 - 带拖尾效果 */
+.scan-line {
+ position: absolute;
+ left: 5%;
+ right: 5%;
+ height: 8rpx;
+ background: linear-gradient(90deg,
+ transparent 0%,
+ rgba(0, 255, 136, 0.4) 5%,
+ rgba(0, 255, 136, 0.8) 20%,
+ #00ff88 40%,
+ #ffffff 50%,
+ #00ff88 60%,
+ rgba(0, 255, 136, 0.8) 80%,
+ rgba(0, 255, 136, 0.4) 95%,
+ transparent 100%
+ );
+ border-radius: 4rpx;
+ box-shadow:
+ 0 0 40rpx #00ff88,
+ 0 0 80rpx rgba(0, 255, 136, 0.7),
+ 0 0 120rpx rgba(0, 255, 136, 0.4);
+ animation: scanLineMove 3s ease-in-out infinite;
+}
+
+/* 扫描线移动动画 - 从15%位置开始到控制栏顶部 */
+@keyframes scanLineMove {
+ 0% {
+ top: 15%; /* 从上面15%位置开始 */
+ opacity: 0;
+ transform: scaleY(0.5);
+ }
+ 5% {
+ opacity: 1;
+ transform: scaleY(1);
+ }
+ 85% {
+ opacity: 1;
+ transform: scaleY(1);
+ }
+ 95% {
+ opacity: 0.3;
+ transform: scaleY(0.8);
+ }
+ 100% {
+ top: 85%; /* 到控制栏顶部附近消失 */
+ opacity: 0;
+ transform: scaleY(0.5);
+ }
+}
+
+/* 底部控制栏 - 全屏覆盖层 */
+.control-bar {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1000;
+ padding: 20rpx 30rpx;
+ padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(20rpx);
+}
+
+/* 隐藏测试区域 - 保持功能但不显示 */
+.test-section {
+ display: none; /* 隐藏测试按钮,保持iOS简洁 */
+}
+
+.test-btn {
+ width: 100%;
+ font-size: 24rpx;
+ background: linear-gradient(135deg, #ff4444, #cc0000);
+ color: #ffffff;
+ border-radius: 25rpx;
+ padding: 15rpx;
+ text-align: center;
+ border: 2rpx solid rgba(255, 255, 255, 0.2);
+}
+
+.test-btn.pressed {
+ background: linear-gradient(135deg, #cc0000, #990000);
+ border-color: rgba(255, 255, 255, 0.4);
+}
+
+/* iOS风格模式切换 */
+.mode-switch {
+ display: flex;
+ background: rgba(118, 118, 128, 0.12);
+ border-radius: 20rpx;
+ padding: 4rpx;
+ margin-bottom: 24rpx;
+ backdrop-filter: blur(20rpx);
+}
+
+.switch-item {
+ flex: 1;
+ text-align: center;
+ padding: 16rpx;
+ border-radius: 16rpx;
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ cursor: pointer;
+}
+
+.switch-item.active {
+ background: rgba(255, 255, 255, 0.9);
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
+}
+
+.switch-item.active .switch-text {
+ color: #000000;
+}
+
+.switch-item.pressed {
+ background: rgba(118, 118, 128, 0.4);
+}
+
+.switch-item.active.pressed {
+ background: rgba(255, 255, 255, 0.7);
+}
+
+.switch-text {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #ffffff;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+/* 模式说明文字 */
+.mode-description {
+ margin-bottom: 30rpx;
+ text-align: center;
+}
+
+.desc-text {
+ font-size: 22rpx;
+ color: rgba(255, 255, 255, 0.7);
+ background: rgba(0, 0, 0, 0.3);
+ padding: 8rpx 16rpx;
+ border-radius: 15rpx;
+ backdrop-filter: blur(10rpx);
+ border: 0.5rpx solid rgba(255, 255, 255, 0.1);
+ line-height: 1.4;
+}
+
+/* iOS风格功能按钮组 */
+.action-buttons {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ margin-bottom: 24rpx;
+ gap: 16rpx;
+}
+
+.action-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 120rpx;
+ height: 120rpx;
+ background: rgba(118, 118, 128, 0.12);
+ border-radius: 30rpx;
+ backdrop-filter: blur(20rpx);
+ border: 0.5rpx solid rgba(255, 255, 255, 0.1);
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ cursor: pointer;
+}
+
+.action-btn:active {
+ transform: scale(0.9);
+ background: rgba(118, 118, 128, 0.3);
+ opacity: 0.8;
+}
+
+.action-btn.pressed {
+ background: rgba(118, 118, 128, 0.4);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.action-btn.primary {
+ width: 120rpx; /* 统一大小 */
+ height: 120rpx;
+ background: #00C853;
+ border: none;
+ box-shadow: 0 4rpx 20rpx rgba(0, 200, 83, 0.3);
+}
+
+.action-btn.primary:active {
+ background: #00A043;
+ transform: scale(0.9);
+ opacity: 0.8;
+}
+
+.action-btn.primary.pressed {
+ background: #00A043;
+ box-shadow: 0 2rpx 10rpx rgba(0, 200, 83, 0.4);
+}
+
+.btn-icon {
+ font-size: 32rpx;
+ margin-bottom: 8rpx;
+ color: #ffffff;
+ opacity: 0.9;
+}
+
+/* 按钮图标图片 */
+.btn-icon-img {
+ width: 32rpx;
+ height: 32rpx;
+ margin-bottom: 8rpx;
+ opacity: 0.9;
+}
+
+.btn-text {
+ font-size: 20rpx;
+ color: #ffffff;
+ font-weight: 500;
+ opacity: 0.8;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+/* iOS风格返回按钮 */
+.back-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(118, 118, 128, 0.12);
+ border-radius: 24rpx;
+ padding: 16rpx 24rpx;
+ backdrop-filter: blur(20rpx);
+ cursor: pointer;
+ margin-bottom: 16rpx;
+ border: 0.5rpx solid rgba(255, 255, 255, 0.1);
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+}
+
+.back-btn:active {
+ background: rgba(118, 118, 128, 0.2);
+ transform: scale(0.98);
+}
+
+.back-btn.pressed {
+ background: rgba(118, 118, 128, 0.4);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.back-icon {
+ font-size: 28rpx;
+ color: #ffffff;
+ margin-right: 8rpx;
+ opacity: 0.9;
+}
+
+.back-text {
+ font-size: 28rpx;
+ color: #ffffff;
+ font-weight: 500;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+/* 隐藏调试信息 - 保持功能但不显示 */
+.debug-info {
+ display: none; /* 隐藏调试信息,保持iOS简洁 */
+}
+
+.debug-text {
+ display: block;
+ font-size: 20rpx;
+ color: #ffff00;
+ margin-bottom: 6rpx;
+ line-height: 1.3;
+}
+
+.debug-text:last-child {
+ margin-bottom: 0;
+}
+
+/* iOS风格加载提示 - 解决遮挡问题 */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 8000; /* 降低层级,避免遮挡关闭按钮 */
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(20rpx);
+}
+
+.loading-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: rgba(58, 58, 60, 0.95);
+ padding: 40rpx;
+ border-radius: 24rpx;
+ backdrop-filter: blur(40rpx);
+ border: 0.5rpx solid rgba(255, 255, 255, 0.1);
+ min-width: 200rpx;
+}
+
+.loading-spinner {
+ width: 40rpx;
+ height: 40rpx;
+ border: 3rpx solid rgba(255, 255, 255, 0.2);
+ border-top: 3rpx solid #ffffff;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 20rpx;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: #ffffff;
+ text-align: center;
+ font-weight: 500;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+/* iOS风格结果弹窗 - 浅色主题 */
+.result-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 9000; /* 高于加载层级,确保可以关闭 */
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 40rpx;
+ backdrop-filter: blur(20rpx);
+}
+
+.result-content {
+ width: 100%;
+ max-width: 640rpx;
+ background: rgba(255, 255, 255, 0.98);
+ border-radius: 28rpx;
+ backdrop-filter: blur(40rpx);
+ overflow: hidden;
+ border: 0.5rpx solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32rpx;
+ background: rgba(245, 245, 247, 0.8);
+ border-bottom: 0.5rpx solid rgba(0, 0, 0, 0.1);
+}
+
+.result-title {
+ font-size: 34rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+.result-close {
+ font-size: 32rpx;
+ color: #666666;
+ width: 56rpx;
+ height: 56rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 28rpx;
+ background: rgba(0, 0, 0, 0.05);
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ cursor: pointer;
+}
+
+.result-close:active {
+ background: rgba(0, 0, 0, 0.1);
+ transform: scale(0.95);
+}
+
+.result-close.pressed {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.result-body {
+ padding: 32rpx;
+ background: rgba(255, 255, 255, 0.95);
+}
+
+.result-item {
+ margin-bottom: 24rpx;
+}
+
+.result-item:last-child {
+ margin-bottom: 0;
+}
+
+.result-label {
+ display: block;
+ font-size: 24rpx;
+ color: #666666;
+ margin-bottom: 8rpx;
+ font-weight: 500;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+.result-value {
+ display: block;
+ font-size: 32rpx;
+ color: #1d1d1f;
+ background: rgba(245, 245, 247, 0.8);
+ padding: 16rpx;
+ border-radius: 16rpx;
+ word-break: break-all;
+ line-height: 1.4;
+ font-family: 'SF Mono', Monaco, monospace;
+ border: 1rpx solid rgba(0, 0, 0, 0.05);
+}
+
+.result-actions {
+ display: flex;
+ gap: 16rpx;
+ padding: 0 32rpx 32rpx;
+ background: rgba(255, 255, 255, 0.95);
+}
+
+.result-btn {
+ flex: 1;
+ height: 88rpx;
+ border-radius: 22rpx;
+ font-size: 32rpx;
+ font-weight: 600;
+ border: none;
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display';
+}
+
+.result-btn:active {
+ transform: scale(0.98);
+}
+
+.result-btn.pressed {
+ /* 移除transform动画 */
+}
+
+.result-btn.secondary {
+ background: rgba(0, 0, 0, 0.05);
+ color: #1d1d1f;
+ border: 1rpx solid rgba(0, 0, 0, 0.1);
+}
+
+.result-btn.secondary:active {
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.result-btn.secondary.pressed {
+ background: rgba(0, 0, 0, 0.15);
+ border-color: rgba(0, 0, 0, 0.2);
+}
+
+.result-btn.primary {
+ background: #00C853;
+ color: #ffffff;
+}
+
+.result-btn.primary:active {
+ background: #00A043;
+}
+
+.result-btn.primary.pressed {
+ background: #00A043;
+}
+
+/* 响应式适配 */
+@media (max-width: 750rpx) {
+ .scan-box {
+ width: 400rpx;
+ height: 400rpx;
+ }
+
+ .action-buttons {
+ gap: 12rpx;
+ }
+
+ .action-btn {
+ width: 100rpx;
+ height: 100rpx;
+ }
+
+ .action-btn.primary {
+ width: 100rpx; /* 响应式模式下也统一大小 */
+ height: 100rpx;
+ }
+}
+
+/* 深色模式优化 */
+@media (prefers-color-scheme: dark) {
+ .container {
+ background: #000000;
+ }
+}
\ No newline at end of file
diff --git a/pages/generate-barcode/generate-barcode.js b/pages/generate-barcode/generate-barcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..3db9153fc02ee66d2dd74b47ecfce834b8b45cd7
--- /dev/null
+++ b/pages/generate-barcode/generate-barcode.js
@@ -0,0 +1,373 @@
+// 生成条形码页面 - iOS风格 - 混合生成版本(服务器端+本地生成)
+const apiService = require('../../utils/api-service.js');
+const MessageUtil = require('../../utils/message-util.js');
+
+Page({
+ data: {
+ // 基础数据
+ inputContent: '',
+ batchContent: '',
+ batchMode: false,
+ batchLines: 0,
+
+ // 条形码类型
+ barcodeType: 'Code 128',
+
+ // 限制设置
+ maxLength: 100,
+ maxBatchLength: 2000,
+
+ // 美化选项
+ foregroundColor: '#000000',
+ backgroundColor: '#ffffff',
+ barcodeWidth: 300,
+
+ // 状态控制
+ generating: false,
+
+ // 离线模式状态
+ isOfflineMode: false
+ },
+
+ onLoad(options) {
+ // 从参数中获取类型信息
+ if (options.type) {
+ const type = decodeURIComponent(options.type);
+ this.setData({ barcodeType: type });
+ this.adjustLimits();
+ }
+
+ // 检查离线模式
+ this.checkOfflineMode();
+ },
+
+ onShow() {
+ // 页面显示时重新检查离线状态
+ this.checkOfflineMode();
+ },
+
+ // 检查离线模式状态
+ checkOfflineMode() {
+ try {
+ const app = getApp();
+ const isOffline = app.isOfflineMode();
+
+ this.setData({
+ isOfflineMode: isOffline
+ });
+
+ console.log('条形码生成页面 - 离线模式状态:', isOffline);
+ } catch (error) {
+ console.error('检查离线模式失败:', error);
+ this.setData({
+ isOfflineMode: false
+ });
+ }
+ },
+
+ // 切换批量模式
+ toggleBatchMode() {
+ const newBatchMode = !this.data.batchMode;
+ this.setData({
+ batchMode: newBatchMode,
+ inputContent: '',
+ batchContent: '',
+ batchLines: 0
+ });
+ },
+
+ // 单个输入变化
+ onInputChange(e) {
+ const value = e.detail.value;
+ this.setData({ inputContent: value });
+ this.validateInput(value);
+ },
+
+ // 批量输入变化
+ onBatchInputChange(e) {
+ const value = e.detail.value;
+ const lines = value.split('\n').filter(line => line.trim()).length;
+
+ // 限制行数
+ if (lines > 50) {
+ const limitedLines = value.split('\n').slice(0, 50).join('\n');
+ this.setData({
+ batchContent: limitedLines,
+ batchLines: 50
+ });
+ wx.showToast({
+ title: '最多支持50行',
+ icon: 'none'
+ });
+ return;
+ }
+
+ this.setData({
+ batchContent: value,
+ batchLines: lines
+ });
+ },
+
+ // 切换条形码类型
+ switchType(e) {
+ const type = e.currentTarget.dataset.type;
+ this.setData({ barcodeType: type });
+ this.adjustLimits();
+
+ // 重新验证当前输入
+ if (!this.data.batchMode && this.data.inputContent) {
+ this.validateInput(this.data.inputContent);
+ }
+ },
+
+ // 根据类型调整输入限制
+ adjustLimits() {
+ let maxLength = 100;
+
+ switch (this.data.barcodeType) {
+ case 'EAN-13':
+ maxLength = 13;
+ break;
+ case 'EAN-8':
+ maxLength = 8;
+ break;
+ case 'Code 39':
+ maxLength = 80;
+ break;
+ case 'ITF':
+ maxLength = 50;
+ break;
+ case 'Code 128':
+ default:
+ maxLength = 100;
+ break;
+ }
+
+ this.setData({ maxLength });
+ },
+
+ // 输入验证
+ validateInput(value) {
+ const type = this.data.barcodeType;
+
+ // 数字格式验证
+ if (type === 'EAN-13' || type === 'EAN-8' || type === 'ITF') {
+ const numericOnly = value.replace(/[^0-9]/g, '');
+ if (numericOnly !== value) {
+ this.setData({ inputContent: numericOnly });
+ }
+ }
+
+ // Code 39 字符限制
+ if (type === 'Code 39') {
+ const validChars = value.replace(/[^A-Z0-9\-. $\/+%]/g, '');
+ if (validChars !== value) {
+ this.setData({ inputContent: validChars.toUpperCase() });
+ }
+ }
+ },
+
+ // 选择颜色
+ selectColor(e) {
+ const { type, color } = e.currentTarget.dataset;
+ if (type === 'foreground') {
+ this.setData({ foregroundColor: color });
+ } else if (type === 'background') {
+ this.setData({ backgroundColor: color });
+ }
+ },
+
+ // 选择尺寸
+ selectSize(e) {
+ const size = parseInt(e.currentTarget.dataset.size);
+ this.setData({ barcodeWidth: size });
+ },
+
+ // 生成条形码
+ async generateBarcode() {
+ // 防止重复点击
+ if (this.data.generating) return;
+
+ // 验证输入
+ if (!this.validateBeforeGenerate()) return;
+
+ // 开始生成动画
+ this.setData({ generating: true });
+
+ try {
+ await this.performGeneration();
+
+ } catch (error) {
+ console.error('生成条形码失败:', error);
+ MessageUtil.showNetworkError(error);
+ } finally {
+ // 结束生成动画
+ this.setData({ generating: false });
+ }
+ },
+
+ // 生成前验证
+ validateBeforeGenerate() {
+ if (this.data.batchMode) {
+ if (!this.data.batchContent.trim()) {
+ wx.showToast({
+ title: '请输入批量内容',
+ icon: 'none'
+ });
+ return false;
+ }
+
+ const lines = this.data.batchContent.split('\n').filter(line => line.trim());
+ if (lines.length === 0) {
+ wx.showToast({
+ title: '请输入有效内容',
+ icon: 'none'
+ });
+ return false;
+ }
+
+ // 验证每行内容
+ for (let i = 0; i < lines.length; i++) {
+ if (!this.validateSingleContent(lines[i].trim())) {
+ wx.showToast({
+ title: `第${i + 1}行内容格式不正确`,
+ icon: 'none'
+ });
+ return false;
+ }
+ }
+ } else {
+ if (!this.data.inputContent.trim()) {
+ wx.showToast({
+ title: '请输入条形码内容',
+ icon: 'none'
+ });
+ return false;
+ }
+
+ if (!this.validateSingleContent(this.data.inputContent)) {
+ wx.showToast({
+ title: '内容格式不正确',
+ icon: 'none'
+ });
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ // 验证单个内容
+ validateSingleContent(content) {
+ const type = this.data.barcodeType;
+
+ switch (type) {
+ case 'EAN-13':
+ return /^\d{13}$/.test(content);
+ case 'EAN-8':
+ return /^\d{8}$/.test(content);
+ case 'ITF':
+ return /^\d+$/.test(content) && content.length % 2 === 0;
+ case 'Code 39':
+ return /^[A-Z0-9\-. $\/+%]+$/.test(content);
+ case 'Code 128':
+ default:
+ return content.length > 0 && content.length <= this.data.maxLength;
+ }
+ },
+
+ // 执行生成
+ async performGeneration() {
+ // 统一跳转到结果页面,由结果页面根据离线状态选择生成方式
+ const generateData = {
+ content: this.data.batchMode ?
+ this.data.batchContent.split('\n').filter(line => line.trim()) :
+ this.data.inputContent,
+ barcodeType: this.data.barcodeType,
+ foregroundColor: this.data.foregroundColor,
+ backgroundColor: this.data.backgroundColor,
+ barcodeWidth: this.data.barcodeWidth,
+ batchMode: this.data.batchMode
+ };
+
+ // 保存到历史记录
+ this.saveToHistory();
+
+ // 跳转到结果页面
+ const dataParam = encodeURIComponent(JSON.stringify(generateData));
+ wx.navigateTo({
+ url: '/pages/generate-result/generate-result?data=' + dataParam
+ });
+ },
+
+
+
+ // 服务器生成条形码
+ async generateWithServer() {
+ try {
+ const content = this.data.batchMode ?
+ this.data.batchContent.split('\n').filter(line => line.trim()) :
+ this.data.inputContent;
+
+ const params = {
+ codeType: 'barcode',
+ subType: this.data.barcodeType,
+ content: content,
+ batchMode: this.data.batchMode,
+ options: {
+ foregroundColor: this.data.foregroundColor,
+ backgroundColor: this.data.backgroundColor,
+ size: this.data.barcodeWidth
+ }
+ };
+
+ // 调用API服务
+ const response = await apiService.generateCode(params);
+
+ // 保存到历史记录
+ this.saveToHistory();
+
+ if (this.data.batchMode) {
+ // 批量生成,保存压缩包
+ const saveResult = await apiService.saveBatchResult(response.data);
+ // 跳转到结果页面
+ wx.navigateTo({
+ url: '/pages/generate-result/generate-result?type=batch&path=' + encodeURIComponent(saveResult.path)
+ });
+ } else {
+ // 单个生成,保存图片
+ const saveResult = await apiService.saveImageToAlbum(response.data);
+ // 跳转到结果页面
+ wx.navigateTo({
+ url: '/pages/generate-result/generate-result?type=single&path=' + encodeURIComponent(saveResult.path)
+ });
+ }
+ } catch (error) {
+ throw error;
+ }
+ },
+
+
+
+ // 保存到历史记录
+ saveToHistory() {
+ const historyManager = require('../../utils/history-manager.js');
+ const timestamp = new Date().getTime();
+
+ const data = {
+ batchMode: this.data.batchMode,
+ batchContent: this.data.batchContent,
+ inputContent: this.data.inputContent,
+ barcodeType: this.data.barcodeType,
+ foregroundColor: this.data.foregroundColor,
+ backgroundColor: this.data.backgroundColor,
+ barcodeWidth: this.data.barcodeWidth,
+ timestamp: timestamp,
+ localGenerated: this.data.isOfflineMode
+ };
+
+ historyManager.saveBarcodeRecord(data);
+ },
+
+
+});
\ No newline at end of file
diff --git a/pages/generate-barcode/generate-barcode.json b/pages/generate-barcode/generate-barcode.json
new file mode 100644
index 0000000000000000000000000000000000000000..e9ce31f96c550c790cab60d463044ead6823b790
--- /dev/null
+++ b/pages/generate-barcode/generate-barcode.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "小籽二维码",
+ "backgroundTextStyle": "dark",
+ "backgroundColor": "#f8f9fa"
+}
\ No newline at end of file
diff --git a/pages/generate-barcode/generate-barcode.wxml b/pages/generate-barcode/generate-barcode.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..ba0e230fb6798340c7a8774c0edb0dd3cf31e4d5
--- /dev/null
+++ b/pages/generate-barcode/generate-barcode.wxml
@@ -0,0 +1,190 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 条形码类型
+
+
+ Code 128
+ 通用格式
+
+
+ EAN-13
+ 商品条码
+
+
+ EAN-8
+ 短商品码
+
+
+ Code 39
+ 字母数字
+
+
+ ITF
+ 数字格式
+
+
+
+
+
+
+ 美化选项
+
+
+
+ 条形码颜色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 背景色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 尺寸大小
+
+
+ 窄
+ 200px
+
+
+ 标准
+ 300px
+
+
+ 宽
+ 400px
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/generate-barcode/generate-barcode.wxss b/pages/generate-barcode/generate-barcode.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..e647faf2a5c371df9188b21c181d2f0bbc20596b
--- /dev/null
+++ b/pages/generate-barcode/generate-barcode.wxss
@@ -0,0 +1,524 @@
+/* 生成条形码页面样式 - iOS风格 */
+
+/* 主容器 */
+.container {
+ min-height: 100vh;
+ background: linear-gradient(180deg,
+ #f0f9ff 0%,
+ #f8fafc 50%,
+ #f1f5f9 100%);
+}
+
+/* 标题区域 */
+.header-section {
+ padding: 20rpx 40rpx 20rpx;
+ text-align: center;
+}
+
+.page-title {
+ font-size: 50rpx;
+ font-weight: 700;
+ color: #1e293b;
+ line-height: 1.2;
+ margin-bottom: 16rpx;
+}
+
+.page-subtitle {
+ font-size: 26rpx;
+ color: #64748b;
+ line-height: 1.5;
+}
+
+.page-subtitle text {
+ display: block;
+}
+
+/* 输入区域 */
+.input-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.input-card {
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ box-shadow: 0 4rpx 32rpx rgba(0, 0, 0, 0.04);
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+}
+
+.input-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.input-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+/* 批量模式开关 */
+.batch-toggle {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.toggle-text {
+ font-size: 26rpx;
+ color: #64748b;
+ font-weight: 500;
+}
+
+.toggle-switch {
+ width: 80rpx;
+ height: 40rpx;
+ background: #e2e8f0;
+ border-radius: 20rpx;
+ position: relative;
+ transition: all 0.3s ease;
+}
+
+.toggle-switch.active {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+}
+
+.toggle-circle {
+ width: 32rpx;
+ height: 32rpx;
+ background: white;
+ border-radius: 50%;
+ position: absolute;
+ top: 4rpx;
+ left: 4rpx;
+ transition: all 0.3s ease;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.toggle-switch.active .toggle-circle {
+ left: 44rpx;
+}
+
+/* 输入框样式 */
+.input-area {
+ width: 100%;
+ min-height: 120rpx;
+ padding: 24rpx;
+ background: #f8fafc;
+ border: 2rpx solid #e2e8f0;
+ border-radius: 16rpx;
+ font-size: 28rpx;
+ color: #1e293b;
+ line-height: 1.5;
+ box-sizing: border-box;
+}
+
+.input-area.batch-input {
+ min-height: 200rpx;
+}
+
+.input-footer {
+ margin-top: 16rpx;
+ text-align: right;
+}
+
+.char-count {
+ font-size: 24rpx;
+ color: #94a3b8;
+}
+
+/* 类型选择区域 */
+.type-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.section-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 24rpx;
+ padding-left: 8rpx;
+}
+
+.type-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ gap: 16rpx;
+}
+
+.type-grid .type-card:first-child {
+ grid-column: 1 / -1;
+}
+
+.type-card {
+ background: white;
+ border-radius: 16rpx;
+ padding: 20rpx 16rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ border: 2rpx solid transparent;
+ transition: all 0.3s ease;
+ min-height: 80rpx;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.type-card.active {
+ border-color: #22c55e;
+ box-shadow: 0 4rpx 24rpx rgba(34, 197, 94, 0.15);
+}
+
+.type-name {
+ font-size: 24rpx;
+ font-weight: 600;
+ color: #1e293b;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.type-desc {
+ font-size: 20rpx;
+ color: #64748b;
+ display: block;
+}
+
+/* 美化选项区域 */
+.beautify-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.beautify-options {
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+}
+
+.option-group {
+ margin-bottom: 40rpx;
+}
+
+.option-group:last-child {
+ margin-bottom: 0;
+}
+
+.option-label {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 20rpx;
+ display: block;
+}
+
+/* 颜色选择 */
+.color-scroll {
+ width: 100%;
+}
+
+.color-list {
+ display: flex;
+ gap: 24rpx;
+ padding: 8rpx;
+}
+
+.color-item {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ position: relative;
+ transition: all 0.3s ease;
+ flex-shrink: 0;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.color-item:active {
+ transform: scale(0.9);
+}
+
+.color-item.active {
+ transform: scale(1.1);
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
+}
+
+.color-item.active::after {
+ content: '✓';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 24rpx;
+ font-weight: bold;
+ text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
+}
+
+/* 前景色对勾 - 白色 */
+.foreground-color.active::after {
+ color: white;
+}
+
+/* 背景色对勾 - 黑色 */
+.background-color.active::after {
+ color: black;
+ text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.5);
+}
+
+/* 尺寸选择 */
+.size-options {
+ display: flex;
+ gap: 16rpx;
+}
+
+.size-item {
+ flex: 1;
+ background: #f9fafb;
+ border: 2rpx solid #e5e7eb;
+ border-radius: 16rpx;
+ padding: 24rpx 16rpx;
+ text-align: center;
+ transition: all 0.3s ease;
+}
+
+.size-item:active {
+ transform: scale(0.98);
+}
+
+.size-item.active {
+ border-color: #22c55e;
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.05), rgba(22, 163, 74, 0.02));
+ box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.15);
+}
+
+.size-text {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 8rpx;
+}
+
+.size-desc {
+ font-size: 24rpx;
+ color: #6b7280;
+}
+
+/* 生成按钮区域 */
+.generate-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.generate-button {
+ width: 100% !important;
+ height: 120rpx;
+ margin:0 0;
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ border-radius: 20rpx;
+ border: none;
+ color: white;
+ font-size: 0;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ box-shadow: 0 8rpx 24rpx rgba(34, 197, 94, 0.3);
+}
+
+.generate-button:active {
+ transform: scale(0.98);
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.4);
+}
+
+.generate-button.generating {
+ background: linear-gradient(135deg, #9ca3af, #6b7280);
+ box-shadow: 0 4rpx 16rpx rgba(156, 163, 175, 0.3);
+}
+
+.button-normal, .button-loading {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+}
+
+.button-icon {
+ width: 50rpx;
+ height: 50rpx;
+ filter: brightness(0) invert(1);
+}
+
+.button-text {
+ font-size: 40rpx;
+ font-weight: 600;
+ color: white;
+ line-height: 1;
+ margin: 0;
+ padding: 0;
+}
+
+/* 加载动画 */
+.loading-spinner {
+ width: 32rpx;
+ height: 32rpx;
+ border: 3rpx solid rgba(255, 255, 255, 0.3);
+ border-top: 3rpx solid white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* 底部安全区域 */
+.bottom-safe-area {
+ height: 120rpx;
+}
+
+/* 本地生成结果弹窗 */
+.result-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(4rpx);
+}
+
+.result-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.result-content {
+ width: 90%;
+ max-width: 600rpx;
+ background: white;
+ border-radius: 28rpx;
+ overflow: hidden;
+ animation: resultShow 0.3s ease-out;
+ box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
+}
+
+@keyframes resultShow {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8) translateY(40rpx);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 40rpx 40rpx 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.result-title {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #333333;
+}
+
+.result-close {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: #666666;
+ transition: all 0.3s ease;
+}
+
+.result-close:active {
+ background: #e5e5e5;
+ transform: scale(0.9);
+}
+
+.result-body {
+ padding: 40rpx;
+ text-align: center;
+}
+
+.result-barcode-image {
+ width: 100%;
+ max-width: 400rpx;
+ height: 160rpx;
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
+}
+
+.result-desc {
+ font-size: 28rpx;
+ color: #666666;
+ line-height: 1.5;
+}
+
+.result-actions {
+ display: flex;
+ gap: 16rpx;
+ padding: 32rpx;
+ border-top: 1rpx solid #f1f5f9;
+}
+
+.result-btn {
+ flex: 1;
+ height: 80rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: none;
+}
+
+.result-btn.primary {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ color: white;
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.3);
+}
+
+.result-btn.primary:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.3);
+}
+
+.result-btn.secondary {
+ background: #f8fafc;
+ color: #475569;
+ border: 1rpx solid #e2e8f0;
+}
+
+.result-btn.secondary:active {
+ background: #e2e8f0;
+}
\ No newline at end of file
diff --git a/pages/generate-qr/generate-qr.js b/pages/generate-qr/generate-qr.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a3434251f6c9f6cdbcf6c635eb6ee5db183789d
--- /dev/null
+++ b/pages/generate-qr/generate-qr.js
@@ -0,0 +1,357 @@
+// 生成二维码页面 - iOS风格 - 混合生成版本(服务器端+本地生成)
+const apiService = require('../../utils/api-service.js');
+const MessageUtil = require('../../utils/message-util.js');
+const drawQrcode = require('../../utils/qrcode.js');
+
+
+Page({
+ data: {
+ // 输入内容
+ inputContent: '',
+ batchContent: '',
+ batchMode: false,
+ batchLines: 0,
+
+ // 二维码类型
+ qrType: 'standard',
+
+ // 美化选项
+ foregroundColor: '#000000',
+ backgroundColor: '#ffffff',
+ qrSize: 300,
+
+ // 生成状态
+ generating: false,
+
+ // 输入限制
+ maxLength: 1000,
+ maxBatchLength: 5000,
+
+ // 离线模式状态
+ isOfflineMode: false
+ },
+
+ onLoad(options) {
+ if (options.type) {
+ this.setData({
+ qrType: options.type
+ });
+ }
+
+ // 检查离线模式
+ this.checkOfflineMode();
+ },
+
+ onShow() {
+ // 页面显示时重新检查离线状态
+ this.checkOfflineMode();
+ },
+
+ // 检查离线模式状态
+ checkOfflineMode() {
+ try {
+ const app = getApp();
+ const isOffline = app.isOfflineMode();
+
+ this.setData({
+ isOfflineMode: isOffline
+ });
+
+ console.log('二维码生成页面 - 离线模式状态:', isOffline);
+ } catch (error) {
+ console.error('检查离线模式失败:', error);
+ this.setData({
+ isOfflineMode: false
+ });
+ }
+ },
+
+ // 切换批量模式
+ toggleBatchMode() {
+ const newBatchMode = !this.data.batchMode;
+ this.setData({
+ batchMode: newBatchMode
+ });
+
+ // 清空输入内容
+ if (newBatchMode) {
+ this.setData({
+ batchContent: '',
+ batchLines: 0
+ });
+ } else {
+ this.setData({
+ inputContent: ''
+ });
+ }
+ },
+
+ // 单个输入变化
+ onInputChange(e) {
+ this.setData({
+ inputContent: e.detail.value
+ });
+ },
+
+ // 批量输入变化
+ onBatchInputChange: function(e) {
+ var content = e.detail.value;
+ var lines = content.split('\n').filter(function(line) {
+ return line.trim() !== '';
+ });
+
+ this.setData({
+ batchContent: content,
+ batchLines: lines.length
+ });
+ },
+
+ // 切换二维码类型
+ switchType: function(e) {
+ var type = e.currentTarget.dataset.type;
+
+ // 如果在离线模式下选择微型码,给出提示
+ if (type === 'micro' && this.data.isOfflineMode) {
+ wx.showModal({
+ title: '功能限制',
+ content: '离线模式下暂不支持微型二维码生成。\n\n微型二维码需要在线服务支持,请等待网络恢复后使用。',
+ confirmText: '知道了',
+ showCancel: false
+ });
+ return;
+ }
+
+ // 根据类型设置不同的输入限制
+ var maxLength, maxBatchLength;
+ if (type === 'micro') {
+ // 微型码容量较小 (Micro QR Code 最大支持24个字符)
+ maxLength = 24; // Micro QR Code 最大容量
+ maxBatchLength = 1200; // 50 * 24
+
+ // 显示 Micro QR Code 说明
+ wx.showModal({
+ title: 'Micro QR Code 说明',
+ content: 'Micro QR Code 是更小的二维码格式,但容量有限:\n• 最多支持24个字符\n• 适合简短文本、数字\n• 扫描距离较近\n• 部分扫码器可能不支持',
+ confirmText: '我知道了',
+ showCancel: false
+ });
+ } else {
+ // 标准码容量较大
+ maxLength = 1000;
+ maxBatchLength = 5000;
+ }
+
+ this.setData({
+ qrType: type,
+ maxLength: maxLength,
+ maxBatchLength: maxBatchLength
+ });
+ },
+
+ // 选择颜色
+ selectColor: function(e) {
+ var type = e.currentTarget.dataset.type;
+ var color = e.currentTarget.dataset.color;
+ if (type === 'foreground') {
+ this.setData({
+ foregroundColor: color
+ });
+ } else if (type === 'background') {
+ this.setData({
+ backgroundColor: color
+ });
+ }
+ },
+
+ // 选择尺寸
+ selectSize: function(e) {
+ var size = parseInt(e.currentTarget.dataset.size);
+ this.setData({
+ qrSize: size
+ });
+ },
+
+ // 生成二维码
+ generateQR: function() {
+ if (this.data.generating) return;
+
+ // 检查离线模式和微型码的组合
+ if (this.data.isOfflineMode && this.data.qrType === 'micro') {
+ wx.showModal({
+ title: '功能限制',
+ content: '离线模式下暂不支持微型二维码生成。\n\n您可以:\n• 切换为标准二维码\n• 或等待网络恢复后使用微型二维码',
+ confirmText: '切换标准码',
+ cancelText: '取消',
+ success: (res) => {
+ if (res.confirm) {
+ // 用户选择切换为标准二维码
+ this.setData({
+ qrType: 'standard',
+ maxLength: 1000,
+ maxBatchLength: 5000
+ });
+
+ wx.showToast({
+ title: '已切换为标准二维码',
+ icon: 'success',
+ duration: 1500
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // 验证输入
+ if (this.data.batchMode) {
+ if (!this.data.batchContent.trim()) {
+ wx.showToast({
+ title: '请输入批量内容',
+ icon: 'none'
+ });
+ return;
+ }
+
+ var lines = this.data.batchContent.split('\n').filter(function(line) {
+ return line.trim() !== '';
+ });
+ if (lines.length === 0) {
+ wx.showToast({
+ title: '请输入有效内容',
+ icon: 'none'
+ });
+ return;
+ }
+
+ if (lines.length > 50) {
+ wx.showToast({
+ title: '批量生成最多50个',
+ icon: 'none'
+ });
+ return;
+ }
+
+ // 检查微型码的单行长度限制
+ if (this.data.qrType === 'micro') {
+ var longLine = lines.find(function(line) {
+ return line.length > 24;
+ });
+ if (longLine) {
+ wx.showToast({
+ title: 'Micro QR Code 每行最多24个字符',
+ icon: 'none'
+ });
+ return;
+ }
+ }
+ } else {
+ if (!this.data.inputContent.trim()) {
+ wx.showToast({
+ title: '请输入内容',
+ icon: 'none'
+ });
+ return;
+ }
+ }
+
+ // 开始生成
+ this.setData({ generating: true });
+
+ var self = this;
+
+ // 统一跳转到结果页面,由结果页面根据离线状态选择生成方式
+ var generateData = {
+ content: this.data.batchMode ? this.data.batchContent : this.data.inputContent,
+ type: this.data.qrType,
+ foregroundColor: this.data.foregroundColor,
+ backgroundColor: this.data.backgroundColor,
+ size: this.data.qrSize,
+ batchMode: this.data.batchMode
+ };
+
+ // 保存生成历史
+ this.saveGenerateHistory(generateData);
+
+ // 跳转到结果页面
+ var dataParam = encodeURIComponent(JSON.stringify(generateData));
+ wx.navigateTo({
+ url: '/pages/generate-result/generate-result?data=' + dataParam,
+ success: function() {
+ self.setData({ generating: false });
+ },
+ fail: function() {
+ self.setData({ generating: false });
+ }
+ });
+ },
+
+
+
+ // 使用服务器端生成
+ generateWithServer: async function() {
+ try {
+ const content = this.data.batchMode ?
+ this.data.batchContent.split('\n').filter(line => line.trim()) :
+ this.data.inputContent;
+
+ const params = {
+ codeType: 'qrcode',
+ subType: this.data.qrType,
+ content: content,
+ batchMode: this.data.batchMode,
+ options: {
+ foregroundColor: this.data.foregroundColor,
+ backgroundColor: this.data.backgroundColor,
+ size: this.data.qrSize
+ }
+ };
+
+ // 调用API服务
+ const response = await apiService.generateCode(params);
+
+ if (this.data.batchMode) {
+ // 批量生成,保存压缩包
+ const saveResult = await apiService.saveBatchResult(response.data);
+ return {
+ isBatch: true,
+ filePath: saveResult.path,
+ fileName: saveResult.fileName
+ };
+ } else {
+ // 单个生成,保存图片
+ const saveResult = await apiService.saveImageToAlbum(response.data);
+ return {
+ isBatch: false,
+ filePath: saveResult.path
+ };
+ }
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ // 模拟生成过程
+ simulateGeneration: function() {
+ return new Promise(function(resolve) {
+ setTimeout(function() {
+ resolve();
+ }, 1500); // 模拟1.5秒生成时间
+ });
+ },
+
+ // 保存生成历史
+ saveGenerateHistory: function(params) {
+ const historyManager = require('../../utils/history-manager.js');
+ historyManager.saveGenerateHistory(params);
+ },
+
+
+
+ // 页面分享
+ onShareAppMessage() {
+ return {
+ title: '小籽二维码 - 生成二维码',
+ path: '/pages/generate-qr/generate-qr'
+ };
+ }
+});
\ No newline at end of file
diff --git a/pages/generate-qr/generate-qr.json b/pages/generate-qr/generate-qr.json
new file mode 100644
index 0000000000000000000000000000000000000000..e9ce31f96c550c790cab60d463044ead6823b790
--- /dev/null
+++ b/pages/generate-qr/generate-qr.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "小籽二维码",
+ "backgroundTextStyle": "dark",
+ "backgroundColor": "#f8f9fa"
+}
\ No newline at end of file
diff --git a/pages/generate-qr/generate-qr.wxml b/pages/generate-qr/generate-qr.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..154475e7eb9a17026c5ae67e5063ea2ea9d6e956
--- /dev/null
+++ b/pages/generate-qr/generate-qr.wxml
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 二维码类型
+
+
+
+
+
+
+ 标准码
+ 适用于大部分场景
+
+
+
+
+
+ 微型码
+ {{isOfflineMode ? '离线不可用' : '体积更小,信息有限'}}
+
+
+
+
+
+
+
+
+
+
+ 美化选项
+
+
+
+
+ 前景色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 背景色
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 尺寸大小
+
+
+ 小
+ 200px
+
+
+ 中
+ 300px
+
+
+ 大
+ 400px
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/generate-qr/generate-qr.wxss b/pages/generate-qr/generate-qr.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..c65af2382b49e0344fb9e020a050e5a50c20dac5
--- /dev/null
+++ b/pages/generate-qr/generate-qr.wxss
@@ -0,0 +1,574 @@
+/* 生成二维码页面样式 - iOS风格 */
+.container {
+ min-height: 100vh;
+ background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
+}
+
+/* 标题区域 */
+.header-section {
+ padding: 20rpx 40rpx 20rpx;
+ text-align: center;
+}
+
+.page-title {
+ font-size: 50rpx;
+ font-weight: 700;
+ color: #1d1d1f;
+ margin-bottom: 20rpx;
+ letter-spacing: -1rpx;
+}
+
+.page-subtitle {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+}
+
+.page-subtitle text {
+ font-size: 28rpx;
+ color: #6b7280;
+ font-weight: 400;
+ line-height: 1.3;
+}
+
+/* 输入区域 */
+.input-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.input-card {
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+}
+
+.input-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.input-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+}
+
+/* 批量模式切换开关 */
+.batch-toggle {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.toggle-text {
+ font-size: 28rpx;
+ color: #6b7280;
+ font-weight: 500;
+}
+
+.toggle-switch {
+ width: 80rpx;
+ height: 40rpx;
+ background: #e5e7eb;
+ border-radius: 20rpx;
+ position: relative;
+ transition: all 0.3s ease;
+}
+
+.toggle-switch.active {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+}
+
+.toggle-circle {
+ width: 32rpx;
+ height: 32rpx;
+ background: white;
+ border-radius: 50%;
+ position: absolute;
+ top: 4rpx;
+ left: 4rpx;
+ transition: all 0.3s ease;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
+}
+
+.toggle-switch.active .toggle-circle {
+ transform: translateX(40rpx);
+}
+
+/* 输入框样式 */
+.input-area {
+ width: 100%;
+ padding: 24rpx;
+ border: 2rpx solid #e5e7eb;
+ border-radius: 20rpx;
+ font-size: 28rpx;
+ background: #f9fafb;
+ color: #1d1d1f;
+ transition: all 0.3s ease;
+ box-sizing: border-box;
+}
+
+.input-area:focus {
+ border-color: #22c55e;
+ background: white;
+ box-shadow: 0 0 0 6rpx rgba(34, 197, 94, 0.1);
+}
+
+.single-input {
+ min-height: 200rpx;
+}
+
+.batch-input {
+ min-height: 300rpx;
+ font-family: monospace;
+ line-height: 1.6;
+}
+
+.input-footer {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 16rpx;
+}
+
+.char-count {
+ font-size: 24rpx;
+ color: #9ca3af;
+}
+
+/* 类型选择区域 */
+.type-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.section-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 24rpx;
+ padding-left: 8rpx;
+}
+
+.type-cards {
+ display: flex;
+ gap: 24rpx;
+}
+
+.type-card {
+ flex: 1;
+ background: white;
+ border-radius: 20rpx;
+ padding: 32rpx 24rpx;
+ text-align: center;
+ border: 2rpx solid #e5e7eb;
+ transition: all 0.3s ease;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
+}
+
+.type-card:active {
+ transform: scale(0.98);
+}
+
+.type-card.active {
+ border-color: #22c55e;
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.05), rgba(22, 163, 74, 0.02));
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.15);
+}
+
+.type-icon {
+ margin-bottom: 16rpx;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 48rpx;
+}
+
+.type-icon-img {
+ width: 50rpx;
+ height: 50rpx;
+}
+
+.type-name {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 8rpx;
+}
+
+.type-desc {
+ font-size: 24rpx;
+ color: #6b7280;
+}
+
+/* 禁用状态的类型卡片 */
+.type-card.disabled {
+ opacity: 0.5;
+ position: relative;
+ pointer-events: none;
+}
+
+.type-card.disabled .type-name {
+ color: #9ca3af;
+}
+
+.type-card.disabled .type-desc {
+ color: #d1d5db;
+}
+
+.disabled-overlay {
+ position: absolute;
+ top: 8rpx;
+ right: 8rpx;
+ width: 32rpx;
+ height: 32rpx;
+ background: rgba(239, 68, 68, 0.1);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.disabled-icon {
+ width: 20rpx;
+ height: 20rpx;
+ opacity: 0.7;
+}
+
+/* 美化选项区域 */
+.beautify-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.beautify-options {
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+}
+
+.option-group {
+ margin-bottom: 40rpx;
+}
+
+.option-group:last-child {
+ margin-bottom: 0;
+}
+
+.option-label {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 20rpx;
+ display: block;
+}
+
+/* 颜色选择 */
+.color-scroll {
+ width: 100%;
+}
+
+.color-list {
+ display: flex;
+ gap: 24rpx;
+ padding: 8rpx;
+}
+
+.color-item {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ position: relative;
+ transition: all 0.3s ease;
+ flex-shrink: 0;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
+}
+
+.color-item:active {
+ transform: scale(0.9);
+}
+
+.color-item.active {
+ transform: scale(1.1);
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
+}
+
+.color-item.active::after {
+ content: '✓';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 24rpx;
+ font-weight: bold;
+ text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.5);
+}
+
+/* 前景色对勾 - 白色 */
+.foreground-color.active::after {
+ color: white;
+}
+
+/* 背景色对勾 - 黑色 */
+.background-color.active::after {
+ color: black;
+ text-shadow: 0 1rpx 2rpx rgba(255, 255, 255, 0.5);
+}
+
+/* 尺寸选择 */
+.size-options {
+ display: flex;
+ gap: 16rpx;
+}
+
+.size-item {
+ flex: 1;
+ background: #f9fafb;
+ border: 2rpx solid #e5e7eb;
+ border-radius: 16rpx;
+ padding: 24rpx 16rpx;
+ text-align: center;
+ transition: all 0.3s ease;
+}
+
+.size-item:active {
+ transform: scale(0.98);
+}
+
+.size-item.active {
+ border-color: #22c55e;
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.05), rgba(22, 163, 74, 0.02));
+ box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.15);
+}
+
+.size-text {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 8rpx;
+}
+
+.size-desc {
+ font-size: 24rpx;
+ color: #6b7280;
+}
+
+/* 生成按钮区域 */
+.generate-section {
+ padding: 0 40rpx 40rpx;
+}
+
+.generate-button {
+ width: 100% !important;
+ height: 120rpx;
+ margin:0 0;
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ border-radius: 20rpx;
+ border: none;
+ color: white;
+ font-size: 0;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ box-shadow: 0 8rpx 24rpx rgba(34, 197, 94, 0.3);
+}
+
+.generate-button:active {
+ transform: scale(0.98);
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.4);
+}
+
+.generate-button.generating {
+ background: linear-gradient(135deg, #9ca3af, #6b7280);
+ box-shadow: 0 4rpx 16rpx rgba(156, 163, 175, 0.3);
+}
+
+.button-normal, .button-loading {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+}
+
+.button-icon {
+ width: 50rpx;
+ height: 50rpx;
+ filter: brightness(0) invert(1);
+}
+
+.button-text {
+ font-size: 40rpx;
+ font-weight: 600;
+ color: white;
+ line-height: 1;
+ margin: 0;
+ padding: 0;
+}
+
+.loading-spinner {
+ width: 32rpx;
+ height: 32rpx;
+ border: 3rpx solid rgba(255, 255, 255, 0.3);
+ border-top: 3rpx solid white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* 底部安全区域 */
+.bottom-safe-area {
+ height: 120rpx;
+}
+
+/* 本地生成结果弹窗 */
+.result-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(4rpx);
+}
+
+.result-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.result-content {
+ width: 90%;
+ max-width: 600rpx;
+ background: white;
+ border-radius: 28rpx;
+ overflow: hidden;
+ animation: resultShow 0.3s ease-out;
+ box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
+}
+
+@keyframes resultShow {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8) translateY(40rpx);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.result-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 40rpx 40rpx 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.result-title {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #333333;
+}
+
+.result-close {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: #666666;
+ transition: all 0.3s ease;
+}
+
+.result-close:active {
+ background: #e5e5e5;
+ transform: scale(0.9);
+}
+
+.result-body {
+ padding: 40rpx;
+ text-align: center;
+}
+
+.result-qr-image {
+ width: 300rpx;
+ height: 300rpx;
+ border-radius: 16rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
+}
+
+.result-desc {
+ font-size: 28rpx;
+ color: #666666;
+ line-height: 1.5;
+}
+
+.result-actions {
+ display: flex;
+ gap: 16rpx;
+ padding: 32rpx;
+ border-top: 1rpx solid #f1f5f9;
+}
+
+.result-btn {
+ flex: 1;
+ height: 80rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ margin: 0;
+ padding: 0;
+ border: none;
+ background: none;
+}
+
+.result-btn.primary {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ color: white;
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.3);
+}
+
+.result-btn.primary:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.3);
+}
+
+.result-btn.secondary {
+ background: #f8fafc;
+ color: #475569;
+ border: 1rpx solid #e2e8f0;
+}
+
+.result-btn.secondary:active {
+ background: #e2e8f0;
+}
\ No newline at end of file
diff --git a/pages/generate-result/generate-result.js b/pages/generate-result/generate-result.js
new file mode 100644
index 0000000000000000000000000000000000000000..0d71954776ee58fdd64a0c467c14e81ab462a6df
--- /dev/null
+++ b/pages/generate-result/generate-result.js
@@ -0,0 +1,1037 @@
+// 码生成结果页面(支持二维码和条形码)
+const drawQrcode = require('../../utils/qrcode.js');
+const apiService = require('../../utils/api-service.js');
+const MessageUtil = require('../../utils/message-util.js');
+const JsBarcodeUtil = require('../../utils/jsbarcode-util.js');
+
+Page({
+ data: {
+ // 基础数据
+ content: '',
+ codeType: 'barcode', // 'barcode' 或 'qr'
+ barcodeType: '',
+ qrType: '',
+ typeText: '',
+ batchMode: false,
+ barcodes: [],
+
+ // 样式设置
+ foregroundColor: '#000000',
+ backgroundColor: '#ffffff',
+ barcodeWidth: 300,
+ qrSize: 300,
+
+ // Canvas 尺寸
+ canvasWidth: 300,
+ canvasHeight: 120,
+
+ // 缩放相关
+ scaleRatio: 1,
+ batchScaleRatio: 1,
+ screenWidth: 0, // 初始为0,将在onLoad时动态获取
+
+ // 状态控制
+ loading: false,
+ loadingText: '正在生成...',
+
+ // 离线模式状态
+ isOfflineMode: false
+ },
+
+ onLoad(options) {
+ console.log('码结果页面参数:', options);
+
+ // 同步获取屏幕信息(必须在其他计算之前完成)
+ this.initScreenInfo();
+
+ // 检查离线模式状态
+ this.checkOfflineMode();
+
+ if (options.data) {
+ try {
+ const data = JSON.parse(decodeURIComponent(options.data));
+ console.log('解析的数据:', data);
+
+ // 判断码类型
+ const isQR = data.type === 'standard' || data.type === 'micro' || data.qrType || !data.barcodeType;
+ const codeType = isQR ? 'qr' : 'barcode';
+
+ // 处理批量数据
+ let barcodes = [];
+ if (data.batchMode) {
+ if (isQR) {
+ // 二维码批量模式,需要从内容中解析
+ const lines = data.content.split('\n').filter(line => line.trim());
+ barcodes = lines.map(line => ({ content: line.trim() }));
+ } else {
+ // 条形码批量模式,内容可能是数组或字符串
+ if (Array.isArray(data.content)) {
+ barcodes = data.content.map(item => ({ content: item }));
+ } else {
+ const lines = data.content.split('\n').filter(line => line.trim());
+ barcodes = lines.map(line => ({ content: line.trim() }));
+ }
+ }
+ }
+
+ this.setData({
+ content: Array.isArray(data.content) ? data.content[0] : data.content || '',
+ codeType: codeType,
+ barcodeType: data.barcodeType || 'Code 128',
+ qrType: data.type || 'standard',
+ typeText: this.getTypeText(isQR ? data.type : data.barcodeType),
+ batchMode: data.batchMode || false,
+ barcodes: barcodes,
+ foregroundColor: data.foregroundColor || '#000000',
+ backgroundColor: data.backgroundColor || '#ffffff',
+ barcodeWidth: data.barcodeWidth || 300,
+ qrSize: data.size || 300
+ });
+
+ // 设置Canvas尺寸和缩放比例(此时screenWidth已经是真实值)
+ this.setCanvasSize();
+
+ // 保存到历史记录
+ this.saveToHistory(data);
+
+ // 延迟绘制码
+ setTimeout(() => {
+ if (this.data.isOfflineMode) {
+ console.log('离线模式,使用本地生成');
+ // 检查是否为批量生成
+ if (data.batchMode) {
+ MessageUtil.showError('离线模式下暂不支持批量生成');
+ return;
+ }
+
+ if (isQR) {
+ this.generateQRLocally(data);
+ } else {
+ this.generateBarcodeLocally(data);
+ }
+ } else {
+ console.log('在线模式,使用服务器生成');
+ // 在线模式应该通过服务器API生成,这里应该通过API调用获得结果
+ // 由于当前服务器接口未提供,这里会报错,这是正确的行为
+ this.generateWithServer(data).catch(error => {
+ console.error('服务器生成失败:', error);
+ MessageUtil.showNetworkError(error);
+ });
+ }
+ }, 500);
+
+ } catch (error) {
+ console.error('解析参数失败:', error);
+ wx.showToast({
+ title: '参数错误',
+ icon: 'none'
+ });
+ }
+ }
+ },
+
+ // 初始化屏幕信息(同步执行,确保后续计算准确)
+ initScreenInfo() {
+ try {
+ const systemInfo = wx.getSystemInfoSync();
+ const screenWidth = systemInfo.screenWidth || systemInfo.windowWidth || 375;
+
+ console.log('屏幕信息初始化:', {
+ screenWidth: screenWidth,
+ screenHeight: systemInfo.screenHeight,
+ pixelRatio: systemInfo.pixelRatio,
+ system: systemInfo.system,
+ model: systemInfo.model
+ });
+
+ this.setData({
+ screenWidth: screenWidth
+ });
+
+ // 存储到页面实例,供后续方法使用
+ this.screenWidth = screenWidth;
+
+ } catch (error) {
+ console.error('获取屏幕信息失败,使用通用默认值:', error);
+ // 使用iPhone 6/7/8的标准宽度作为fallback
+ const fallbackWidth = 375;
+ this.setData({
+ screenWidth: fallbackWidth
+ });
+ this.screenWidth = fallbackWidth;
+ }
+ },
+
+ // 获取类型显示文本
+ getTypeText(type) {
+ const typeMap = {
+ // 条形码类型
+ 'Code 128': 'Code 128 条形码',
+ 'EAN-13': 'EAN-13 商品条码',
+ 'EAN-8': 'EAN-8 商品条码',
+ 'Code 39': 'Code 39 条形码',
+ 'ITF': 'ITF 交叉25码',
+ // 二维码类型
+ 'standard': '标准二维码',
+ 'micro': '微型二维码'
+ };
+ return typeMap[type] || type;
+ },
+
+ // 设置Canvas尺寸
+ setCanvasSize() {
+ // 确保屏幕宽度已经正确获取
+ const currentScreenWidth = this.data.screenWidth || this.screenWidth || 375;
+
+ if (currentScreenWidth <= 0) {
+ console.warn('屏幕宽度获取异常,使用默认值375px');
+ this.setData({ screenWidth: 375 });
+ }
+
+ if (this.data.codeType === 'qr') {
+ // 二维码是正方形
+ const actualSize = this.data.qrSize;
+
+ // 精确计算可用宽度
+ const pagePadding = Math.floor(currentScreenWidth * 0.08); // 约8%作为页面边距
+ const cardPadding = Math.floor(currentScreenWidth * 0.12); // 约12%作为卡片边距
+ const maxWidth = currentScreenWidth - pagePadding - cardPadding;
+
+ // 计算缩放比例 - 支持放大和缩小
+ let scaleRatio = maxWidth / actualSize;
+
+ // 限制缩放比例在合理范围内
+ // 最小缩放:0.2(避免过度缩小)
+ // 最大缩放:2.5(避免过度放大)
+ scaleRatio = Math.max(0.2, Math.min(2.5, scaleRatio));
+
+ console.log(`二维码缩放计算详情:
+ 屏幕宽度: ${currentScreenWidth}px
+ 实际尺寸: ${actualSize}px
+ 页面边距: ${pagePadding}px
+ 卡片边距: ${cardPadding}px
+ 可用宽度: ${maxWidth}px
+ 计算比例: ${(maxWidth / actualSize).toFixed(3)}
+ 最终比例: ${scaleRatio.toFixed(3)} ${scaleRatio > 1 ? '(放大)' : scaleRatio < 1 ? '(缩小)' : '(原尺寸)'}
+ 显示尺寸: ${Math.round(actualSize * scaleRatio)}px`);
+
+ // 直接计算显示尺寸,避免使用 transform: scale()
+ const displayWidth = Math.round(actualSize * scaleRatio);
+ const displayHeight = Math.round(actualSize * scaleRatio);
+
+ this.setData({
+ canvasWidth: actualSize,
+ canvasHeight: actualSize,
+ displayWidth: displayWidth,
+ displayHeight: displayHeight,
+ scaleRatio: scaleRatio
+ });
+
+ } else {
+ // 条形码是长方形
+ const actualWidth = this.data.barcodeWidth;
+ const actualHeight = Math.max(80, actualWidth * 0.3);
+
+ // 计算可用宽度
+ const pagePadding = Math.floor(currentScreenWidth * 0.08);
+ const cardPadding = Math.floor(currentScreenWidth * 0.12);
+ const maxWidth = currentScreenWidth - pagePadding - cardPadding;
+
+ // 计算缩放比例 - 支持放大和缩小
+ let scaleRatio = maxWidth / actualWidth;
+
+ // 限制缩放比例在合理范围内
+ scaleRatio = Math.max(0.2, Math.min(2.5, scaleRatio));
+
+ // 如果计算出的缩放比例接近1.0,则不进行缩放
+ if (scaleRatio > 0.95 && scaleRatio < 1.05) {
+ scaleRatio = 1.0;
+ }
+
+ console.log(`条形码缩放计算详情:
+ 屏幕宽度: ${currentScreenWidth}px
+ 实际尺寸: ${actualWidth}x${actualHeight}px
+ 页面边距: ${pagePadding}px
+ 卡片边距: ${cardPadding}px
+ 可用宽度: ${maxWidth}px
+ 计算比例: ${(maxWidth / actualWidth).toFixed(3)}
+ 最终比例: ${scaleRatio.toFixed(3)} ${scaleRatio > 1 ? '(放大)' : scaleRatio < 1 ? '(缩小)' : '(原尺寸)'}
+ 显示尺寸: ${Math.round(actualWidth * scaleRatio)}x${Math.round(actualHeight * scaleRatio)}px`);
+
+ // 直接计算显示尺寸,避免使用 transform: scale()
+ const displayWidth = Math.round(actualWidth * scaleRatio);
+ const displayHeight = Math.round(actualHeight * scaleRatio);
+
+ this.setData({
+ canvasWidth: actualWidth,
+ canvasHeight: actualHeight,
+ displayWidth: displayWidth,
+ displayHeight: displayHeight,
+ scaleRatio: scaleRatio
+ });
+ }
+ },
+
+ // 绘制二维码
+ drawQRCodes() {
+ if (this.data.batchMode) {
+ this.drawBatchQRCodes();
+ } else {
+ this.drawSingleQRCode();
+ }
+ },
+
+ // 绘制条形码
+ drawBarcodes() {
+ if (this.data.batchMode) {
+ this.drawBatchBarcodes();
+ } else {
+ this.drawSingleBarcode();
+ }
+ },
+
+ // 绘制单个二维码
+ drawSingleQRCode() {
+ try {
+ if (this.data.qrType === 'micro') {
+ // 微型二维码仅支持在线生成
+ wx.showToast({
+ title: '微型二维码仅支持在线生成',
+ icon: 'none'
+ });
+ return;
+ } else {
+ // 使用标准二维码库 - 使用显示尺寸进行绘制
+ drawQrcode({
+ canvasId: 'barcodeCanvas',
+ text: this.data.content,
+ width: this.data.displayWidth,
+ height: this.data.displayHeight,
+ foreground: this.data.foregroundColor,
+ background: this.data.backgroundColor,
+ _this: this
+ });
+ }
+ } catch (error) {
+ console.error('绘制二维码失败:', error);
+ wx.showToast({
+ title: '生成失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 绘制单个条形码
+ drawSingleBarcode() {
+ const ctx = wx.createCanvasContext('barcodeCanvas', this);
+ // 使用显示尺寸进行绘制
+ this.drawBarcodeOnCanvas(ctx, this.data.content, this.data.displayWidth, this.data.displayHeight);
+ },
+
+ // 绘制批量二维码
+ drawBatchQRCodes() {
+ // 计算批量模式下的缩放比例
+ const screenWidth = this.data.screenWidth || 375;
+ const itemWidth = 150; // 二维码固定宽度
+ const availableWidth = screenWidth * 0.3; // 可用宽度约为屏幕的30%
+ const batchScaleRatio = Math.min(1.0, availableWidth / itemWidth);
+
+ // 计算批量显示尺寸
+ const batchDisplayWidth = Math.round(itemWidth * batchScaleRatio);
+ const batchDisplayHeight = Math.round(itemWidth * batchScaleRatio);
+
+ this.setData({
+ batchScaleRatio: batchScaleRatio,
+ batchDisplayWidth: batchDisplayWidth,
+ batchDisplayHeight: batchDisplayHeight
+ });
+
+ this.data.barcodes.forEach((item, index) => {
+ const canvasId = `barcodeCanvas${index}`;
+
+ setTimeout(() => {
+ try {
+ if (this.data.qrType === 'micro') {
+ // 微型二维码仅支持在线生成
+ console.log(`批量第${index}个微型二维码跳过(仅支持在线生成)`);
+ return;
+ } else {
+ // 标准二维码 - 使用批量显示尺寸
+ drawQrcode({
+ canvasId: canvasId,
+ text: item.content,
+ width: this.data.batchDisplayWidth,
+ height: this.data.batchDisplayHeight,
+ foreground: this.data.foregroundColor,
+ background: this.data.backgroundColor,
+ _this: this
+ });
+ }
+ } catch (error) {
+ console.error(`绘制第${index}个二维码失败:`, error);
+ }
+ }, index * 100);
+ });
+ },
+
+ // 绘制批量条形码
+ drawBatchBarcodes() {
+ // 计算批量模式下的缩放比例
+ const screenWidth = this.data.screenWidth || 375;
+ const itemWidth = 200; // 条形码固定宽度
+ const itemHeight = 80; // 条形码固定高度
+ const availableWidth = screenWidth * 0.35; // 可用宽度约为屏幕的35%
+ const batchScaleRatio = Math.min(1.0, availableWidth / itemWidth);
+
+ // 计算批量显示尺寸
+ const batchDisplayWidth = Math.round(itemWidth * batchScaleRatio);
+ const batchDisplayHeight = Math.round(itemHeight * batchScaleRatio);
+
+ this.setData({
+ batchScaleRatio: batchScaleRatio,
+ batchDisplayWidth: batchDisplayWidth,
+ batchDisplayHeight: batchDisplayHeight
+ });
+
+ this.data.barcodes.forEach((item, index) => {
+ setTimeout(() => {
+ const ctx = wx.createCanvasContext(`barcodeCanvas${index}`, this);
+ // 使用批量显示尺寸进行绘制
+ this.drawBarcodeOnCanvas(ctx, item.content, this.data.batchDisplayWidth, this.data.batchDisplayHeight);
+ }, index * 100);
+ });
+ },
+
+ // 在Canvas上绘制条形码
+ drawBarcodeOnCanvas(ctx, content, width, height) {
+ try {
+ const pattern = this.generateBarcodePattern(content);
+ if (!pattern) {
+ throw new Error('无法生成条形码图案');
+ }
+
+ // 获取设备像素比,提高绘制质量
+ const systemInfo = wx.getSystemInfoSync();
+ const pixelRatio = systemInfo.pixelRatio || 2;
+
+ // 计算实际绘制尺寸(考虑像素密度)
+ const drawWidth = width * pixelRatio;
+ const drawHeight = height * pixelRatio;
+
+ // 设置Canvas尺寸
+ ctx.scale(pixelRatio, pixelRatio);
+
+ // 清空画布
+ ctx.setFillStyle(this.data.backgroundColor || '#ffffff');
+ ctx.fillRect(0, 0, width, height);
+
+ // 绘制条形码
+ const barWidth = width / pattern.length;
+
+ // 将字符串转换为数组进行遍历
+ for (let i = 0; i < pattern.length; i++) {
+ if (pattern[i] === '1') {
+ ctx.setFillStyle(this.data.foregroundColor || '#000000');
+ ctx.fillRect(i * barWidth, 0, barWidth, height * 0.8);
+ }
+ }
+
+ // 绘制文本 - 根据Canvas大小动态调整字体大小
+ const fontSize = Math.max(10, Math.min(20, height * 0.15));
+ ctx.setFillStyle(this.data.foregroundColor || '#000000');
+ ctx.setFontSize(fontSize);
+ ctx.setTextAlign('center');
+ ctx.fillText(content, width / 2, height * 0.95);
+
+ // 使用与二维码库相同的draw方式,确保渲染完成
+ ctx.draw(false, () => {
+ console.log('条形码Canvas绘制完成,尺寸:', width, 'x', height, '像素比:', pixelRatio);
+ });
+ } catch (error) {
+ console.error('绘制条形码失败:', error);
+ wx.showToast({
+ title: '生成条形码失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 生成条形码图案(标准Code 128实现)
+ generateBarcodePattern(content) {
+ // 标准Code 128编码表 (值0-106: 0-102数据 + 103-105起始码 + 106停止码)
+ const CODE128_PATTERNS = [
+ // 值 0-9
+ '11011001100', '11001101100', '11001100110', '10010011000', '10010001100',
+ '10001001100', '10011001000', '10011000100', '10001100100', '11001001000',
+ // 值 10-19
+ '11001000100', '11000100100', '10110011100', '10011011100', '10011001110',
+ '10111001100', '10011101100', '10011100110', '11001110010', '11001011100',
+ // 值 20-29
+ '11001001110', '11011100100', '11001110100', '11101101110', '11101001100',
+ '11100101100', '11100100110', '11101100100', '11100110100', '11100110010',
+ // 值 30-39
+ '11011011000', '11011000110', '11000110110', '10100011000', '10001011000',
+ '10001000110', '10110001000', '10001101000', '10001100010', '11010001000',
+ // 值 40-49
+ '11000101000', '11000100010', '10110111000', '10110001110', '10001101110',
+ '10111011000', '10111000110', '10001110110', '11101110110', '11010001110',
+ // 值 50-59
+ '11000101110', '11011101000', '11011100010', '11011101110', '11101011000',
+ '11101000110', '11100010110', '11101101000', '11101100010', '11100011010',
+ // 值 60-69
+ '11101111010', '11001000010', '11110001010', '10100110000', '10100001100',
+ '10010110000', '10010000110', '10000101100', '10000100110', '10110010000',
+ // 值 70-79
+ '10110000100', '10011010000', '10011000010', '10000110100', '10000110010',
+ '11000010010', '11001010000', '11110111010', '11000010100', '10001111010',
+ // 值 80-89
+ '10100111100', '10010111100', '10010011110', '10111100100', '10011110100',
+ '10011110010', '11110100100', '11110010100', '11110010010', '11011011110',
+ // 值 90-99
+ '11011110110', '11110110110', '10101111000', '10100011110', '10001011110',
+ '10111101000', '10111100010', '11110101000', '11110100010', '10111011110',
+ // 值 100-102 (FNC4, Code A, FNC1)
+ '10111101110', '11101011110', '11110101110',
+ // 值 103-105 (Start A, Start B, Start C)
+ '11010000100', '11010010000', '11010011100',
+ // 值 106 (Stop)
+ '1100011101011'
+ ];
+
+ // 验证输入
+ if (!content || content.length === 0) {
+ console.error('内容不能为空');
+ return null;
+ }
+
+ try {
+ // Start Code B (值104) - 用于ASCII 32-127字符
+ const START_CODE_B = 104;
+ let pattern = CODE128_PATTERNS[START_CODE_B];
+ let checksum = START_CODE_B; // 起始码也参与校验计算
+
+ // 编码每个字符
+ for (let i = 0; i < content.length; i++) {
+ const char = content.charAt(i);
+ const ascii = char.charCodeAt(0);
+
+ // Code 128 Set B: ASCII 32-127 映射到值 0-95
+ if (ascii >= 32 && ascii <= 127) {
+ const codeValue = ascii - 32; // 转换为Code 128值
+
+ // 验证值范围 (0-95对应ASCII 32-127)
+ if (codeValue >= 0 && codeValue <= 95) {
+ pattern += CODE128_PATTERNS[codeValue];
+ // 校验和计算:值 × 位置权重,起始码位置为0,数据从位置1开始
+ checksum += codeValue * (i + 1);
+ } else {
+ console.error(`字符值超出Code Set B范围: ${char} (ASCII: ${ascii}, 值: ${codeValue})`);
+ return null;
+ }
+ } else {
+ console.error(`不支持的ASCII字符: ${char} (ASCII: ${ascii})`);
+ return null;
+ }
+ }
+
+ // 计算校验码 (modulo 103)
+ const checksumValue = checksum % 103;
+
+ // 验证校验码值范围
+ if (checksumValue < 0 || checksumValue >= CODE128_PATTERNS.length - 1) {
+ console.error(`校验码值超出范围: ${checksumValue}`);
+ return null;
+ }
+
+ // 添加校验码
+ pattern += CODE128_PATTERNS[checksumValue];
+
+ // 添加停止码 (值106,索引106)
+ const STOP_CODE = 106;
+ pattern += CODE128_PATTERNS[STOP_CODE];
+
+ console.log('Code 128标准生成详情:', {
+ content: content,
+ contentLength: content.length,
+ startCode: START_CODE_B,
+ checksumCalculation: checksum,
+ checksumValue: checksumValue,
+ stopCode: STOP_CODE,
+ totalPatternLength: pattern.length,
+ expectedModules: (content.length + 3) * 11 + 2, // (数据+起始+校验+停止)*11 + 停止码额外2模块
+ patternPreview: pattern.substring(0, 50) + '...'
+ });
+
+ return pattern;
+
+ } catch (error) {
+ console.error('Code 128标准生成失败:', error);
+ return null;
+ }
+ },
+
+ // 保存到相册
+ saveToAlbum() {
+ wx.showLoading({
+ title: '正在保存...'
+ });
+
+ const canvasId = 'barcodeCanvas';
+
+ const { displayWidth, displayHeight } = this.data;
+
+ wx.canvasToTempFilePath({
+ canvasId: canvasId,
+ width: displayWidth,
+ height: displayHeight,
+ destWidth: displayWidth * 2, // 提高分辨率
+ destHeight: displayHeight * 2,
+ success: (res) => {
+ this.saveImageToAlbum(res.tempFilePath);
+ },
+ fail: (error) => {
+ console.error('生成图片失败:', error);
+ wx.hideLoading();
+ wx.showToast({
+ title: '生成图片失败',
+ icon: 'none'
+ });
+ }
+ }, this);
+ },
+
+ // 分享条形码
+ shareBarcode() {
+ wx.showLoading({
+ title: '正在生成分享图片...'
+ });
+
+ const { displayWidth, displayHeight } = this.data;
+
+ wx.canvasToTempFilePath({
+ canvasId: 'barcodeCanvas',
+ width: displayWidth,
+ height: displayHeight,
+ destWidth: displayWidth * 2,
+ destHeight: displayHeight * 2,
+ success: (res) => {
+ wx.hideLoading();
+ this.shareToFriend(res.tempFilePath);
+ },
+ fail: (error) => {
+ console.error('生成分享图片失败:', error);
+ wx.hideLoading();
+ wx.showToast({
+ title: '生成图片失败',
+ icon: 'none'
+ });
+ }
+ }, this);
+ },
+
+ // 分享给朋友
+ shareToFriend(imagePath) {
+ wx.showActionSheet({
+ itemList: ['发送给朋友', '分享到朋友圈', '保存图片'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ // 发送给朋友
+ wx.showShareImageMenu({
+ path: imagePath,
+ fail: (error) => {
+ console.error('分享失败:', error);
+ wx.showToast({
+ title: '分享失败',
+ icon: 'none'
+ });
+ }
+ });
+ } else if (res.tapIndex === 1) {
+ // 分享到朋友圈(小程序无法直接分享到朋友圈,可以保存图片让用户手动分享)
+ this.saveImageToAlbum(imagePath);
+ } else if (res.tapIndex === 2) {
+ // 保存图片
+ this.saveImageToAlbum(imagePath);
+ }
+ }
+ });
+ },
+
+ // 保存图片到相册
+ saveImageToAlbum(imagePath) {
+ wx.getSetting({
+ success: (res) => {
+ if (res.authSetting['scope.writePhotosAlbum']) {
+ // 已经授权
+ this.doSaveImage(imagePath);
+ } else {
+ // 未授权,请求授权
+ wx.authorize({
+ scope: 'scope.writePhotosAlbum',
+ success: () => {
+ this.doSaveImage(imagePath);
+ },
+ fail: () => {
+ wx.showModal({
+ title: '提示',
+ content: '需要您授权保存相册权限',
+ showCancel: false,
+ confirmText: '去设置',
+ success: (modalRes) => {
+ if (modalRes.confirm) {
+ wx.openSetting();
+ }
+ }
+ });
+ }
+ });
+ }
+ }
+ });
+ },
+
+ // 执行保存图片
+ doSaveImage(imagePath) {
+ wx.saveImageToPhotosAlbum({
+ filePath: imagePath,
+ success: () => {
+ wx.hideLoading();
+ wx.showToast({
+ title: '保存成功',
+ icon: 'success'
+ });
+ },
+ fail: (error) => {
+ console.error('保存失败:', error);
+ wx.hideLoading();
+ wx.showToast({
+ title: '保存失败',
+ icon: 'none'
+ });
+ }
+ });
+ },
+
+ // 保存全部到相册
+ saveAllToAlbum() {
+ if (this.data.barcodes.length === 0) {
+ wx.showToast({
+ title: '没有要保存的内容',
+ icon: 'none'
+ });
+ return;
+ }
+
+ wx.showModal({
+ title: '确认保存',
+ content: `确定要保存全部 ${this.data.barcodes.length} 个${this.data.codeType === 'qr' ? '二维码' : '条形码'}到相册吗?`,
+ success: (res) => {
+ if (res.confirm) {
+ this.setData({
+ loading: true,
+ loadingText: '正在批量保存...'
+ });
+ this.saveBatchImages();
+ }
+ }
+ });
+ },
+
+ // 批量保存图片
+ saveBatchImages() {
+ let savedCount = 0;
+ const totalCount = this.data.barcodes.length;
+
+ const saveNext = (index) => {
+ if (index >= totalCount) {
+ this.setData({
+ loading: false
+ });
+ wx.showToast({
+ title: `成功保存${savedCount}张图片`,
+ icon: 'success'
+ });
+ return;
+ }
+
+ const canvasId = `barcodeCanvas${index}`;
+
+ wx.canvasToTempFilePath({
+ canvasId: canvasId,
+ width: this.data.batchDisplayWidth,
+ height: this.data.batchDisplayHeight,
+ success: (res) => {
+ this.doSaveImage(res.tempFilePath);
+ savedCount++;
+ setTimeout(() => saveNext(index + 1), 500);
+ },
+ fail: (error) => {
+ console.error(`保存第${index + 1}个图片失败:`, error);
+ setTimeout(() => saveNext(index + 1), 500);
+ }
+ }, this);
+ };
+
+ saveNext(0);
+ },
+
+ // 分享全部
+ shareAll() {
+ wx.showToast({
+ title: '批量分享功能开发中',
+ icon: 'none'
+ });
+ },
+
+ // 逐个分享
+ async shareAllOneByOne() {
+ if (this.data.barcodes.length === 0) {
+ wx.showToast({
+ title: '没有要分享的内容',
+ icon: 'none'
+ });
+ return;
+ }
+
+ this.setData({
+ loading: true,
+ loadingText: '正在准备分享...'
+ });
+
+ try {
+ for (let i = 0; i < this.data.barcodes.length; i++) {
+ const canvasId = `barcodeCanvas${i}`;
+
+ const tempFilePath = await new Promise((resolve, reject) => {
+ wx.canvasToTempFilePath({
+ canvasId: canvasId,
+ width: this.data.batchDisplayWidth,
+ height: this.data.batchDisplayHeight,
+ success: (res) => resolve(res.tempFilePath),
+ fail: reject
+ }, this);
+ });
+
+ // 这里可以调用分享API,或者保存到临时目录
+ console.log(`第${i + 1}个码的临时文件路径:`, tempFilePath);
+ }
+
+ this.setData({
+ loading: false
+ });
+
+ wx.showToast({
+ title: '准备完成',
+ icon: 'success'
+ });
+
+ } catch (error) {
+ console.error('批量分享失败:', error);
+ this.setData({
+ loading: false
+ });
+ wx.showToast({
+ title: '分享失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 重新生成
+ regenerate() {
+ wx.navigateBack();
+ },
+
+ // 保存到历史记录
+ saveToHistory(data) {
+ const historyManager = require('../../utils/history-manager.js');
+ historyManager.saveQRGenerateRecord(data);
+ },
+
+ // 分享给朋友
+ onShareAppMessage() {
+ return {
+ title: `我生成了一个${this.data.codeType === 'qr' ? '二维码' : '条形码'}`,
+ path: '/pages/index/index'
+ };
+ },
+
+ // 检查离线模式状态
+ checkOfflineMode() {
+ const app = getApp();
+ const isOffline = app.isOfflineMode();
+ console.log('结果页检查离线模式状态:', isOffline);
+ this.setData({ isOfflineMode: isOffline });
+ },
+
+ // 本地生成二维码
+ generateQRLocally(data) {
+ const content = Array.isArray(data.content) ? data.content[0] : data.content;
+ const trimmedContent = content.trim();
+
+ if (data.type === 'micro') {
+ // 微型二维码仅支持在线生成
+ MessageUtil.showError('微型二维码仅支持在线生成,请确保网络连接正常');
+ return;
+ } else {
+ // 生成标准二维码
+ this.generateStandardQRLocally(trimmedContent, data);
+ }
+ },
+
+ // 本地生成标准二维码
+ generateStandardQRLocally(content, data) {
+ try {
+ // 使用页面设置的显示尺寸,确保与WXML中的显示尺寸一致
+ drawQrcode({
+ width: this.data.displayWidth,
+ height: this.data.displayHeight,
+ canvasId: 'barcodeCanvas',
+ text: content,
+ foreground: data.foregroundColor || '#000000',
+ background: data.backgroundColor || '#ffffff',
+ _this: this,
+ callback: (res) => {
+ console.log('标准二维码本地生成完成,尺寸:', this.data.displayWidth, 'x', this.data.displayHeight);
+ this.handleLocalGenerateSuccess();
+ }
+ });
+ } catch (error) {
+ console.error('标准二维码本地生成失败:', error);
+ MessageUtil.showError('生成失败: ' + error.message);
+ }
+ },
+
+
+
+ // 本地生成条形码
+ generateBarcodeLocally(data) {
+ const content = Array.isArray(data.content) ? data.content[0] : data.content;
+ const trimmedContent = content.trim();
+
+ // 简单的兼容性检查
+ if (!trimmedContent || trimmedContent.length === 0) {
+ MessageUtil.showError('请输入有效内容');
+ return;
+ }
+
+ // 检查内容长度(Code 128 通常支持较长的内容)
+ if (trimmedContent.length > 100) {
+ MessageUtil.showError('内容过长,请减少字符数量');
+ return;
+ }
+
+ try {
+ console.log('[generateBarcodeLocally] 开始使用JsBarcode生成条形码:', trimmedContent);
+
+ // 使用Canvas 2D API获取canvas节点
+ wx.createSelectorQuery()
+ .in(this)
+ .select('#barcodeCanvas')
+ .fields({
+ node: true,
+ size: true
+ })
+ .exec((res) => {
+ if (!res[0] || !res[0].node) {
+ console.error('[generateBarcodeLocally] 无法获取canvas节点');
+ MessageUtil.showError('Canvas初始化失败');
+ return;
+ }
+
+ const canvas = res[0].node;
+ const dpr = wx.getSystemInfoSync().pixelRatio || 1;
+
+ // 设置canvas尺寸为显示尺寸(简化处理)
+ canvas.width = this.data.displayWidth;
+ canvas.height = this.data.displayHeight;
+
+ console.log('[generateBarcodeLocally] Canvas设置:', {
+ width: canvas.width,
+ height: canvas.height,
+ displayWidth: this.data.displayWidth,
+ displayHeight: this.data.displayHeight,
+ dpr: dpr
+ });
+
+ // 使用JsBarcode生成条形码,使用逻辑尺寸
+ const options = {
+ width: 2,
+ height: this.data.displayHeight - 20, // 留出一些边距
+ displayValue: false, // 不显示文本,避免重叠
+ background: data.backgroundColor || '#ffffff',
+ lineColor: data.foregroundColor || '#000000',
+ margin: 10,
+ marginTop: 10,
+ marginBottom: 10,
+ marginLeft: 10,
+ marginRight: 10
+ };
+
+ try {
+ JsBarcodeUtil.generateCode128(canvas, trimmedContent, options);
+ console.log('[generateBarcodeLocally] 条形码生成完成');
+ this.handleLocalGenerateSuccess();
+ } catch (error) {
+ console.error('[generateBarcodeLocally] JsBarcode生成失败:', error);
+ MessageUtil.showError('生成失败: ' + error.message);
+ }
+ });
+
+ } catch (error) {
+ console.error('[generateBarcodeLocally] 条形码本地生成失败:', error);
+ MessageUtil.showError('生成失败: ' + error.message);
+ }
+ },
+
+ // 服务器生成
+ async generateWithServer(data) {
+ try {
+ const params = {
+ codeType: this.data.codeType === 'qr' ? 'qrcode' : 'barcode',
+ subType: this.data.codeType === 'qr' ? data.type : data.barcodeType,
+ content: data.content,
+ batchMode: data.batchMode,
+ options: {
+ foregroundColor: data.foregroundColor,
+ backgroundColor: data.backgroundColor,
+ size: this.data.codeType === 'qr' ? data.size : data.barcodeWidth
+ }
+ };
+
+ // 调用API服务
+ const response = await apiService.generateCode(params);
+
+ if (data.batchMode) {
+ // 批量生成,处理服务器返回的批量结果
+ this.handleServerBatchResult(response.data);
+ } else {
+ // 单个生成,处理服务器返回的单个结果
+ this.handleServerSingleResult(response.data);
+ }
+ } catch (error) {
+ throw error;
+ }
+ },
+
+ // 处理服务器单个生成结果
+ handleServerSingleResult(data) {
+ // 这里应该根据服务器返回的数据来显示结果
+ // 由于服务器接口未实现,这个方法暂时为空
+ console.log('服务器单个生成结果:', data);
+ },
+
+ // 处理服务器批量生成结果
+ handleServerBatchResult(data) {
+ // 这里应该根据服务器返回的数据来显示批量结果
+ // 由于服务器接口未实现,这个方法暂时为空
+ console.log('服务器批量生成结果:', data);
+ },
+
+ // 处理本地生成成功
+ handleLocalGenerateSuccess() {
+ // 本地生成完成,但不自动保存到相册,让用户自己点击按钮保存
+ console.log('本地生成完成,等待用户操作');
+ MessageUtil.showSuccess((this.data.codeType === 'qr' ? '二维码' : '条形码') + '生成完成');
+ }
+});
\ No newline at end of file
diff --git a/pages/generate-result/generate-result.json b/pages/generate-result/generate-result.json
new file mode 100644
index 0000000000000000000000000000000000000000..8835af0699ccec004cbe685ef938cd2d63ea7037
--- /dev/null
+++ b/pages/generate-result/generate-result.json
@@ -0,0 +1,3 @@
+{
+ "usingComponents": {}
+}
\ No newline at end of file
diff --git a/pages/generate-result/generate-result.wxml b/pages/generate-result/generate-result.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..380a6e28f75b46c371087c8c2825415d725883d2
--- /dev/null
+++ b/pages/generate-result/generate-result.wxml
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{content}}
+ {{typeText}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.content}}
+ 第 {{index + 1}} 个
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{loadingText}}
+
+
\ No newline at end of file
diff --git a/pages/generate-result/generate-result.wxss b/pages/generate-result/generate-result.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..bad1873b93846f30645b162b073799c31bb91dfa
--- /dev/null
+++ b/pages/generate-result/generate-result.wxss
@@ -0,0 +1,262 @@
+/* 码生成结果页面样式(支持二维码和条形码) - iOS风格 */
+.container {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 40rpx 30rpx 30rpx;
+}
+
+/* 顶部导航 */
+.header {
+ text-align: center;
+ margin-bottom: 60rpx;
+}
+
+.title {
+ font-size: 48rpx;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 16rpx;
+}
+
+.subtitle {
+ font-size: 28rpx;
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 单个条形码展示 */
+.single-barcode {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.barcode-card {
+ background: #ffffff;
+ border-radius: 24rpx;
+ padding: 40rpx;
+ margin-bottom: 40rpx;
+ box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1);
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.barcode-container {
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 30rpx;
+ width: 100%;
+ /* min-height: 600rpx; 最小高度,可以根据内容扩展 */
+ margin: 0 auto 30rpx auto; /* 居中显示 */
+}
+
+.barcode-canvas {
+ border: 2rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ background: #ffffff;
+}
+
+.qr-canvas {
+ border: 2rpx solid #f0f0f0;
+ border-radius: 12rpx;
+ background: #ffffff;
+}
+
+.barcode-info {
+ text-align: center;
+}
+
+.barcode-content {
+ font-size: 28rpx;
+ color: #333333;
+ margin-bottom: 12rpx;
+ word-break: break-all;
+ line-height: 1.5;
+}
+
+.barcode-type {
+ font-size: 24rpx;
+ color: #666666;
+}
+
+/* 批量条形码展示 */
+.batch-barcode {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.barcode-list {
+ flex: 1;
+ max-height: 800rpx;
+ margin-bottom: 40rpx;
+}
+
+.barcode-item {
+ background: #ffffff;
+ border-radius: 20rpx;
+ margin-bottom: 20rpx;
+ padding: 30rpx;
+ box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
+}
+
+.barcode-item-container {
+ display: flex;
+ align-items: center;
+}
+
+.barcode-canvas-small {
+ border: 2rpx solid #f0f0f0;
+ border-radius: 8rpx;
+ margin-right: 30rpx;
+ flex-shrink: 0;
+ background: #ffffff;
+}
+
+.qr-canvas-small {
+ border: 2rpx solid #f0f0f0;
+ border-radius: 8rpx;
+ margin-right: 30rpx;
+ flex-shrink: 0;
+ background: #ffffff;
+}
+
+.barcode-item-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.barcode-item-content {
+ font-size: 28rpx;
+ color: #333333;
+ margin-bottom: 8rpx;
+ word-break: break-all;
+ line-height: 1.4;
+}
+
+.barcode-item-index {
+ font-size: 24rpx;
+ color: #666666;
+}
+
+/* 操作按钮 */
+.actions {
+ display: flex;
+ gap: 20rpx;
+ width: 100%;
+ max-width: 600rpx;
+}
+
+.batch-actions {
+ display: flex;
+ gap: 20rpx;
+ margin-bottom: 20rpx;
+}
+
+.bottom-actions {
+ display: flex;
+ justify-content: center;
+ margin-top: 40rpx;
+}
+
+.action-btn {
+ flex: 1;
+ height: 88rpx;
+ border-radius: 44rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ font-weight: 500;
+ border: none;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.action-btn::after {
+ border: none;
+}
+
+.action-btn.primary {
+ background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
+ color: #ffffff;
+ box-shadow: 0 4rpx 16rpx rgba(76, 175, 80, 0.3);
+}
+
+.action-btn.primary:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 8rpx rgba(76, 175, 80, 0.3);
+}
+
+.action-btn.secondary {
+ background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
+ color: #ffffff;
+ box-shadow: 0 4rpx 16rpx rgba(33, 150, 243, 0.3);
+}
+
+.action-btn.secondary:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 8rpx rgba(33, 150, 243, 0.3);
+}
+
+.action-btn.outline {
+ background: rgba(255, 255, 255, 0.2);
+ color: #ffffff;
+ border: 2rpx solid rgba(255, 255, 255, 0.3);
+ backdrop-filter: blur(10rpx);
+ max-width: 300rpx;
+}
+
+.action-btn.outline:active {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.btn-text {
+ font-size: inherit;
+ font-weight: inherit;
+}
+
+/* 加载提示 */
+.loading-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+}
+
+.loading-content {
+ background: #ffffff;
+ border-radius: 20rpx;
+ padding: 60rpx 40rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 300rpx;
+}
+
+.loading-spinner {
+ width: 60rpx;
+ height: 60rpx;
+ border: 4rpx solid #f0f0f0;
+ border-top: 4rpx solid #4CAF50;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin-bottom: 30rpx;
+}
+
+.loading-text {
+ font-size: 28rpx;
+ color: #333333;
+ text-align: center;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
\ No newline at end of file
diff --git a/pages/history/history.js b/pages/history/history.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7240532e89fa95bb73c21c288b6a839f71d0163
--- /dev/null
+++ b/pages/history/history.js
@@ -0,0 +1,784 @@
+// 历史记录页面 - iOS风格
+Page({
+ data: {
+ historyList: [],
+ selectMode: false,
+ selectedItems: [],
+ showDetail: false,
+ currentItem: null,
+ searchValue: '',
+ showSearch: false,
+ currentPage: 1,
+ pageSize: 20,
+ hasMore: true,
+ loading: false
+ },
+
+ onLoad(options) {
+ this.loadHistory();
+
+ // 如果没有历史记录,创建一些测试数据
+ if (options.demo === 'true') {
+ this.createTestData();
+ }
+ },
+
+ onShow() {
+ // 每次显示页面时重新加载历史记录
+ this.loadHistory();
+ },
+
+ // 加载历史记录
+ loadHistory() {
+ try {
+ const historyManager = require('../../utils/history-manager.js');
+ const scanHistory = historyManager.getHistory('scanHistory');
+
+ // 只显示扫描历史记录,不包含生成记录
+ const allHistory = scanHistory.map(item => ({ ...item, source: 'scan' }));
+
+ // 按时间排序(最新的在前面)
+ allHistory.sort((a, b) => {
+ const timeA = a.scanTime || a.timestamp || 0;
+ const timeB = b.scanTime || b.timestamp || 0;
+ return timeB - timeA;
+ });
+
+ const processedHistory = allHistory.map(item => this.processHistoryItem(item));
+
+ this.setData({
+ historyList: processedHistory,
+ selectMode: false,
+ selectedItems: [],
+ hasMore: false // 暂时不支持分页
+ });
+ } catch (error) {
+ console.error('加载历史记录失败:', error);
+ wx.showToast({
+ title: '加载失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 处理历史记录项
+ processHistoryItem(item) {
+ const now = new Date();
+ const recordTime = new Date(item.scanTime || item.timestamp || item.time);
+ const timeDiff = now - recordTime;
+
+ // 格式化时间显示
+ let timeText = '';
+ let fullTime = this.formatFullTime(recordTime);
+
+ if (timeDiff < 60000) { // 1分钟内
+ timeText = '刚刚';
+ } else if (timeDiff < 3600000) { // 1小时内
+ timeText = Math.floor(timeDiff / 60000) + '分钟前';
+ } else if (timeDiff < 86400000) { // 24小时内
+ timeText = Math.floor(timeDiff / 3600000) + '小时前';
+ } else if (timeDiff < 604800000) { // 7天内
+ timeText = Math.floor(timeDiff / 86400000) + '天前';
+ } else {
+ timeText = this.formatDate(recordTime);
+ }
+
+ // 识别内容类型
+ const contentType = this.identifyContentType(item.content);
+
+ // 生成预览文本
+ const preview = this.generatePreview(item.content, contentType);
+
+ // 生成额外信息
+ const extra = this.generateExtraInfo(item.content, contentType);
+
+ return {
+ ...item,
+ timeText,
+ fullTime,
+ type: contentType.type,
+ typeText: contentType.text,
+ sourceText: item.source === 'generate' ? '生成' : '扫描',
+ preview,
+ extra,
+ selected: false
+ };
+ },
+
+ // 识别内容类型
+ identifyContentType(content) {
+ // URL检测
+ if (/^https?:\/\/.+/i.test(content)) {
+ return { type: 'url', text: '网址' };
+ }
+
+ // WiFi信息检测
+ if (/^WIFI:/i.test(content)) {
+ return { type: 'wifi', text: 'WiFi' };
+ }
+
+ // 电话号码检测
+ if (/^tel:/i.test(content) || /^(\+?86)?1[3-9]\d{9}$/.test(content)) {
+ return { type: 'phone', text: '电话' };
+ }
+
+ // 联系人信息检测
+ if (/^BEGIN:VCARD/i.test(content)) {
+ return { type: 'contact', text: '联系人' };
+ }
+
+ // 邮箱检测
+ if (/^mailto:/i.test(content) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
+ return { type: 'email', text: '邮箱' };
+ }
+
+ // 地理位置检测
+ if (/^geo:/i.test(content)) {
+ return { type: 'location', text: '位置' };
+ }
+
+ // 默认为文本
+ return { type: 'text', text: '文本' };
+ },
+
+ // 生成预览文本
+ generatePreview(content, contentType) {
+ const maxLength = 100;
+
+ switch (contentType.type) {
+ case 'wifi':
+ const wifiMatch = content.match(/S:([^;]*)/);
+ return wifiMatch ? `WiFi: ${wifiMatch[1]}` : content.substring(0, maxLength);
+
+ case 'url':
+ try {
+ const url = new URL(content);
+ return url.hostname + url.pathname;
+ } catch {
+ return content.substring(0, maxLength);
+ }
+
+ case 'phone':
+ return content.replace(/^tel:/, '');
+
+ case 'email':
+ return content.replace(/^mailto:/, '');
+
+ default:
+ return content.length > maxLength ?
+ content.substring(0, maxLength) + '...' : content;
+ }
+ },
+
+ // 生成额外信息
+ generateExtraInfo(content, contentType) {
+ switch (contentType.type) {
+ case 'wifi':
+ const passwordMatch = content.match(/P:([^;]*)/);
+ return passwordMatch && passwordMatch[1] ? '需要密码' : '开放网络';
+
+ case 'url':
+ try {
+ const url = new URL(content);
+ return url.protocol === 'https:' ? '安全连接' : '普通连接';
+ } catch {
+ return null;
+ }
+
+ case 'contact':
+ const nameMatch = content.match(/FN:([^\r\n]*)/);
+ return nameMatch ? `联系人: ${nameMatch[1]}` : null;
+
+ default:
+ return null;
+ }
+ },
+
+ // 格式化完整时间
+ formatFullTime(date) {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hour = String(date.getHours()).padStart(2, '0');
+ const minute = String(date.getMinutes()).padStart(2, '0');
+ const second = String(date.getSeconds()).padStart(2, '0');
+
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
+ },
+
+ // 格式化日期
+ formatDate(date) {
+ const month = date.getMonth() + 1;
+ const day = date.getDate();
+ return `${month}月${day}日`;
+ },
+
+ // 切换选择模式
+ toggleSelectMode() {
+ this.setData({
+ selectMode: !this.data.selectMode,
+ selectedItems: []
+ });
+ },
+
+ // 切换选择项
+ toggleSelect(e) {
+ const { item } = e.currentTarget.dataset;
+ const selectedItems = [...this.data.selectedItems];
+ const index = selectedItems.indexOf(item.id);
+
+ if (index >= 0) {
+ selectedItems.splice(index, 1);
+ } else {
+ selectedItems.push(item.id);
+ }
+
+ this.setData({ selectedItems });
+ },
+
+ // 查看详情
+ viewDetail(e) {
+ const { item } = e.currentTarget.dataset;
+ this.setData({
+ currentItem: item,
+ showDetail: true
+ });
+ },
+
+ // 隐藏详情
+ hideDetail() {
+ this.setData({
+ showDetail: false,
+ currentItem: null
+ });
+ },
+
+ // 阻止事件冒泡
+ stopPropagation() {
+ // 空函数,用于阻止事件冒泡
+ },
+
+ // 复制内容
+ copyContent(e) {
+ // 阻止事件冒泡
+ e.stopPropagation && e.stopPropagation();
+
+ const content = e.currentTarget.dataset.content;
+
+ wx.setClipboardData({
+ data: content,
+ success: () => {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+
+ // 如果是在弹窗中,关闭弹窗
+ if (this.data.showDetail) {
+ this.hideDetail();
+ }
+ },
+ fail: () => {
+ wx.showToast({
+ title: '复制失败',
+ icon: 'none'
+ });
+ }
+ });
+ },
+
+ // 分享内容
+ shareContent(e) {
+ // 阻止事件冒泡
+ e.stopPropagation && e.stopPropagation();
+
+ const item = e.currentTarget.dataset.item;
+
+ wx.showActionSheet({
+ itemList: ['发送给朋友', '分享到朋友圈', '复制链接'],
+ success: (res) => {
+ switch (res.tapIndex) {
+ case 0:
+ // 发送给朋友的逻辑
+ wx.showToast({
+ title: '功能开发中',
+ icon: 'none'
+ });
+ break;
+ case 1:
+ // 分享到朋友圈的逻辑
+ wx.showToast({
+ title: '功能开发中',
+ icon: 'none'
+ });
+ break;
+ case 2:
+ // 复制链接
+ this.copyContent({ currentTarget: { dataset: { content: item.content } } });
+ break;
+ }
+
+ // 如果是在弹窗中,关闭弹窗
+ if (this.data.showDetail) {
+ this.hideDetail();
+ }
+ }
+ });
+ },
+
+ // 删除选中项
+ deleteSelected() {
+ if (this.data.selectedItems.length === 0) return;
+
+ wx.showModal({
+ title: '确认删除',
+ content: `确定要删除选中的 ${this.data.selectedItems.length} 条记录吗?`,
+ confirmText: '删除',
+ confirmColor: '#dc2626',
+ success: (res) => {
+ if (res.confirm) {
+ this.performDelete(this.data.selectedItems);
+ }
+ }
+ });
+ },
+
+ // 清空所有历史
+ clearAllHistory() {
+ wx.showModal({
+ title: '确认清空',
+ content: '确定要清空所有历史记录吗?此操作不可恢复。',
+ confirmText: '清空',
+ confirmColor: '#dc2626',
+ success: (res) => {
+ if (res.confirm) {
+ this.performClearAll();
+ }
+ }
+ });
+ },
+
+ // 执行删除
+ performDelete(idsToDelete) {
+ try {
+ const historyManager = require('../../utils/history-manager.js');
+ const success = historyManager.deleteHistoryItems('scanHistory', idsToDelete);
+
+ if (success) {
+ wx.showToast({
+ title: '删除成功',
+ icon: 'success'
+ });
+ // 重新加载数据
+ this.loadHistory();
+ } else {
+ wx.showToast({
+ title: '删除失败',
+ icon: 'none'
+ });
+ }
+ } catch (error) {
+ console.error('删除失败:', error);
+ wx.showToast({
+ title: '删除失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 执行清空
+ performClearAll() {
+ try {
+ const historyManager = require('../../utils/history-manager.js');
+ const success = historyManager.clearHistory('scanHistory');
+
+ if (success) {
+ wx.showToast({
+ title: '清空成功',
+ icon: 'success'
+ });
+ // 重新加载数据
+ this.loadHistory();
+ } else {
+ wx.showToast({
+ title: '清空失败',
+ icon: 'none'
+ });
+ }
+ } catch (error) {
+ console.error('清空失败:', error);
+ wx.showToast({
+ title: '清空失败',
+ icon: 'none'
+ });
+ }
+ },
+
+ // 跳转到扫码页面
+ goToScan() {
+ // 直接跳转到首页并触发扫码
+ wx.reLaunch({
+ url: '/pages/index/index?autoScan=true'
+ });
+ },
+
+ // 创建测试数据
+ createTestData() {
+ const testData = [
+ {
+ id: '1',
+ content: 'https://www.baidu.com',
+ scanTime: Date.now() - 60000 // 1分钟前
+ },
+ {
+ id: '2',
+ content: 'WIFI:T:WPA;S:MyWiFi;P:password123;H:false;;',
+ scanTime: Date.now() - 3600000 // 1小时前
+ },
+ {
+ id: '3',
+ content: '13812345678',
+ scanTime: Date.now() - 86400000 // 1天前
+ },
+ {
+ id: '4',
+ content: 'BEGIN:VCARD\nVERSION:3.0\nFN:张三\nTEL:13800138000\nEND:VCARD',
+ scanTime: Date.now() - 172800000 // 2天前
+ },
+ {
+ id: '5',
+ content: '这是一段普通的文本内容,用于测试文本类型的二维码识别功能。',
+ scanTime: Date.now() - 259200000 // 3天前
+ }
+ ];
+
+ try {
+ wx.setStorageSync('scanHistory', testData);
+ this.loadHistory();
+ wx.showToast({
+ title: '测试数据已创建',
+ icon: 'success'
+ });
+ } catch (error) {
+ console.error('创建测试数据失败:', error);
+ }
+ },
+
+ // 搜索功能
+ onSearchInput: function(e) {
+ var value = e.detail.value;
+ this.setData({
+ searchValue: value
+ });
+
+ // 实时搜索
+ this.performSearch(value);
+ },
+
+ // 执行搜索
+ performSearch: function(keyword) {
+ if (!keyword.trim()) {
+ this.loadHistory();
+ return;
+ }
+
+ try {
+ var scanHistory = wx.getStorageSync('scanHistory') || [];
+
+ // 过滤匹配的记录
+ var filteredHistory = scanHistory.filter(function(item) {
+ var content = item.content || '';
+ return content.toLowerCase().includes(keyword.toLowerCase());
+ });
+
+ // 合并并处理
+ var allHistory = filteredHistory.map(function(item) {
+ return Object.assign({}, item, { source: 'scan' });
+ });
+
+ // 按时间排序
+ allHistory.sort(function(a, b) {
+ return (b.scanTime || b.timestamp || 0) - (a.scanTime || a.timestamp || 0);
+ });
+
+ // 处理每个项目
+ var processedHistory = allHistory.map(this.processHistoryItem.bind(this));
+
+ this.setData({
+ historyList: processedHistory
+ });
+
+ } catch (error) {
+ console.error('搜索失败:', error);
+ }
+ },
+
+ // 清空搜索
+ clearSearch: function() {
+ this.setData({
+ searchValue: '',
+ showSearch: false
+ });
+ this.loadHistory();
+ },
+
+ // 显示搜索框
+ showSearchInput: function() {
+ this.setData({
+ showSearch: true
+ });
+ },
+
+ // 隐藏搜索框
+ hideSearchInput: function() {
+ this.setData({
+ showSearch: false,
+ searchValue: ''
+ });
+ this.loadHistory();
+ },
+
+ // 点击历史记录项目
+ onItemTap: function(e) {
+ var item = e.currentTarget.dataset.item;
+
+ if (this.data.selectMode) {
+ // 选择模式下切换选中状态
+ this.toggleItemSelection(item);
+ } else {
+ // 正常模式下处理内容
+ this.processItemContent(item.content);
+ }
+ },
+
+ // 处理项目内容
+ processItemContent: function(content) {
+ // URL处理
+ if (/^https?:\/\/.+/i.test(content)) {
+ wx.showModal({
+ title: '发现网址',
+ content: content,
+ confirmText: '复制',
+ showCancel: false,
+ success: function(res) {
+ if (res.confirm) {
+ // 复制到剪贴板
+ wx.setClipboardData({
+ data: content,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // WiFi信息处理
+ if (/^WIFI:/i.test(content)) {
+ var wifiMatch = content.match(/WIFI:T:([^;]*);S:([^;]*);P:([^;]*);H:([^;]*);?/);
+ if (wifiMatch) {
+ var wifiInfo = '网络名称: ' + wifiMatch[2] + '\n' +
+ '密码: ' + wifiMatch[3] + '\n' +
+ '加密类型: ' + wifiMatch[1];
+
+ wx.showModal({
+ title: 'WiFi信息',
+ content: wifiInfo,
+ confirmText: '复制密码',
+ cancelText: '知道了',
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: wifiMatch[3],
+ success: function() {
+ wx.showToast({
+ title: '密码已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+ }
+
+ // 电话号码处理
+ if (/^tel:/i.test(content) || /^(\+?86)?1[3-9]\d{9}$/.test(content)) {
+ var phoneNumber = content.replace(/^tel:/, '');
+ wx.showModal({
+ title: '电话号码',
+ content: phoneNumber,
+ confirmText: '拨打',
+ cancelText: '复制',
+ success: function(res) {
+ if (res.confirm) {
+ wx.makePhoneCall({
+ phoneNumber: phoneNumber,
+ fail: function() {
+ wx.showToast({
+ title: '拨打失败',
+ icon: 'none'
+ });
+ }
+ });
+ } else if (res.cancel) {
+ wx.setClipboardData({
+ data: phoneNumber,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // 邮箱处理
+ if (/^mailto:/i.test(content) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
+ var email = content.replace(/^mailto:/, '');
+ wx.showModal({
+ title: '邮箱地址',
+ content: email,
+ confirmText: '复制',
+ showCancel: false,
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: email,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // 普通文本
+ wx.showModal({
+ title: '内容详情',
+ content: content,
+ confirmText: '复制',
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: content,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ },
+
+ // 进入选择模式
+ enterSelectMode: function() {
+ this.setData({
+ selectMode: true,
+ selectedItems: []
+ });
+ },
+
+ // 退出选择模式
+ exitSelectMode: function() {
+ this.setData({
+ selectMode: false,
+ selectedItems: []
+ });
+
+ // 重置所有项目的选中状态
+ var historyList = this.data.historyList.map(function(item) {
+ return Object.assign({}, item, { selected: false });
+ });
+
+ this.setData({
+ historyList: historyList
+ });
+ },
+
+ // 切换项目选中状态
+ toggleItemSelection: function(targetItem) {
+ var historyList = this.data.historyList;
+ var selectedItems = this.data.selectedItems.slice(); // 复制数组
+
+ // 找到目标项目并切换选中状态
+ for (var i = 0; i < historyList.length; i++) {
+ if (historyList[i].id === targetItem.id) {
+ historyList[i].selected = !historyList[i].selected;
+
+ if (historyList[i].selected) {
+ // 添加到选中列表
+ selectedItems.push(historyList[i]);
+ } else {
+ // 从选中列表移除
+ selectedItems = selectedItems.filter(function(item) {
+ return item.id !== targetItem.id;
+ });
+ }
+ break;
+ }
+ }
+
+ this.setData({
+ historyList: historyList,
+ selectedItems: selectedItems
+ });
+ },
+
+ // 全选
+ selectAll: function() {
+ var historyList = this.data.historyList.map(function(item) {
+ return Object.assign({}, item, { selected: true });
+ });
+
+ this.setData({
+ historyList: historyList,
+ selectedItems: historyList.slice()
+ });
+ },
+
+ // 取消全选
+ deselectAll: function() {
+ var historyList = this.data.historyList.map(function(item) {
+ return Object.assign({}, item, { selected: false });
+ });
+
+ this.setData({
+ historyList: historyList,
+ selectedItems: []
+ });
+ },
+
+ // 分享功能
+ onShareAppMessage() {
+ return {
+ title: '小籽二维码 - 扫码生成一站式服务',
+ path: '/pages/index/index'
+ };
+ }
+});
+
+// 工具函数:保存扫描记录
+export function saveScanRecord(content) {
+ const historyManager = require('../../utils/history-manager.js');
+ return historyManager.saveScanRecord(content);
+}
\ No newline at end of file
diff --git a/pages/history/history.json b/pages/history/history.json
new file mode 100644
index 0000000000000000000000000000000000000000..03e9159a42f719fa69092dac7e0af6ef58adee26
--- /dev/null
+++ b/pages/history/history.json
@@ -0,0 +1,5 @@
+{
+ "navigationBarTitleText": "扫描记录",
+ "backgroundTextStyle": "dark",
+ "backgroundColor": "#f8f9fa"
+}
\ No newline at end of file
diff --git a/pages/history/history.wxml b/pages/history/history.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..8fb254c4a9959dba4eb5a8b7096e0ce4d3a8f797
--- /dev/null
+++ b/pages/history/history.wxml
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+ 共 {{historyList.length}} 条记录
+
+
+
+ {{selectMode ? '取消' : '选择'}}
+
+
+ 清空
+
+
+ 删除({{selectedItems.length}})
+
+
+
+
+
+
+
+
+
+
+ ✓
+
+
+
+
+
+
+
+ {{item.preview}}
+
+
+
+
+
+
+
+
+
+
+
+
+ 暂无扫描记录
+ 扫描二维码后,记录会显示在这里
+
+ 立即扫码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 类型
+ {{currentItem.typeText}}
+
+
+ 时间
+ {{currentItem.fullTime}}
+
+
+ 内容
+
+ {{currentItem.content}}
+
+
+
+
+
+
+ 复制
+
+
+ 分享
+
+
+
+
\ No newline at end of file
diff --git a/pages/history/history.wxss b/pages/history/history.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..28272aa3f1cc876d11c162f67f2b01be66bf2763
--- /dev/null
+++ b/pages/history/history.wxss
@@ -0,0 +1,477 @@
+/* 历史记录页面样式 - iOS风格 */
+
+/* 主容器 */
+.container {
+ min-height: 100vh;
+ background: linear-gradient(180deg,
+ #f0f9ff 0%,
+ #f8fafc 50%,
+ #f1f5f9 100%);
+ padding: 0 32rpx;
+}
+
+/* 标题区域 */
+.header-section {
+ padding: 40rpx 0;
+ text-align: center;
+}
+
+.page-title {
+ font-size: 48rpx;
+ font-weight: 700;
+ color: #1e293b;
+ line-height: 1.2;
+ margin-bottom: 16rpx;
+}
+
+.page-subtitle {
+ font-size: 26rpx;
+ color: #64748b;
+ line-height: 1.5;
+}
+
+/* 操作栏 */
+.action-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24rpx 32rpx;
+ background: white;
+ border-radius: 20rpx;
+ margin-bottom: 24rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+}
+
+.record-count text {
+ font-size: 26rpx;
+ color: #64748b;
+ font-weight: 500;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 16rpx;
+}
+
+.action-btn {
+ padding: 12rpx 24rpx;
+ border-radius: 12rpx;
+ background: #f1f5f9;
+ border: 1rpx solid #e2e8f0;
+ transition: all 0.3s ease;
+}
+
+.action-btn text {
+ font-size: 24rpx;
+ color: #475569;
+ font-weight: 500;
+}
+
+.action-btn.danger {
+ background: #fef2f2;
+ border-color: #fecaca;
+}
+
+.action-btn.danger text {
+ color: #dc2626;
+}
+
+.action-btn:active {
+ transform: scale(0.95);
+}
+
+/* 历史记录列表 */
+.history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.history-item {
+ background: white;
+ border-radius: 20rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: flex-start;
+ gap: 20rpx;
+}
+
+.history-item:active {
+ transform: scale(0.98);
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.08);
+}
+
+.history-item.select-mode {
+ padding-left: 20rpx;
+}
+
+/* 选择框 */
+.select-checkbox {
+ width: 40rpx;
+ height: 40rpx;
+ border: 2rpx solid #d1d5db;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 8rpx;
+ transition: all 0.3s ease;
+}
+
+.select-checkbox.checked {
+ background: #22c55e;
+ border-color: #22c55e;
+}
+
+.check-icon {
+ font-size: 20rpx;
+ color: white;
+ font-weight: bold;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.select-checkbox.checked .check-icon {
+ opacity: 1;
+}
+
+/* 记录内容 */
+.item-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.content-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 16rpx;
+}
+
+.content-type {
+ display: flex;
+ align-items: center;
+ gap: 16rpx;
+}
+
+.type-tag {
+ display: inline-block;
+ padding: 6rpx 16rpx;
+ border-radius: 12rpx;
+ font-size: 20rpx;
+ font-weight: 600;
+ color: white;
+ background: #6b7280;
+}
+
+.type-tag.url {
+ background: #3b82f6;
+}
+
+.type-tag.text {
+ background: #22c55e;
+}
+
+.type-tag.wifi {
+ background: #8b5cf6;
+}
+
+.type-tag.contact {
+ background: #f59e0b;
+}
+
+.type-tag.phone {
+ background: #ef4444;
+}
+
+.source-tag {
+ display: inline-block;
+ padding: 4rpx 12rpx;
+ border-radius: 8rpx;
+ font-size: 18rpx;
+ font-weight: 500;
+ color: white;
+ background: #64748b;
+}
+
+.source-tag.scan {
+ background: #06b6d4;
+}
+
+.source-tag.generate {
+ background: #f59e0b;
+}
+
+.scan-time {
+ font-size: 22rpx;
+ color: #94a3b8;
+}
+
+.content-actions {
+ display: flex;
+ gap: 12rpx;
+}
+
+.action-btn-text {
+ padding: 0 12rpx;
+ transition: all 0.3s ease;
+}
+
+.action-btn-text:active {
+ opacity: 0.6;
+}
+
+.action-btn-text text {
+ font-size: 26rpx;
+ color: #3b82f6;
+ font-weight: 500;
+ text-decoration: underline;
+}
+
+/* 内容文本 */
+.content-text {
+ margin-bottom: 12rpx;
+}
+
+.content-preview {
+ font-size: 28rpx;
+ color: #1e293b;
+ line-height: 1.5;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ overflow: hidden;
+ word-break: break-all;
+}
+
+.content-footer {
+ margin-top: 12rpx;
+}
+
+.extra-info {
+ font-size: 24rpx;
+ color: #64748b;
+ background: #f1f5f9;
+ padding: 8rpx 16rpx;
+ border-radius: 8rpx;
+ display: inline-block;
+}
+
+/* 空状态 */
+.empty-state {
+ text-align: center;
+ padding: 120rpx 40rpx;
+}
+
+.empty-icon {
+ margin-bottom: 32rpx;
+ opacity: 0.6;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.empty-icon-img {
+ width: 120rpx;
+ height: 120rpx;
+}
+
+.empty-title {
+ font-size: 36rpx;
+ font-weight: 600;
+ color: #1e293b;
+ display: block;
+ margin-bottom: 16rpx;
+}
+
+.empty-desc {
+ font-size: 26rpx;
+ color: #64748b;
+ line-height: 1.5;
+ display: block;
+ margin-bottom: 48rpx;
+}
+
+.empty-action {
+ display: inline-block;
+ padding: 20rpx 40rpx;
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ border-radius: 20rpx;
+ box-shadow: 0 4rpx 20rpx rgba(34, 197, 94, 0.3);
+ transition: all 0.3s ease;
+}
+
+.empty-action:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 12rpx rgba(34, 197, 94, 0.25);
+}
+
+.empty-action text {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: white;
+}
+
+/* 详情弹窗 */
+.detail-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.detail-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-content {
+ width: 90%;
+ max-width: 600rpx;
+ background: white;
+ border-radius: 24rpx;
+ overflow: hidden;
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+
+.detail-modal.show .modal-content {
+ transform: scale(1);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32rpx;
+ border-bottom: 1rpx solid #f1f5f9;
+}
+
+.modal-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+.modal-close {
+ width: 48rpx;
+ height: 48rpx;
+ border-radius: 50%;
+ background: #f8fafc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+}
+
+.modal-close:active {
+ transform: scale(0.9);
+ background: #e2e8f0;
+}
+
+.modal-close text {
+ font-size: 24rpx;
+ color: #64748b;
+}
+
+.modal-body {
+ padding: 32rpx;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.detail-item {
+ display: flex;
+ margin-bottom: 24rpx;
+}
+
+.detail-item.full {
+ flex-direction: column;
+}
+
+.detail-label {
+ font-size: 26rpx;
+ color: #64748b;
+ font-weight: 500;
+ width: 120rpx;
+ flex-shrink: 0;
+}
+
+.detail-value {
+ font-size: 26rpx;
+ color: #1e293b;
+ flex: 1;
+}
+
+.detail-content {
+ margin-top: 12rpx;
+ padding: 20rpx;
+ background: #f8fafc;
+ border-radius: 12rpx;
+ border: 1rpx solid #e2e8f0;
+}
+
+.detail-text {
+ font-size: 26rpx;
+ color: #1e293b;
+ line-height: 1.6;
+ word-break: break-all;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 16rpx;
+ padding: 32rpx;
+ border-top: 1rpx solid #f1f5f9;
+}
+
+.modal-btn {
+ flex: 1;
+ padding: 24rpx;
+ border-radius: 16rpx;
+ text-align: center;
+ transition: all 0.3s ease;
+}
+
+.modal-btn.secondary {
+ background: #f8fafc;
+ border: 1rpx solid #e2e8f0;
+}
+
+.modal-btn.secondary text {
+ font-size: 28rpx;
+ font-weight: 500;
+ color: #475569;
+}
+
+.modal-btn.primary {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+}
+
+.modal-btn.primary text {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: white;
+}
+
+.modal-btn:active {
+ transform: scale(0.95);
+}
+
+/* 底部安全区域 */
+.bottom-safe-area {
+ height: 80rpx;
+}
\ No newline at end of file
diff --git a/pages/index/index.js b/pages/index/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..24d0f1aa08e90562fca05ab727fa9bb6a076410a
--- /dev/null
+++ b/pages/index/index.js
@@ -0,0 +1,903 @@
+// 二维码工具箱首页 - iOS风格
+Page({
+ data: {
+ // 扫码动画状态
+ scanAnimation: false,
+
+ // 广告显示状态
+ showAd: true,
+
+ // 历史记录数量统计
+ totalHistory: 0,
+
+ // 最近使用项目(前6个)
+ recentItems: [],
+
+ // 最近使用详情弹窗
+ showRecentDetail: false,
+ currentRecentItem: null,
+
+ // 扫码模式设置
+ scanMode: 'custom', // 'custom' 或 'wechat'
+ showModeSelector: false,
+
+ // 离线模式状态
+ isOfflineMode: false,
+
+ // 初始化状态
+ isInitializing: true,
+ initializationError: null,
+ showOfflineBanner: false
+ },
+
+ onLoad: function(options) {
+ console.log('首页加载');
+
+ // 检查应用初始化状态
+ this.checkAppInitialization();
+
+ this.loadScanMode();
+
+ // 如果应用还在初始化,则不加载数据,等待初始化完成
+ if (!this.data.isInitializing) {
+ this.loadData();
+ this.checkOfflineMode();
+ }
+
+ // 检查是否需要自动扫码
+ if (options.autoScan === 'true') {
+ // 延迟一点时间确保页面完全加载
+ var self = this;
+ setTimeout(function() {
+ if (!self.data.isInitializing) {
+ self.startScan();
+ }
+ }, 500);
+ }
+ },
+
+ onShow: function() {
+ console.log('首页显示');
+
+ // 检查应用初始化状态
+ this.checkAppInitialization();
+
+ // 如果应用还在初始化,则不刷新数据
+ if (!this.data.isInitializing) {
+ // 页面显示时刷新数据
+ this.loadData();
+ this.checkOfflineMode();
+ }
+ },
+
+ // 检查应用初始化状态
+ checkAppInitialization: function() {
+ try {
+ const app = getApp();
+ const isInitializing = app.isInitializing();
+ const initError = app.getInitializationError();
+
+ this.setData({
+ isInitializing: isInitializing,
+ initializationError: initError
+ });
+
+ console.log('应用初始化状态:', { isInitializing, initError });
+ } catch (error) {
+ console.error('检查初始化状态失败:', error);
+ // 如果无法获取状态,假设初始化已完成
+ this.setData({
+ isInitializing: false,
+ initializationError: null
+ });
+ }
+ },
+
+ // 应用初始化完成回调
+ onAppInitComplete: function(success) {
+ console.log('收到应用初始化完成通知,成功:', success);
+
+ this.setData({
+ isInitializing: false
+ });
+
+ if (success) {
+ // 初始化成功,加载数据
+ this.loadData();
+ this.checkOfflineMode();
+ } else {
+ // 初始化失败,显示离线模式
+ this.handleInitializationFailure();
+ }
+ },
+
+ // 处理初始化失败
+ handleInitializationFailure: function() {
+ console.log('处理初始化失败,进入离线模式');
+
+ // 延迟显示离线横幅,给用户更好的体验
+ setTimeout(() => {
+ this.setData({
+ showOfflineBanner: true
+ });
+ }, 500);
+
+ // 仍然加载本地数据
+ this.loadData();
+ this.checkOfflineMode();
+ },
+
+ // 加载所有数据
+ loadData: function() {
+ try {
+ this.loadHistoryStats();
+ this.loadRecentItems();
+ } catch (error) {
+ console.error('加载数据失败:', error);
+ }
+ },
+
+ // 加载扫码模式设置
+ loadScanMode: function() {
+ try {
+ var savedMode = wx.getStorageSync('scanMode');
+ if (savedMode === 'custom' || savedMode === 'wechat') {
+ this.setData({
+ scanMode: savedMode
+ });
+ console.log('加载扫码模式:', savedMode);
+ } else {
+ // 默认使用自定义模式
+ this.setData({
+ scanMode: 'custom'
+ });
+ console.log('使用默认扫码模式: custom');
+ }
+ } catch (error) {
+ console.error('加载扫码模式失败:', error);
+ this.setData({
+ scanMode: 'custom'
+ });
+ }
+ },
+
+ // 检查离线模式状态
+ checkOfflineMode: function() {
+ try {
+ const app = getApp();
+ const isOffline = app.isOfflineMode();
+
+ this.setData({
+ isOfflineMode: isOffline
+ });
+
+ console.log('当前离线模式状态:', isOffline);
+ } catch (error) {
+ console.error('检查离线模式失败:', error);
+ this.setData({
+ isOfflineMode: false
+ });
+ }
+ },
+
+ // 切换离线模式(测试功能)
+ toggleOfflineMode: function(e) {
+ const isOffline = e.detail.value;
+ console.log('手动切换离线模式:', isOffline);
+
+ try {
+ const app = getApp();
+
+ // 更新全局状态
+ app.globalData.isOfflineMode = isOffline;
+
+ // 更新本地存储
+ if (isOffline) {
+ wx.setStorageSync('app_offline_mode', true);
+ wx.setStorageSync('test_offline_mode', true); // 标记为测试模式
+ } else {
+ wx.removeStorageSync('app_offline_mode');
+ wx.removeStorageSync('test_offline_mode');
+ }
+
+ // 更新页面状态
+ this.setData({
+ isOfflineMode: isOffline
+ });
+
+ // 显示状态提示
+ if (isOffline) {
+ wx.showToast({
+ title: '已切换为离线模式',
+ icon: 'none',
+ duration: 1500
+ });
+ } else {
+ wx.showToast({
+ title: '已切换为在线模式',
+ icon: 'success',
+ duration: 1500
+ });
+ }
+
+ } catch (error) {
+ console.error('切换离线模式失败:', error);
+ wx.showToast({
+ title: '切换失败',
+ icon: 'error'
+ });
+ }
+ },
+
+ // 加载历史记录统计
+ loadHistoryStats: function() {
+ const historyManager = require('../../utils/history-manager.js');
+ const stats = historyManager.getHistoryStats();
+ this.setData({
+ totalHistory: stats.scanCount
+ });
+ },
+
+ // 加载最近使用项目
+ loadRecentItems: function() {
+ try {
+ var scanHistory = wx.getStorageSync('scanHistory') || [];
+ var self = this;
+
+ // 只显示扫描历史,按时间排序
+ var allItems = scanHistory
+ .sort(function(a, b) {
+ return (b.scanTime || b.timestamp || 0) - (a.scanTime || a.timestamp || 0);
+ })
+ .slice(0, 6);
+
+ this.setData({
+ recentItems: allItems.map(function(item) {
+ return self.processRecentItem(item);
+ })
+ });
+ } catch (error) {
+ console.error('加载最近使用项目失败:', error);
+ this.setData({ recentItems: [] });
+ }
+ },
+
+ // 处理最近使用项目
+ processRecentItem: function(item) {
+ var content = item.content || '';
+ var isGenerated = !!item.type; // 生成的记录有type字段
+
+ // 识别内容类型
+ var contentType = this.identifyContentType(content);
+
+ // 生成标题
+ var title = this.generateItemTitle(content, contentType);
+
+ // 生成时间显示
+ var time = this.formatItemTime(item.scanTime || item.timestamp);
+
+ // 使用Object.assign替代扩展运算符
+ return Object.assign({}, item, {
+ type: contentType.type,
+ typeText: contentType.text,
+ title: title,
+ time: time,
+ isGenerated: isGenerated
+ });
+ },
+
+ // 识别内容类型
+ identifyContentType: function(content) {
+ // URL检测
+ if (/^https?:\/\/.+/i.test(content)) {
+ return { type: 'url', text: '网址' };
+ }
+
+ // WiFi信息检测
+ if (/^WIFI:/i.test(content)) {
+ return { type: 'wifi', text: 'WiFi' };
+ }
+
+ // 电话号码检测
+ if (/^tel:/i.test(content) || /^(\+?86)?1[3-9]\d{9}$/.test(content)) {
+ return { type: 'phone', text: '电话' };
+ }
+
+ // 联系人信息检测
+ if (/^BEGIN:VCARD/i.test(content)) {
+ return { type: 'contact', text: '联系人' };
+ }
+
+ // 邮箱检测
+ if (/^mailto:/i.test(content) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
+ return { type: 'email', text: '邮箱' };
+ }
+
+ // 地理位置检测
+ if (/^geo:/i.test(content)) {
+ return { type: 'location', text: '位置' };
+ }
+
+ // 默认为文本
+ return { type: 'text', text: '文本' };
+ },
+
+ // 生成项目标题
+ generateItemTitle: function(content, contentType) {
+ var maxLength = 12;
+
+ switch (contentType.type) {
+ case 'wifi':
+ var wifiMatch = content.match(/S:([^;]*)/);
+ return wifiMatch ? wifiMatch[1] : '未知网络';
+
+ case 'url':
+ try {
+ var url = new URL(content);
+ return url.hostname;
+ } catch (e) {
+ return content.substring(0, maxLength);
+ }
+
+ case 'phone':
+ return content.replace(/^tel:/, '');
+
+ case 'email':
+ return content.replace(/^mailto:/, '');
+
+ case 'contact':
+ var nameMatch = content.match(/FN:([^\r\n]*)/);
+ return nameMatch ? nameMatch[1] : '联系人';
+
+ default:
+ return content.length > maxLength ?
+ content.substring(0, maxLength) + '...' : content;
+ }
+ },
+
+ // 格式化项目时间
+ formatItemTime: function(timestamp) {
+ if (!timestamp) return '未知时间';
+
+ var now = new Date();
+ var time = new Date(timestamp);
+ var timeDiff = now - time;
+
+ if (timeDiff < 60000) { // 1分钟内
+ return '刚刚';
+ } else if (timeDiff < 3600000) { // 1小时内
+ return Math.floor(timeDiff / 60000) + '分钟前';
+ } else if (timeDiff < 86400000) { // 24小时内
+ return Math.floor(timeDiff / 3600000) + '小时前';
+ } else if (timeDiff < 604800000) { // 7天内
+ return Math.floor(timeDiff / 86400000) + '天前';
+ } else {
+ var month = time.getMonth() + 1;
+ var day = time.getDate();
+ return month + '月' + day + '日';
+ }
+ },
+
+ // 开始扫码 - 根据模式选择
+ startScan: function() {
+ // 检查是否可以操作
+ if (!this.checkCanOperate()) return;
+
+ console.log('开始扫码 - 模式:', this.data.scanMode, '离线模式:', this.data.isOfflineMode);
+
+ // 设置扫码动画
+ this.setData({
+ scanAnimation: true
+ });
+
+ // 离线模式下强制使用微信API扫码
+ if (this.data.isOfflineMode) {
+ console.log('离线模式,使用微信API扫码');
+ this.startWechatScan();
+ return;
+ }
+
+ // 根据当前模式选择扫码方式
+ if (this.data.scanMode === 'custom') {
+ this.startCustomScan();
+ } else {
+ this.startWechatScan();
+ }
+ },
+
+ // 自定义摄像头扫码
+ startCustomScan: function() {
+ console.log('启动自定义摄像头扫码');
+
+ var self = this;
+
+ // 跳转到自定义扫码页面
+ wx.navigateTo({
+ url: '/pages/custom-scan/custom-scan',
+ success: function() {
+ console.log('跳转到自定义扫码页面成功');
+ },
+ fail: function(err) {
+ console.error('跳转到自定义扫码页面失败:', err);
+ // 如果自定义扫码页面跳转失败,回退到微信API
+ self.startWechatScan();
+ },
+ complete: function() {
+ // 停止扫码动画
+ setTimeout(() => {
+ self.setData({
+ scanAnimation: false
+ });
+ }, 500);
+ }
+ });
+ },
+
+ // 微信API扫码(备用方法)
+ startWechatScan: function() {
+ console.log('开始扫码 - 使用微信API');
+
+ var self = this;
+
+ wx.scanCode({
+ onlyFromCamera: false, // 支持摄像头扫码+相册选择
+ scanType: ['qrCode', 'datamatrix', 'pdf417'],
+ success: function(res) {
+ console.log('微信API扫码成功:', res);
+ self.handleScanResult(res);
+ },
+ fail: function(err) {
+ console.error('微信API扫码失败:', err);
+
+ // 检查是否是用户主动取消
+ if (err.errMsg && err.errMsg.includes('cancel')) {
+ // 用户取消,不显示错误提示
+ return;
+ }
+
+ // 真正的扫码失败才显示提示
+ wx.showModal({
+ title: '识别失败',
+ content: '无法识别二维码。请确保二维码清晰可见,或尝试选择相册中的图片进行识别。',
+ confirmText: '知道了',
+ showCancel: false
+ });
+ },
+ complete: function() {
+ // 停止扫码动画
+ self.setData({
+ scanAnimation: false
+ });
+ }
+ });
+ },
+
+ // 处理扫码结果
+ handleScanResult: function(result) {
+ console.log('处理扫码结果:', result);
+
+ // 兼容不同的结果格式
+ var content;
+ if (typeof result === 'string') {
+ // 直接传入字符串(自定义扫码页面)
+ content = result;
+ } else if (result.result) {
+ // 微信API扫码结果格式
+ content = result.result;
+ } else {
+ console.error('未知的扫码结果格式:', result);
+ return;
+ }
+
+ this.saveScanHistory(content);
+ this.processScanResult(content);
+ },
+
+ // 保存扫描历史
+ saveScanHistory: function(content) {
+ const historyManager = require('../../utils/history-manager.js');
+ const success = historyManager.saveScanRecord(content);
+
+ if (success) {
+ // 更新页面数据
+ this.loadData();
+ }
+ },
+
+ // 处理扫描结果
+ processScanResult: function(content) {
+ console.log('处理扫描内容:', content);
+
+ // URL处理
+ if (/^https?:\/\/.+/i.test(content)) {
+ wx.showModal({
+ title: '发现网址',
+ content: content,
+ confirmText: '复制',
+ showCancel: false,
+ success: function(res) {
+ if (res.confirm) {
+ // 复制到剪贴板
+ wx.setClipboardData({
+ data: content,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // WiFi信息处理
+ if (/^WIFI:/i.test(content)) {
+ var wifiMatch = content.match(/WIFI:T:([^;]*);S:([^;]*);P:([^;]*);H:([^;]*);?/);
+ if (wifiMatch) {
+ var wifiInfo = '网络名称: ' + wifiMatch[2] + '\n' +
+ '密码: ' + wifiMatch[3] + '\n' +
+ '加密类型: ' + wifiMatch[1];
+
+ wx.showModal({
+ title: 'WiFi信息',
+ content: wifiInfo,
+ confirmText: '复制密码',
+ cancelText: '知道了',
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: wifiMatch[3],
+ success: function() {
+ wx.showToast({
+ title: '密码已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+ }
+
+ // 电话号码处理
+ if (/^tel:/i.test(content) || /^(\+?86)?1[3-9]\d{9}$/.test(content)) {
+ var phoneNumber = content.replace(/^tel:/, '');
+ wx.showModal({
+ title: '电话号码',
+ content: phoneNumber,
+ confirmText: '拨打',
+ cancelText: '复制',
+ success: function(res) {
+ if (res.confirm) {
+ wx.makePhoneCall({
+ phoneNumber: phoneNumber,
+ fail: function() {
+ wx.showToast({
+ title: '拨打失败',
+ icon: 'none'
+ });
+ }
+ });
+ } else if (res.cancel) {
+ wx.setClipboardData({
+ data: phoneNumber,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // 邮箱处理
+ if (/^mailto:/i.test(content) || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
+ var email = content.replace(/^mailto:/, '');
+ wx.showModal({
+ title: '邮箱地址',
+ content: email,
+ confirmText: '复制',
+ showCancel: false,
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: email,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ return;
+ }
+
+ // 普通文本
+ wx.showModal({
+ title: '扫描结果',
+ content: content.length > 100 ? content.substring(0, 100) + '...' : content,
+ confirmText: '复制',
+ success: function(res) {
+ if (res.confirm) {
+ wx.setClipboardData({
+ data: content,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ }
+ });
+ }
+ }
+ });
+ },
+
+ // 检查是否可以操作
+ checkCanOperate: function() {
+ if (this.data.isInitializing) {
+ wx.showToast({
+ title: '初始化中,请稍候...',
+ icon: 'loading',
+ duration: 1500
+ });
+ return false;
+ }
+ return true;
+ },
+
+ // 前往生成二维码页面
+ goToGenerateQR: function() {
+ if (!this.checkCanOperate()) return;
+
+ wx.navigateTo({
+ url: '/pages/generate-qr/generate-qr'
+ });
+ },
+
+ // 前往生成条形码页面
+ goToGenerateBarcode: function() {
+ if (!this.checkCanOperate()) return;
+
+ wx.navigateTo({
+ url: '/pages/generate-barcode/generate-barcode'
+ });
+ },
+
+ // 前往历史记录页面
+ goToHistory: function() {
+ if (!this.checkCanOperate()) return;
+
+ wx.navigateTo({
+ url: '/pages/history/history'
+ });
+ },
+
+
+
+ // 打开相册扫码
+ chooseImage: function() {
+ // 检查是否可以操作
+ if (!this.checkCanOperate()) return;
+
+ console.log('打开相册扫码');
+
+ var self = this;
+
+ // 直接调用扫码,但允许从相册选择
+ wx.scanCode({
+ onlyFromCamera: false, // 允许从相册选择
+ scanType: ['qrCode', 'datamatrix', 'pdf417'],
+ success: function(res) {
+ console.log('图片识别成功:', res);
+ self.handleScanResult(res);
+ },
+ fail: function(err) {
+ console.error('图片识别失败:', err);
+
+ // 检查是否是用户主动取消
+ if (err.errMsg && err.errMsg.includes('cancel')) {
+ // 用户取消,不显示错误提示
+ return;
+ }
+
+ // 真正的识别失败才显示提示
+ wx.showModal({
+ title: '识别失败',
+ content: '无法识别图片中的二维码。请确保图片清晰且包含标准二维码(不支持微型二维码和条形码)。\n\n提示:点击"打开相册"后,请选择"从相册选择"选项。',
+ confirmText: '知道了',
+ showCancel: false
+ });
+ }
+ });
+ },
+
+ // 查看最近使用项目
+ viewRecentItem: function(e) {
+ var item = e.currentTarget.dataset.item;
+
+ // 为弹窗准备完整的数据
+ var fullItem = Object.assign({}, item, {
+ sourceText: item.isGenerated ? '生成' : '扫描',
+ fullTime: this.formatFullTime(new Date(item.scanTime || item.timestamp))
+ });
+
+ this.setData({
+ currentRecentItem: fullItem,
+ showRecentDetail: true
+ });
+ },
+
+ // 隐藏最近使用详情弹窗
+ hideRecentDetail: function() {
+ this.setData({
+ showRecentDetail: false,
+ currentRecentItem: null
+ });
+ },
+
+ // 阻止事件冒泡
+ stopPropagation: function() {
+ // 空函数,用于阻止事件冒泡
+ },
+
+ // 复制最近使用内容
+ copyRecentContent: function() {
+ var content = this.data.currentRecentItem.content;
+ var self = this;
+
+ wx.setClipboardData({
+ data: content,
+ success: function() {
+ wx.showToast({
+ title: '已复制',
+ icon: 'success'
+ });
+ self.hideRecentDetail();
+ },
+ fail: function() {
+ wx.showToast({
+ title: '复制失败',
+ icon: 'none'
+ });
+ }
+ });
+ },
+
+ // 分享最近使用内容
+ shareRecentContent: function() {
+ var item = this.data.currentRecentItem;
+ var self = this;
+
+ wx.showActionSheet({
+ itemList: ['发送给朋友', '分享到朋友圈', '复制内容'],
+ success: function(res) {
+ switch (res.tapIndex) {
+ case 0:
+ // 发送给朋友的逻辑
+ wx.showToast({
+ title: '功能开发中',
+ icon: 'none'
+ });
+ break;
+ case 1:
+ // 分享到朋友圈的逻辑
+ wx.showToast({
+ title: '功能开发中',
+ icon: 'none'
+ });
+ break;
+ case 2:
+ // 复制内容
+ self.copyRecentContent();
+ break;
+ }
+
+ self.hideRecentDetail();
+ }
+ });
+ },
+
+ // 格式化完整时间
+ formatFullTime: function(date) {
+ var year = date.getFullYear();
+ var month = String(date.getMonth() + 1).padStart(2, '0');
+ var day = String(date.getDate()).padStart(2, '0');
+ var hour = String(date.getHours()).padStart(2, '0');
+ var minute = String(date.getMinutes()).padStart(2, '0');
+ var second = String(date.getSeconds()).padStart(2, '0');
+
+ return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
+ },
+
+ // 广告点击
+ onAdClick: function() {
+ wx.showToast({
+ title: '广告跳转',
+ icon: 'none'
+ });
+ },
+
+ // 关闭广告
+ closeAd: function() {
+ this.setData({
+ showAd: false
+ });
+ },
+
+ // 分享功能
+ onShareAppMessage: function() {
+ return {
+ title: '小籽二维码 - 扫码生成一站式服务',
+ path: '/pages/index/index'
+ };
+ },
+
+ // 显示扫码模式选择器
+ showScanModeSelector: function() {
+ console.log('显示扫码模式选择器');
+ this.setData({
+ showModeSelector: true
+ });
+
+ // 触感反馈
+ wx.vibrateShort({
+ type: 'medium'
+ });
+ },
+
+ // 隐藏扫码模式选择器
+ hideModeSelector: function() {
+ this.setData({
+ showModeSelector: false
+ });
+ },
+
+ // 设置扫码模式
+ setScanMode: function(e) {
+ var mode = e.currentTarget.dataset.mode;
+ console.log('设置扫码模式:', mode);
+
+ this.setData({
+ scanMode: mode,
+ showModeSelector: false
+ });
+
+ // 触感反馈
+ wx.vibrateShort({
+ type: 'light'
+ });
+
+ // 显示提示
+ var modeText = mode === 'custom' ? '自定义摄像头' : '微信API扫码';
+ wx.showToast({
+ title: '已切换到' + modeText,
+ icon: 'none',
+ duration: 2000
+ });
+
+ // 保存模式设置到本地存储
+ try {
+ wx.setStorageSync('scanMode', mode);
+ } catch (error) {
+ console.error('保存扫码模式失败:', error);
+ }
+ },
+
+ // 停止事件冒泡(模式选择器用)
+ stopPropagation: function() {
+ // 阻止事件冒泡
+ }
+});
diff --git a/pages/index/index.json b/pages/index/index.json
new file mode 100644
index 0000000000000000000000000000000000000000..b55b5a25411975f285b21a489b8ef5b6f6c325a6
--- /dev/null
+++ b/pages/index/index.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {
+ }
+}
\ No newline at end of file
diff --git a/pages/index/index.wxml b/pages/index/index.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..5da8c28ae5f1c91bfff72ea2ecc44a5043b1b0f6
--- /dev/null
+++ b/pages/index/index.wxml
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 生成二维码
+ 文本 · 链接 · 名片 · WiFi
+
+
+
+
+
+
+
+
+
+
+
+ 条形码
+ Code128 · EAN13 · Code39
+
+
+
+
+
+
+
+
+
+ 扫描记录
+ {{totalHistory}}条记录
+
+
+
+
+
+
+
+
+
+ 打开相册
+ 识别图中码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 推荐应用
+ 发现更多实用工具
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{item.title}}
+ {{item.time}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 类型
+ {{currentRecentItem.typeText}}
+
+
+ 来源
+ {{currentRecentItem.sourceText}}
+
+
+ 时间
+ {{currentRecentItem.fullTime}}
+
+
+ 内容
+
+ {{currentRecentItem.content}}
+
+
+
+
+
+
+ 复制
+
+
+ 分享
+
+
+
+
+
+
+
+
+
+
+
+ 扫一扫
+
+
+
+
+
+
+
+
+
+
+
+
+ 📱
+
+ 自定义摄像头
+ 拍照识别,支持手动控制
+
+ ✓
+
+
+ 🔍
+
+ 微信API扫码
+ 系统自动识别,速度更快
+
+ ✓
+
+
+
+
diff --git a/pages/index/index.wxss b/pages/index/index.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..63da4cfea6b83ba8ecf84022ab0d23548e6bc197
--- /dev/null
+++ b/pages/index/index.wxss
@@ -0,0 +1,1050 @@
+/**二维码工具箱首页样式 - iOS风格**/
+
+/* 引入本地图标样式 */
+@import "../../assets/icons/icons.wxss";
+
+/* 全局设置 */
+page {
+ background-color: #f8f9fa;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
+ color: #1d1d1f;
+ padding: 0;
+ margin: 0;
+}
+
+/* 主容器 */
+.container {
+ height: 100vh;
+ background-color: #f8f9fa;
+ box-sizing: border-box;
+}
+
+/* Logo和标题区域 */
+.header-section {
+ padding: 40rpx 40rpx 40rpx; /* 恢复合理的内边距,状态栏下方留适当空间 */
+ text-align: center;
+ background: linear-gradient(135deg, #e8f5e8 0%, #f0fdf4 100%);
+}
+
+.app-logo {
+ margin-bottom: 20rpx;
+}
+
+.logo-icon {
+ width: 150rpx;
+ height: 150rpx;
+ margin: 0 auto;
+ border-radius: 28rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 32rpx rgba(34, 197, 94, 0.3);
+ overflow: hidden;
+}
+
+.icon-qr {
+ font-size: 64rpx;
+ color: white;
+}
+
+.logo-icon-img {
+ width: 150rpx;
+ height: 150rpx;
+ border-radius: 28rpx;
+}
+
+.app-title {
+ display: block;
+ font-size: 64rpx;
+ font-weight: 700;
+ color: #1d1d1f;
+ margin-bottom: 10rpx;
+ letter-spacing: -1rpx;
+}
+
+.app-subtitle {
+ display: block;
+ font-size: 28rpx;
+ color: #6b7280;
+ font-weight: 400;
+}
+
+/* 功能按钮区域 */
+.function-section {
+ padding: 0 40rpx;
+ margin-bottom: 40rpx;
+}
+
+.function-grid {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ grid-template-rows: auto auto;
+ gap: 24rpx;
+ grid-template-areas:
+ "large album"
+ "small history";
+}
+
+/* 功能卡片基础样式 */
+.function-card {
+ background: white;
+ border-radius: 24rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ transition: all 0.3s ease;
+ border: 1rpx solid rgba(0, 0, 0, 0.02);
+}
+
+.function-card:active {
+ transform: scale(0.98);
+ box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.08);
+}
+
+/* 各卡片激活状态保持原有颜色 */
+.function-card.large:active {
+ background: linear-gradient(135deg, #16a34a 0%, #15803d 100%);
+}
+
+.function-card.small:active {
+ background: linear-gradient(135deg, #f56500 0%, #ea580c 100%);
+}
+
+.function-card.history:active {
+ background: linear-gradient(135deg, #1d4ed8 0%, #1e40af 100%);
+}
+
+.function-card.album:active {
+ background: linear-gradient(135deg, #7c3aed 0%, #6d28d9 100%);
+}
+
+/* 大卡片 - 生成二维码 */
+.function-card.large {
+ grid-area: large;
+ background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
+ color: white;
+}
+
+.function-card.large .card-title {
+ color: white;
+}
+
+.function-card.large .card-subtitle {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 打开相册卡片 - 右上 */
+.function-card.album {
+ grid-area: album;
+ background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
+ color: white;
+}
+
+.function-card.album .card-title {
+ color: white;
+}
+
+.function-card.album .card-subtitle {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 扫描记录卡片 - 右下 */
+.function-card.history {
+ grid-area: history;
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+ color: white;
+}
+
+.function-card.history .card-title {
+ color: white;
+}
+
+.function-card.history .card-subtitle {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 小卡片 - 条形码 */
+.function-card.small {
+ grid-area: small;
+ background: linear-gradient(135deg, #ff6b35 0%, #f56500 100%);
+ color: white;
+}
+
+.function-card.small .card-title {
+ color: white;
+}
+
+.function-card.small .card-subtitle {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+/* 卡片内容 */
+.card-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.card-icon {
+ margin-bottom: 16rpx;
+ font-size: 48rpx;
+}
+
+.large-icon {
+ font-size: 56rpx;
+}
+
+.medium-icon {
+ font-size: 48rpx;
+}
+
+.small-icon {
+ font-size: 40rpx;
+}
+
+/* 功能卡片图标样式 */
+.card-icon-img {
+ width: 70rpx;
+ height: 70rpx;
+}
+
+.card-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ margin-bottom: 8rpx;
+ color: #1d1d1f;
+}
+
+.card-subtitle {
+ font-size: 24rpx;
+ color: #6b7280;
+ margin-bottom: 16rpx;
+ line-height: 1.4;
+}
+
+/* 移除了选项标签和选择器相关样式,界面更简洁 */
+
+/* 广告Banner区域 */
+.ad-section {
+ margin: 0 40rpx 40rpx;
+}
+
+.ad-banner {
+ background: white;
+ border-radius: 20rpx;
+ padding: 32rpx;
+ box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ position: relative;
+ overflow: hidden;
+}
+
+.ad-banner::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 8rpx;
+ height: 100%;
+ background: linear-gradient(135deg, #ff6b35, #f56500);
+}
+
+.ad-content {
+ display: flex;
+ align-items: center;
+ flex: 1;
+}
+
+.ad-icon {
+ width: 80rpx;
+ height: 80rpx;
+ background: linear-gradient(135deg, #ff6b35, #f56500);
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 24rpx;
+ font-size: 36rpx;
+ color: white;
+}
+
+.ad-text {
+ flex: 1;
+}
+
+.ad-title {
+ display: block;
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1d1d1f;
+ margin-bottom: 4rpx;
+}
+
+.ad-desc {
+ display: block;
+ font-size: 24rpx;
+ color: #6b7280;
+}
+
+.ad-close {
+ width: 48rpx;
+ height: 48rpx;
+ border-radius: 24rpx;
+ background: #f3f4f6;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: 16rpx;
+ font-size: 28rpx;
+ color: #9ca3af;
+}
+
+/* 最近记录区域 */
+.recent-section {
+ margin: 0 40rpx 40rpx;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24rpx;
+}
+
+.section-title {
+ font-size: 40rpx;
+ font-weight: 700;
+ color: #1d1d1f;
+}
+
+.section-action {
+ display: flex;
+ align-items: center;
+ gap: 8rpx;
+}
+
+.action-text {
+ font-size: 24rpx;
+ color: #22c55e;
+ font-weight: 500;
+}
+
+.section-action .ion {
+ font-size: 20rpx;
+ color: #22c55e;
+}
+
+.recent-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20rpx;
+ padding: 8rpx 0;
+}
+
+.recent-item {
+ text-align: center;
+}
+
+.item-preview {
+ width: 120rpx;
+ height: 120rpx;
+ background: white;
+ border-radius: 20rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 12rpx;
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
+}
+
+.item-thumbnail {
+ width: 100%;
+ height: 100%;
+ border-radius: 20rpx;
+}
+
+.item-icon {
+ font-size: 36rpx;
+ color: #22c55e;
+}
+
+.item-icon-img {
+ width: 36rpx;
+ height: 36rpx;
+}
+
+.item-title {
+ font-size: 22rpx;
+ color: #1d1d1f;
+ font-weight: 500;
+ display: block;
+ margin-bottom: 4rpx;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.item-time {
+ font-size: 20rpx;
+ color: #9ca3af;
+ display: block;
+}
+
+/* 底部安全区域 */
+.bottom-safe-area {
+ height: 200rpx; /* 确保最后的内容不被悬浮按钮遮挡 */
+}
+
+/* 底部悬浮扫码按钮 */
+.scan-float-button {
+ position: fixed;
+ bottom: 80rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 160rpx;
+ height: 160rpx;
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ border-radius: 50%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 32rpx rgba(34, 197, 94, 0.4);
+ z-index: 1000;
+ transition: all 0.3s ease;
+ animation: breathe 3s infinite ease-in-out;
+}
+
+.scan-float-button:active {
+ transform: translateX(-50%) scale(0.95);
+}
+
+.scan-button-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 2;
+}
+
+.scan-icon {
+ margin-bottom: 8rpx;
+ font-size: 48rpx;
+ color: white;
+}
+
+/* 扫码按钮图标样式 */
+.scan-icon-img {
+ width: 48rpx;
+ height: 48rpx;
+}
+
+.scan-text {
+ font-size: 20rpx;
+ color: white;
+ font-weight: 600;
+}
+
+/* 扫码按钮向外放射波纹效果 - 绿色到白色渐变 */
+.scan-ripple {
+ position: absolute;
+ width: 160rpx;
+ height: 160rpx;
+ border: 4rpx solid rgba(34, 197, 94, 0.8);
+ border-radius: 50%;
+ animation: rippleOutward 2s infinite ease-out;
+ top: 0;
+ left: 0;
+}
+
+.scan-ripple-2 {
+ animation-delay: 0.6s;
+ border-color: rgba(34, 197, 94, 0.6);
+ border-width: 3rpx;
+}
+
+.scan-ripple-3 {
+ animation-delay: 1.2s;
+ border-color: rgba(34, 197, 94, 0.4);
+ border-width: 2rpx;
+}
+
+@keyframes rippleOutward {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ border-color: rgba(34, 197, 94, 0.8);
+ border-width: 4rpx;
+ }
+ 25% {
+ opacity: 0.9;
+ border-color: rgba(34, 197, 94, 0.6);
+ border-width: 3rpx;
+ }
+ 50% {
+ opacity: 0.6;
+ border-color: rgba(120, 220, 150, 0.5);
+ border-width: 2rpx;
+ }
+ 75% {
+ opacity: 0.3;
+ border-color: rgba(200, 240, 210, 0.4);
+ border-width: 1rpx;
+ }
+ 100% {
+ transform: scale(2.2);
+ opacity: 0;
+ border-color: rgba(255, 255, 255, 0.2);
+ border-width: 1rpx;
+ }
+}
+
+/* 扫码时的快速波纹效果 */
+.scanning .scan-ripple {
+ animation-duration: 1s;
+ border-color: rgba(34, 197, 94, 1);
+ border-width: 5rpx;
+}
+
+.scanning .scan-ripple-2 {
+ animation-delay: 0.3s;
+ border-color: rgba(34, 197, 94, 0.8);
+}
+
+.scanning .scan-ripple-3 {
+ animation-delay: 0.6s;
+ border-color: rgba(34, 197, 94, 0.6);
+}
+
+/* 呼吸灯效果 */
+@keyframes breathe {
+ 0%, 100% {
+ box-shadow: 0 8rpx 32rpx rgba(34, 197, 94, 0.4);
+ }
+ 50% {
+ box-shadow: 0 8rpx 40rpx rgba(34, 197, 94, 0.6);
+ }
+}
+
+/* 响应式适配 */
+@media (max-width: 375px) {
+ .function-grid {
+ gap: 20rpx;
+ }
+
+ .function-card {
+ padding: 24rpx;
+ }
+
+ .app-title {
+ font-size: 56rpx;
+ }
+}
+
+/* 深色模式适配 */
+@media (prefers-color-scheme: dark) {
+ page {
+ background-color: #000000;
+ color: #ffffff;
+ }
+
+ .container {
+ background-color: #000000;
+ }
+
+ .header-section {
+ background: linear-gradient(135deg, #1a2e1a 0%, #0f1f0f 100%);
+ }
+
+ .app-title {
+ color: #ffffff;
+ }
+
+ .function-card {
+ background: #1c1c1e;
+ border-color: rgba(255, 255, 255, 0.1);
+ }
+
+ .card-title {
+ color: #ffffff;
+ }
+
+ .ad-banner {
+ background: #1c1c1e;
+ }
+
+ .ad-title {
+ color: #ffffff;
+ }
+
+ .item-preview {
+ background: #1c1c1e;
+ }
+
+ .item-title {
+ color: #ffffff;
+ }
+}
+
+/* 详情弹窗样式 */
+.detail-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.detail-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.modal-content {
+ width: 90%;
+ max-width: 600rpx;
+ background: white;
+ border-radius: 24rpx;
+ overflow: hidden;
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+
+.detail-modal.show .modal-content {
+ transform: scale(1);
+}
+
+.modal-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 32rpx;
+ border-bottom: 1rpx solid #f1f5f9;
+}
+
+.modal-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #1e293b;
+}
+
+.modal-close {
+ width: 48rpx;
+ height: 48rpx;
+ border-radius: 50%;
+ background: #f8fafc;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+}
+
+.modal-close:active {
+ transform: scale(0.9);
+ background: #e2e8f0;
+}
+
+.modal-close text {
+ font-size: 24rpx;
+ color: #64748b;
+}
+
+.modal-body {
+ padding: 32rpx;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.detail-item {
+ display: flex;
+ margin-bottom: 24rpx;
+}
+
+.detail-item.full {
+ flex-direction: column;
+}
+
+.detail-label {
+ font-size: 26rpx;
+ color: #64748b;
+ font-weight: 500;
+ width: 120rpx;
+ flex-shrink: 0;
+}
+
+.detail-value {
+ font-size: 26rpx;
+ color: #1e293b;
+ flex: 1;
+}
+
+.detail-content {
+ margin-top: 12rpx;
+ padding: 20rpx;
+ background: #f8fafc;
+ border-radius: 12rpx;
+ border: 1rpx solid #e2e8f0;
+}
+
+.detail-text {
+ font-size: 26rpx;
+ color: #1e293b;
+ line-height: 1.6;
+ word-break: break-all;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 16rpx;
+ padding: 32rpx;
+ border-top: 1rpx solid #f1f5f9;
+}
+
+.modal-btn {
+ flex: 1;
+ height: 80rpx;
+ border-radius: 16rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28rpx;
+ font-weight: 500;
+ transition: all 0.3s ease;
+}
+
+.modal-btn.primary {
+ background: linear-gradient(135deg, #22c55e, #16a34a);
+ color: white;
+ box-shadow: 0 4rpx 16rpx rgba(34, 197, 94, 0.3);
+}
+
+.modal-btn.primary:active {
+ transform: translateY(2rpx);
+ box-shadow: 0 2rpx 8rpx rgba(34, 197, 94, 0.3);
+}
+
+.modal-btn.secondary {
+ background: #f8fafc;
+ color: #475569;
+ border: 1rpx solid #e2e8f0;
+}
+
+.modal-btn.secondary:active {
+ background: #e2e8f0;
+}
+
+/* 扫码模式选择器样式 */
+.scan-mode-text {
+ font-size: 18rpx;
+ color: rgba(255, 255, 255, 0.7);
+ margin-top: 4rpx;
+ font-weight: 400;
+}
+
+.mode-selector-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 9999;
+ padding: 40rpx;
+ backdrop-filter: blur(4rpx);
+}
+
+.mode-selector {
+ width: 100%;
+ max-width: 600rpx;
+ background: #ffffff;
+ border-radius: 28rpx;
+ overflow: hidden;
+ animation: modeShow 0.3s ease-out;
+ box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
+}
+
+@keyframes modeShow {
+ 0% {
+ opacity: 0;
+ transform: scale(0.8) translateY(40rpx);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.mode-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 40rpx 40rpx 20rpx;
+ border-bottom: 1rpx solid #f0f0f0;
+}
+
+.mode-title {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #333333;
+}
+
+.mode-close {
+ width: 60rpx;
+ height: 60rpx;
+ border-radius: 50%;
+ background: #f5f5f5;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ color: #666666;
+ transition: all 0.3s ease;
+}
+
+.mode-close:active {
+ background: #e5e5e5;
+ transform: scale(0.9);
+}
+
+.mode-options {
+ padding: 30rpx 40rpx 40rpx;
+}
+
+.mode-option {
+ display: flex;
+ align-items: center;
+ padding: 30rpx 24rpx;
+ border-radius: 20rpx;
+ margin-bottom: 16rpx;
+ transition: all 0.3s ease;
+ position: relative;
+ background: #f8f9fa;
+ border: 2rpx solid transparent;
+}
+
+.mode-option:last-child {
+ margin-bottom: 0;
+}
+
+.mode-option.active {
+ background: linear-gradient(135deg, #667eea15, #764ba215);
+ border-color: #667eea;
+ box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.15);
+}
+
+.mode-option:active {
+ transform: scale(0.98);
+ background: #f0f0f0;
+}
+
+.mode-option.active:active {
+ background: linear-gradient(135deg, #667eea20, #764ba220);
+}
+
+.mode-icon {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 16rpx;
+ background: linear-gradient(135deg, #667eea, #764ba2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 36rpx;
+ margin-right: 24rpx;
+ flex-shrink: 0;
+}
+
+.mode-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.mode-name {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333333;
+ margin-bottom: 8rpx;
+}
+
+.mode-desc {
+ font-size: 26rpx;
+ color: #666666;
+ line-height: 1.4;
+}
+
+.mode-check {
+ width: 48rpx;
+ height: 48rpx;
+ border-radius: 50%;
+ background: #667eea;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 24rpx;
+ font-weight: bold;
+ margin-left: 16rpx;
+ flex-shrink: 0;
+}
+
+/* 初始化横幅 */
+.init-banner {
+ margin-top: 24rpx;
+ background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
+ border-radius: 16rpx;
+ padding: 16rpx 20rpx;
+ border: 1rpx solid #3b82f6;
+}
+
+.init-content {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+
+.init-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28rpx;
+ height: 28rpx;
+}
+
+.loading-spinner {
+ width: 20rpx;
+ height: 20rpx;
+ border: 2rpx solid #93c5fd;
+ border-top: 2rpx solid #3b82f6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.init-text {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+}
+
+.init-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #1e40af;
+ line-height: 1.2;
+}
+
+.init-desc {
+ font-size: 24rpx;
+ color: #2563eb;
+ line-height: 1.3;
+}
+
+/* 离线模式横幅 */
+.offline-banner {
+ margin-top: 24rpx;
+ background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
+ border-radius: 16rpx;
+ padding: 16rpx 20rpx;
+ border: 1rpx solid #fbbf24;
+}
+
+.offline-content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12rpx;
+ position: relative;
+}
+
+.offline-icon {
+ position: absolute;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32rpx;
+ height: 32rpx;
+}
+
+.offline-icon-img {
+ width: 24rpx;
+ height: 24rpx;
+}
+
+.offline-text {
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+ text-align: center;
+}
+
+.offline-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #92400e;
+ line-height: 1.2;
+}
+
+.offline-desc {
+ font-size: 24rpx;
+ color: #a16207;
+ line-height: 1.3;
+}
+
+/* 离线模式测试开关 */
+.offline-switch-container {
+ margin-top: 24rpx;
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
+ border-radius: 16rpx;
+ padding: 20rpx;
+ border: 1rpx solid #0ea5e9;
+}
+
+.offline-switch-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.switch-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4rpx;
+}
+
+.switch-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #0c4a6e;
+ line-height: 1.2;
+}
+
+.switch-desc {
+ font-size: 24rpx;
+ color: #0369a1;
+ line-height: 1.3;
+}
+
+.offline-switch {
+ transform: scale(1.1);
+}
+
+
diff --git a/pages/logs/logs.js b/pages/logs/logs.js
new file mode 100644
index 0000000000000000000000000000000000000000..85f6aac5ab16db728fa27cdf75c4ab2126b1105b
--- /dev/null
+++ b/pages/logs/logs.js
@@ -0,0 +1,18 @@
+// logs.js
+const util = require('../../utils/util.js')
+
+Page({
+ data: {
+ logs: []
+ },
+ onLoad() {
+ this.setData({
+ logs: (wx.getStorageSync('logs') || []).map(log => {
+ return {
+ date: util.formatTime(new Date(log)),
+ timeStamp: log
+ }
+ })
+ })
+ }
+})
diff --git a/pages/logs/logs.json b/pages/logs/logs.json
new file mode 100644
index 0000000000000000000000000000000000000000..b55b5a25411975f285b21a489b8ef5b6f6c325a6
--- /dev/null
+++ b/pages/logs/logs.json
@@ -0,0 +1,4 @@
+{
+ "usingComponents": {
+ }
+}
\ No newline at end of file
diff --git a/pages/logs/logs.wxml b/pages/logs/logs.wxml
new file mode 100644
index 0000000000000000000000000000000000000000..85cf1bfe79c718c4222c94fc090af60dcf29575d
--- /dev/null
+++ b/pages/logs/logs.wxml
@@ -0,0 +1,6 @@
+
+
+
+ {{index + 1}}. {{log.date}}
+
+
diff --git a/pages/logs/logs.wxss b/pages/logs/logs.wxss
new file mode 100644
index 0000000000000000000000000000000000000000..33f9d9e1dca09f6aeb68b3e39c243876ce5d240f
--- /dev/null
+++ b/pages/logs/logs.wxss
@@ -0,0 +1,16 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: hidden;
+}
+.log-item {
+ margin-top: 20rpx;
+ text-align: center;
+}
+.log-item:last-child {
+ padding-bottom: env(safe-area-inset-bottom);
+}
diff --git a/project.config.json b/project.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ba177cd8068c58a43cb56d316783a1f70be2fa5
--- /dev/null
+++ b/project.config.json
@@ -0,0 +1,38 @@
+{
+ "compileType": "miniprogram",
+ "libVersion": "trial",
+ "packOptions": {
+ "ignore": [],
+ "include": []
+ },
+ "setting": {
+ "coverView": true,
+ "es6": false,
+ "postcss": true,
+ "minified": true,
+ "enhance": true,
+ "showShadowRootInWxmlPanel": true,
+ "packNpmRelationList": [],
+ "compileWorklet": false,
+ "uglifyFileName": false,
+ "uploadWithSourceMap": true,
+ "packNpmManually": false,
+ "minifyWXSS": true,
+ "minifyWXML": true,
+ "localPlugins": false,
+ "condition": false,
+ "swc": false,
+ "disableSWC": true,
+ "disableUseStrict": false,
+ "useCompilerPlugins": false,
+ "ignoreUploadUnusedFiles": true,
+ "useIsolateContext": false
+ },
+ "condition": {},
+ "editorSetting": {
+ "tabIndent": "auto",
+ "tabSize": 2
+ },
+ "appid": "touristappid",
+ "simulatorPluginLibVersion": {}
+}
\ No newline at end of file
diff --git a/sitemap.json b/sitemap.json
new file mode 100644
index 0000000000000000000000000000000000000000..ca02add20b581be471b8d17f887b8e8337070546
--- /dev/null
+++ b/sitemap.json
@@ -0,0 +1,7 @@
+{
+ "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
+ "rules": [{
+ "action": "allow",
+ "page": "*"
+ }]
+}
\ No newline at end of file
diff --git a/theme.json b/theme.json
new file mode 100644
index 0000000000000000000000000000000000000000..ebfa4859f78cf7fbeb5145d300f439f2870bfc43
--- /dev/null
+++ b/theme.json
@@ -0,0 +1,14 @@
+{
+ "light": {
+ "navigationBarBackgroundColor": "#ffffff",
+ "navigationBarTextStyle": "black",
+ "backgroundColor": "#f8f9fa",
+ "backgroundTextStyle": "dark"
+ },
+ "dark": {
+ "navigationBarBackgroundColor": "#000000",
+ "navigationBarTextStyle": "white",
+ "backgroundColor": "#000000",
+ "backgroundTextStyle": "light"
+ }
+}
\ No newline at end of file
diff --git a/utils/api-service.js b/utils/api-service.js
new file mode 100644
index 0000000000000000000000000000000000000000..e31e8bc02f98e56b41f1dd214e86e4b6d047162e
--- /dev/null
+++ b/utils/api-service.js
@@ -0,0 +1,481 @@
+// API服务文件
+const ConfigManager = require('../config/config-manager.js');
+const ApiConfig = ConfigManager.getApiConfig();
+const MessageUtil = require('./message-util.js');
+
+class ApiService {
+ constructor() {
+ this.config = ApiConfig;
+ this.userUniqueId = null;
+ this.accessToken = null;
+ this.userCode = null;
+ this.userInfo = null;
+ this.isLoggedIn = false;
+ this.init();
+ }
+
+ // 初始化
+ init() {
+ this.ensureUserUniqueId();
+ this.loadAuthInfo();
+ }
+
+ // 加载认证信息
+ loadAuthInfo() {
+ try {
+ const authKeys = this.config.auth.storageKeys;
+ this.accessToken = wx.getStorageSync(authKeys.accessToken);
+ this.userCode = wx.getStorageSync(authKeys.userCode);
+ this.userInfo = wx.getStorageSync(authKeys.userInfo);
+ this.isLoggedIn = wx.getStorageSync(authKeys.isLoggedIn) || false;
+ } catch (error) {
+ console.error('加载认证信息失败:', error);
+ this.clearAuthInfo();
+ }
+ }
+
+ // 保存认证信息
+ saveAuthInfo(authData) {
+ try {
+ const authKeys = this.config.auth.storageKeys;
+
+ // 提取关键信息
+ this.accessToken = authData.token;
+ this.userCode = authData.user_code;
+ this.userInfo = authData;
+ this.isLoggedIn = true;
+
+ // 验证必要信息是否存在
+ if (!this.accessToken) {
+ console.error('警告:token为空');
+ }
+ if (!this.userCode) {
+ console.error('警告:user_code为空');
+ }
+
+ // 保存到本地存储
+ wx.setStorageSync(authKeys.accessToken, this.accessToken);
+ wx.setStorageSync(authKeys.userCode, this.userCode);
+ wx.setStorageSync(authKeys.userInfo, this.userInfo);
+ wx.setStorageSync(authKeys.isLoggedIn, true);
+
+ console.log('认证信息保存成功');
+
+ } catch (error) {
+ console.error('保存认证信息失败:', error);
+ }
+ }
+
+ // 清除认证信息
+ clearAuthInfo() {
+ try {
+ const authKeys = this.config.auth.storageKeys;
+ this.accessToken = null;
+ this.userCode = null;
+ this.userInfo = null;
+ this.isLoggedIn = false;
+
+ wx.removeStorageSync(authKeys.accessToken);
+ wx.removeStorageSync(authKeys.userCode);
+ wx.removeStorageSync(authKeys.userInfo);
+ wx.removeStorageSync(authKeys.isLoggedIn);
+ } catch (error) {
+ console.error('清除认证信息失败:', error);
+ }
+ }
+
+ // 检查登录状态
+ isUserLoggedIn() {
+ return this.isLoggedIn && this.accessToken && this.userCode;
+ }
+
+ // 静默登录
+ async silentLogin() {
+ try {
+ // 获取微信授权码
+ const loginResult = await new Promise((resolve, reject) => {
+ wx.login({
+ success: resolve,
+ fail: reject
+ });
+ });
+
+ if (!loginResult.code) {
+ throw new Error('获取微信授权码失败');
+ }
+
+ // 调用登录接口
+ const response = await this.wxLogin(loginResult.code);
+
+ // 检查登录是否成功
+ // 根据接口文档,成功时 result="true" 且 statusCode=3001
+ // 注意:statusCode可能是字符串格式
+ if (response.result === "true" && (response.statusCode === 3001 || response.statusCode === "3001")) {
+ console.log('静默登录成功');
+
+ // 登录成功,保存认证信息
+ this.saveAuthInfo(response.data);
+
+ return {
+ success: true,
+ userInfo: response.data
+ };
+ } else {
+ // 登录失败
+ console.error('登录失败:', response.message || '未知错误');
+ throw new Error(response.message || '登录失败');
+ }
+ } catch (error) {
+ console.error('静默登录失败:', error);
+ this.clearAuthInfo();
+ throw new Error('服务异常');
+ }
+ }
+
+ // 微信登录接口
+ async wxLogin(code) {
+ return new Promise((resolve, reject) => {
+ const url = this.config.server.baseUrl + this.config.endpoints.wxLogin;
+
+ // 构建表单数据
+ const requestData = {
+ code: code,
+ param_corpCode: this.config.auth.corpCode
+ };
+
+ wx.request({
+ url: url,
+ method: 'POST',
+ data: requestData, // 使用表单数据格式
+ timeout: this.config.server.timeout,
+ header: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'Accept': 'application/json'
+ },
+ success: (res) => {
+
+ if (res.statusCode === 200) {
+ resolve(res.data);
+ } else {
+ reject(new Error(`HTTP错误: ${res.statusCode}`));
+ }
+ },
+ fail: (error) => {
+ console.error('登录网络请求失败:', error);
+ reject(new Error('网络请求失败'));
+ }
+ });
+ });
+ }
+
+ // 确保用户唯一标识
+ ensureUserUniqueId() {
+ try {
+ let userId = wx.getStorageSync(this.config.userIdentity.storageKey);
+ if (!userId) {
+ // 生成唯一标识:前缀 + 时间戳 + 随机数
+ userId = this.config.userIdentity.prefix +
+ Date.now() + '_' +
+ Math.random().toString(36).substr(2, 9);
+ wx.setStorageSync(this.config.userIdentity.storageKey, userId);
+ }
+ this.userUniqueId = userId;
+ } catch (error) {
+ console.error('获取用户唯一标识失败:', error);
+ // 临时标识
+ this.userUniqueId = this.config.userIdentity.prefix + 'temp_' + Date.now();
+ }
+ }
+
+ // 检查频率限制
+ checkRateLimit(isBatch = false) {
+ const now = Date.now();
+ const rateConfig = this.config.rateLimit;
+
+ try {
+ // 检查单个/批量生成间隔
+ const intervalKey = isBatch ?
+ rateConfig.storageKeys.lastBatchGen :
+ rateConfig.storageKeys.lastSingleGen;
+ const requiredInterval = isBatch ?
+ rateConfig.batchGenInterval :
+ rateConfig.singleGenInterval;
+
+ const lastGenTime = wx.getStorageSync(intervalKey) || 0;
+ const timeSinceLastGen = now - lastGenTime;
+
+ if (timeSinceLastGen < requiredInterval) {
+ const waitTime = Math.ceil((requiredInterval - timeSinceLastGen) / 1000);
+ return {
+ allowed: false,
+ message: `请等待${waitTime}秒后再试`,
+ waitTime: waitTime
+ };
+ }
+
+ // 检查每分钟请求限制
+ const history = this.getRequestHistory();
+ const currentMinute = Math.floor(now / 60000);
+ const currentMinuteRequests = history.filter(time =>
+ Math.floor(time / 60000) === currentMinute
+ ).length;
+
+ if (currentMinuteRequests >= rateConfig.maxRequestsPerMinute) {
+ return {
+ allowed: false,
+ message: '请求过于频繁,请稍后再试',
+ waitTime: 60
+ };
+ }
+
+ return { allowed: true };
+ } catch (error) {
+ console.error('频率检查失败:', error);
+ return { allowed: true }; // 检查失败时允许请求
+ }
+ }
+
+ // 获取请求历史
+ getRequestHistory() {
+ try {
+ const history = wx.getStorageSync(this.config.rateLimit.storageKeys.requestHistory) || [];
+ const now = Date.now();
+ // 只保留最近一分钟的记录
+ return history.filter(time => now - time < 60000);
+ } catch (error) {
+ console.error('获取请求历史失败:', error);
+ return [];
+ }
+ }
+
+ // 记录请求时间
+ recordRequest(isBatch = false) {
+ const now = Date.now();
+ const rateConfig = this.config.rateLimit;
+
+ try {
+ // 记录单个/批量生成时间
+ const intervalKey = isBatch ?
+ rateConfig.storageKeys.lastBatchGen :
+ rateConfig.storageKeys.lastSingleGen;
+ wx.setStorageSync(intervalKey, now);
+
+ // 记录到请求历史
+ const history = this.getRequestHistory();
+ history.push(now);
+ wx.setStorageSync(rateConfig.storageKeys.requestHistory, history);
+ } catch (error) {
+ console.error('记录请求时间失败:', error);
+ }
+ }
+
+ // 生成二维码/条形码
+ async generateCode(params) {
+ const {
+ codeType, // 码类型:'qrcode' | 'barcode'
+ subType, // 子类型:'standard' | 'micro' | 'Code 128' 等
+ content, // 内容(单个或数组)
+ batchMode = false, // 是否批量模式
+ options = {} // 其他选项(颜色、尺寸等)
+ } = params;
+
+ // 频率检查
+ const rateCheck = this.checkRateLimit(batchMode);
+ if (!rateCheck.allowed) {
+ throw new Error(rateCheck.message);
+ }
+
+ // 验证参数
+ this.validateParams(params);
+
+ // 构建请求数据
+ const requestData = this.buildRequestData(params);
+
+ try {
+ // 记录请求时间
+ this.recordRequest(batchMode);
+
+ // 发送请求
+ const response = await this.sendRequest(requestData);
+ return response;
+ } catch (error) {
+ console.error('生成请求失败:', error);
+ throw error;
+ }
+ }
+
+ // 验证参数
+ validateParams(params) {
+ const { codeType, subType, content, batchMode } = params;
+
+ if (!codeType || !subType) {
+ throw new Error('缺少必要的码类型参数');
+ }
+
+ if (!content || (Array.isArray(content) && content.length === 0)) {
+ throw new Error('内容不能为空');
+ }
+
+ if (batchMode) {
+ const contentArray = Array.isArray(content) ? content : [content];
+ if (contentArray.length > this.config.rateLimit.maxBatchSize) {
+ throw new Error(`批量生成最多支持${this.config.rateLimit.maxBatchSize}个`);
+ }
+ }
+ }
+
+ // 构建请求数据
+ buildRequestData(params) {
+ const { codeType, subType, content, batchMode, options = {} } = params;
+
+ // 映射码类型
+ let mappedType;
+ if (codeType === 'qrcode') {
+ mappedType = this.config.codeTypes.qrcode[subType];
+ } else if (codeType === 'barcode') {
+ mappedType = this.config.codeTypes.barcode[subType];
+ }
+
+ if (!mappedType) {
+ throw new Error('不支持的码类型');
+ }
+
+ return {
+ user_id: this.userUniqueId,
+ code_type: mappedType,
+ batch_mode: batchMode,
+ content: batchMode ? (Array.isArray(content) ? content : [content]) : content,
+ options: {
+ foreground_color: options.foregroundColor || '#000000',
+ background_color: options.backgroundColor || '#ffffff',
+ size: options.size || 300,
+ ...options
+ }
+ };
+ }
+
+ // 发送请求
+ sendRequest(data) {
+ return new Promise((resolve, reject) => {
+ const url = this.config.server.baseUrl + this.config.endpoints.codeGeneration;
+
+ // 构建请求头,包含认证信息
+ const headers = {
+ 'content-type': 'application/json'
+ };
+
+ // 添加认证头(如果已登录)
+ if (this.isUserLoggedIn()) {
+ headers['S-Access-Token'] = this.accessToken;
+ headers['S-Code'] = this.userCode;
+ }
+
+ wx.request({
+ url: url,
+ method: 'POST',
+ data: data,
+ timeout: this.config.server.timeout,
+ responseType: data.batch_mode ? 'arraybuffer' : 'arraybuffer', // 都用arraybuffer接收二进制数据
+ header: headers,
+ success: (res) => {
+ // 检查并更新令牌
+ this.handleTokenRefresh(res);
+
+ if (res.statusCode === 200) {
+ resolve({
+ success: true,
+ data: res.data,
+ isBatch: data.batch_mode
+ });
+ } else {
+ reject(new Error(`服务器错误: ${res.statusCode}`));
+ }
+ },
+ fail: (error) => {
+ console.error('请求失败:', error);
+ reject(new Error('网络请求失败,请检查网络连接'));
+ }
+ });
+ });
+ }
+
+ // 处理令牌刷新
+ handleTokenRefresh(response) {
+ try {
+ const newToken = response.header['S-Access-Token'] || response.header['s-access-token'];
+ if (newToken && newToken !== this.accessToken) {
+ console.log('令牌已更新');
+ this.accessToken = newToken;
+ wx.setStorageSync(this.config.auth.storageKeys.accessToken, newToken);
+ }
+ } catch (error) {
+ console.error('处理令牌刷新失败:', error);
+ }
+ }
+
+ // 保存图片到相册
+ async saveImageToAlbum(arrayBuffer, filename = null) {
+ try {
+ // 将arrayBuffer转换为base64
+ const base64 = wx.arrayBufferToBase64(arrayBuffer);
+ const base64Data = 'data:image/png;base64,' + base64;
+
+ // 保存到临时文件
+ const tempFilePath = await new Promise((resolve, reject) => {
+ wx.getFileSystemManager().writeFile({
+ filePath: wx.env.USER_DATA_PATH + '/' + (filename || Date.now() + '.png'),
+ data: arrayBuffer,
+ success: (res) => {
+ resolve(wx.env.USER_DATA_PATH + '/' + (filename || Date.now() + '.png'));
+ },
+ fail: reject
+ });
+ });
+
+ // 保存到相册
+ await new Promise((resolve, reject) => {
+ wx.saveImageToPhotosAlbum({
+ filePath: tempFilePath,
+ success: resolve,
+ fail: reject
+ });
+ });
+
+ return { success: true, path: tempFilePath };
+ } catch (error) {
+ console.error('保存图片失败:', error);
+ throw error;
+ }
+ }
+
+ // 保存压缩包(批量生成结果)
+ async saveBatchResult(arrayBuffer, filename = null) {
+ try {
+ const fileName = filename || `qr_batch_${Date.now()}.zip`;
+ const filePath = wx.env.USER_DATA_PATH + '/' + fileName;
+
+ await new Promise((resolve, reject) => {
+ wx.getFileSystemManager().writeFile({
+ filePath: filePath,
+ data: arrayBuffer,
+ success: resolve,
+ fail: reject
+ });
+ });
+
+ return {
+ success: true,
+ path: filePath,
+ fileName: fileName
+ };
+ } catch (error) {
+ console.error('保存批量结果失败:', error);
+ throw error;
+ }
+ }
+}
+
+// 创建单例实例
+const apiService = new ApiService();
+
+module.exports = apiService;
\ No newline at end of file
diff --git a/utils/barcode-simple.js b/utils/barcode-simple.js
new file mode 100644
index 0000000000000000000000000000000000000000..0519ecba6ea913e21689ec692e81e9e4973fbf73
--- /dev/null
+++ b/utils/barcode-simple.js
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/utils/history-manager.js b/utils/history-manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..6fbb9e5c288ece0555e7afd47c4a279959f57f3b
--- /dev/null
+++ b/utils/history-manager.js
@@ -0,0 +1,254 @@
+// 历史记录管理工具
+const ConfigManager = require('../config/config-manager.js');
+const ApiConfig = ConfigManager.getApiConfig();
+
+/**
+ * 历史记录管理器
+ */
+class HistoryManager {
+ constructor() {
+ this.config = ApiConfig.history;
+ }
+
+ /**
+ * 保存扫码记录
+ * @param {string} content - 扫码内容
+ * @param {Object} extraData - 额外数据
+ */
+ saveScanRecord(content, extraData = {}) {
+ try {
+ const history = this.getHistory(this.config.storageKeys.scanHistory);
+
+ const newRecord = {
+ id: Date.now().toString(),
+ content: content,
+ scanTime: Date.now(),
+ timestamp: Date.now(), // 兼容字段
+ type: this.config.recordTypes.scan,
+ ...extraData
+ };
+
+ this.addToHistory(history, newRecord, this.config.storageKeys.scanHistory);
+ return true;
+ } catch (error) {
+ console.error('保存扫描记录失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 保存二维码生成记录
+ * @param {Object} data - 生成数据
+ */
+ saveQRGenerateRecord(data) {
+ try {
+ const history = this.getHistory(this.config.storageKeys.scanHistory);
+
+ const newRecord = {
+ id: Date.now(),
+ content: data.content,
+ type: data.codeType || (data.type === 'standard' || data.type === 'micro' ? 'qr' : 'barcode'),
+ subType: data.type || data.barcodeType,
+ time: new Date().toLocaleString(),
+ isGenerated: true,
+ batchMode: data.batchMode || false,
+ barcodes: data.barcodes || [],
+ recordType: this.config.recordTypes.qrGenerate
+ };
+
+ this.addToHistory(history, newRecord, this.config.storageKeys.scanHistory);
+ return true;
+ } catch (error) {
+ console.error('保存二维码生成记录失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 保存条形码生成记录
+ * @param {Object} data - 生成数据
+ */
+ saveBarcodeRecord(data) {
+ try {
+ const history = this.getHistory(this.config.storageKeys.barcodeHistory);
+
+ if (data.batchMode) {
+ const lines = data.batchContent.split('\n').filter(line => line.trim());
+ lines.forEach((content, index) => {
+ const record = {
+ id: `${data.timestamp}_${index}`,
+ type: 'barcode',
+ subType: data.barcodeType,
+ content: content.trim(),
+ foregroundColor: data.foregroundColor,
+ backgroundColor: data.backgroundColor,
+ width: data.barcodeWidth,
+ createTime: data.timestamp + index,
+ batch: true,
+ batchIndex: index + 1,
+ batchTotal: lines.length,
+ recordType: this.config.recordTypes.barcodeGenerate
+ };
+
+ this.addToHistory(history, record, this.config.storageKeys.barcodeHistory, false);
+ });
+
+ // 批量模式最后统一保存和限制
+ this.limitHistorySize(history);
+ wx.setStorageSync(this.config.storageKeys.barcodeHistory, history);
+ } else {
+ const record = {
+ id: data.timestamp.toString(),
+ type: 'barcode',
+ subType: data.barcodeType,
+ content: data.inputContent,
+ foregroundColor: data.foregroundColor,
+ backgroundColor: data.backgroundColor,
+ width: data.barcodeWidth,
+ createTime: data.timestamp,
+ batch: false,
+ recordType: this.config.recordTypes.barcodeGenerate
+ };
+
+ this.addToHistory(history, record, this.config.storageKeys.barcodeHistory);
+ }
+
+ return true;
+ } catch (error) {
+ console.error('保存条形码记录失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 保存生成历史记录(通用)
+ * @param {Object} params - 生成参数
+ */
+ saveGenerateHistory(params) {
+ try {
+ const history = this.getHistory(this.config.storageKeys.generateHistory);
+
+ const historyItem = {
+ id: Date.now(),
+ time: new Date(),
+ timestamp: Date.now(),
+ recordType: this.config.recordTypes.qrGenerate,
+ ...params
+ };
+
+ this.addToHistory(history, historyItem, this.config.storageKeys.generateHistory);
+ return true;
+ } catch (error) {
+ console.error('保存生成历史失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 获取历史记录
+ * @param {string} storageKey - 存储键名
+ */
+ getHistory(storageKey) {
+ return wx.getStorageSync(storageKey) || [];
+ }
+
+ /**
+ * 添加记录到历史中
+ * @param {Array} history - 历史记录数组
+ * @param {Object} newRecord - 新记录
+ * @param {string} storageKey - 存储键名
+ * @param {boolean} autoSave - 是否自动保存和限制大小
+ */
+ addToHistory(history, newRecord, storageKey, autoSave = true) {
+ // 检查是否已存在相同内容,如果存在则先删除
+ const existingIndex = history.findIndex(item => item.content === newRecord.content);
+ if (existingIndex !== -1) {
+ history.splice(existingIndex, 1);
+ }
+
+ // 添加到历史记录开头
+ history.unshift(newRecord);
+
+ if (autoSave) {
+ // 限制历史记录数量
+ this.limitHistorySize(history);
+
+ // 保存到本地存储
+ wx.setStorageSync(storageKey, history);
+ }
+ }
+
+ /**
+ * 限制历史记录大小
+ * @param {Array} history - 历史记录数组
+ */
+ limitHistorySize(history) {
+ if (history.length > this.config.maxRecords) {
+ history.splice(this.config.maxRecords);
+ }
+ }
+
+ /**
+ * 清空指定类型的历史记录
+ * @param {string} storageKey - 存储键名
+ */
+ clearHistory(storageKey) {
+ try {
+ wx.removeStorageSync(storageKey);
+ return true;
+ } catch (error) {
+ console.error('清空历史记录失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 删除指定的历史记录项
+ * @param {string} storageKey - 存储键名
+ * @param {Array} idsToDelete - 要删除的记录ID数组
+ */
+ deleteHistoryItems(storageKey, idsToDelete) {
+ try {
+ const history = this.getHistory(storageKey);
+ const filteredHistory = history.filter(item => !idsToDelete.includes(item.id));
+ wx.setStorageSync(storageKey, filteredHistory);
+ return true;
+ } catch (error) {
+ console.error('删除历史记录失败:', error);
+ return false;
+ }
+ }
+
+ /**
+ * 获取历史记录统计信息
+ */
+ getHistoryStats() {
+ try {
+ const scanHistory = this.getHistory(this.config.storageKeys.scanHistory);
+ const generateHistory = this.getHistory(this.config.storageKeys.generateHistory);
+ const barcodeHistory = this.getHistory(this.config.storageKeys.barcodeHistory);
+
+ return {
+ scanCount: scanHistory.length,
+ generateCount: generateHistory.length,
+ barcodeCount: barcodeHistory.length,
+ totalCount: scanHistory.length + generateHistory.length + barcodeHistory.length,
+ maxRecords: this.config.maxRecords
+ };
+ } catch (error) {
+ console.error('获取历史记录统计失败:', error);
+ return {
+ scanCount: 0,
+ generateCount: 0,
+ barcodeCount: 0,
+ totalCount: 0,
+ maxRecords: this.config.maxRecords
+ };
+ }
+ }
+}
+
+// 创建单例实例
+const historyManager = new HistoryManager();
+
+module.exports = historyManager;
\ No newline at end of file
diff --git a/utils/jsbarcode-util.js b/utils/jsbarcode-util.js
new file mode 100644
index 0000000000000000000000000000000000000000..40cf34e316f7140a736904709affc947ed32a3b1
--- /dev/null
+++ b/utils/jsbarcode-util.js
@@ -0,0 +1,238 @@
+/**
+ * JsBarcode 条形码生成工具类
+ * 基于JsBarcode库,适配微信小程序环境
+ */
+
+// 导入JsBarcode库的核心功能
+// 由于小程序环境限制,我们需要手动引入必要的编码模块
+
+/**
+ * Code 128 编码表
+ */
+const CODE128_PATTERNS = [
+ "11011001100", "11001101100", "11001100110", "10010011000", "10010001100",
+ "10001001100", "10011001000", "10011000100", "10001100100", "11001001000",
+ "11001000100", "11000100100", "10110011100", "10011011100", "10011001110",
+ "10111001100", "10011101100", "10011100110", "11001110010", "11001011100",
+ "11001001110", "11011100100", "11001110100", "11101101110", "11101001100",
+ "11100101100", "11100100110", "11101100100", "11100110100", "11100110010",
+ "11011011000", "11011000110", "11000110110", "10100011000", "10001011000",
+ "10001000110", "10110001000", "10001101000", "10001100010", "11010001000",
+ "11000101000", "11000100010", "10110111000", "10110001110", "10001101110",
+ "10111011000", "10111000110", "10001110110", "11101110110", "11010001110",
+ "11000101110", "11011101000", "11011100010", "11011101110", "11101011000",
+ "11101000110", "11100010110", "11101101000", "11101100010", "11100011010",
+ "11101111010", "11001000010", "11110001010", "10100110000", "10100001100",
+ "10010110000", "10010000110", "10000101100", "10000100110", "10110010000",
+ "10110000100", "10011010000", "10011000010", "10000110100", "10000110010",
+ "11000010010", "11001010000", "11110111010", "11000010100", "10001111010",
+ "10100111100", "10010111100", "10010011110", "10111100100", "10011110100",
+ "10011110010", "11110100100", "11110010100", "11110010010", "11011011110",
+ "11011110110", "11110110110", "10101111000", "10100011110", "10001011110",
+ "10111101000", "10111100010", "11110101000", "11110100010", "10111011110",
+ "10111101110", "11101011110", "11110101110", "11010000100", "11010010000",
+ "11010011100", "1100011101011"
+];
+
+/**
+ * Code 128 字符集B映射表 (ASCII 32-127)
+ */
+const CODE128B_CHARS = {};
+for (let i = 32; i <= 126; i++) {
+ CODE128B_CHARS[String.fromCharCode(i)] = i - 32;
+}
+
+class JsBarcodeUtil {
+ /**
+ * 生成Code 128条形码
+ * @param {Object} canvas - 微信小程序canvas对象
+ * @param {string} text - 要编码的文本
+ * @param {Object} options - 配置选项
+ */
+ static generateCode128(canvas, text, options = {}) {
+ const defaultOptions = {
+ width: 2,
+ height: 100,
+ displayValue: false,
+ fontSize: 12,
+ fontFamily: 'monospace',
+ textAlign: 'center',
+ textPosition: 'bottom',
+ textMargin: 2,
+ background: '#ffffff',
+ lineColor: '#000000',
+ margin: 10,
+ marginTop: 10,
+ marginBottom: 10,
+ marginLeft: 10,
+ marginRight: 10
+ };
+
+ const config = Object.assign({}, defaultOptions, options);
+
+ try {
+ // 验证输入文本
+ if (!text || typeof text !== 'string') {
+ throw new Error('文本不能为空且必须是字符串');
+ }
+
+ // 验证字符是否在Code 128B范围内
+ for (let char of text) {
+ if (!(char in CODE128B_CHARS)) {
+ throw new Error(`字符 "${char}" 不在Code 128B支持范围内`);
+ }
+ }
+
+ console.log('[JsBarcodeUtil] 开始生成条形码:', text);
+
+ // 编码数据
+ const encodedData = this.encodeCode128B(text);
+ console.log('[JsBarcodeUtil] 编码完成,模式数量:', encodedData.length);
+
+ // 绘制条形码
+ this.drawBarcode(canvas, encodedData, config);
+
+ console.log('[JsBarcodeUtil] 条形码绘制完成');
+ return true;
+
+ } catch (error) {
+ console.error('[JsBarcodeUtil] 生成条形码失败:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Code 128B 编码
+ * @param {string} text - 要编码的文本
+ * @returns {Array} 编码后的模式数组
+ */
+ static encodeCode128B(text) {
+ const codes = [];
+
+ // Start Code B
+ codes.push(104);
+
+ // 数据字符
+ let checksum = 104; // 起始码也参与校验和计算
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ const value = CODE128B_CHARS[char];
+ codes.push(value);
+ checksum += value * (i + 1); // 位置权重从1开始
+ }
+
+ // 校验码
+ const checksumCode = checksum % 103;
+ codes.push(checksumCode);
+
+ // Stop Code
+ codes.push(106);
+
+ console.log('[JsBarcodeUtil] 编码序列:', codes);
+ console.log('[JsBarcodeUtil] 校验和:', checksum, '校验码:', checksumCode);
+
+ return codes;
+ }
+
+ /**
+ * 绘制条形码
+ * @param {Object} canvas - canvas对象
+ * @param {Array} codes - 编码数组
+ * @param {Object} config - 配置选项
+ */
+ static drawBarcode(canvas, codes, config) {
+ const ctx = canvas.getContext('2d');
+
+ // 计算总宽度
+ let totalBars = 0;
+ for (let code of codes) {
+ const pattern = CODE128_PATTERNS[code];
+ totalBars += pattern.length;
+ }
+
+ const canvasWidth = canvas.width;
+ const canvasHeight = canvas.height;
+
+ console.log('[JsBarcodeUtil] Canvas尺寸检查:', {
+ canvasWidth,
+ canvasHeight,
+ totalBars
+ });
+
+ // 清除画布并设置背景
+ ctx.fillStyle = config.background;
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // 计算条形码绘制区域
+ const drawWidth = canvasWidth - config.marginLeft - config.marginRight;
+ const drawHeight = canvasHeight - config.marginTop - config.marginBottom;
+ const barWidth = drawWidth / totalBars;
+
+ console.log('[JsBarcodeUtil] 绘制参数:', {
+ canvasWidth,
+ canvasHeight,
+ drawWidth,
+ drawHeight,
+ totalBars,
+ barWidth
+ });
+
+ // 绘制条形码
+ ctx.fillStyle = config.lineColor;
+ let x = config.marginLeft;
+
+ for (let code of codes) {
+ const pattern = CODE128_PATTERNS[code];
+
+ for (let i = 0; i < pattern.length; i++) {
+ if (pattern[i] === '1') {
+ // 绘制黑条
+ ctx.fillRect(x, config.marginTop, barWidth, drawHeight);
+ }
+ x += barWidth;
+ }
+ }
+
+ // 如果需要显示文本
+ if (config.displayValue) {
+ this.drawText(ctx, codes, config, canvasWidth, canvasHeight);
+ }
+
+ // Canvas 2D API需要手动触发绘制
+ // 注意:这里不需要调用draw(),因为Canvas 2D是实时绘制的
+ }
+
+ /**
+ * 绘制文本
+ * @param {Object} ctx - canvas上下文
+ * @param {Array} codes - 编码数组
+ * @param {Object} config - 配置选项
+ * @param {number} canvasWidth - 画布宽度
+ * @param {number} canvasHeight - 画布高度
+ */
+ static drawText(ctx, codes, config, canvasWidth, canvasHeight) {
+ // 从编码中还原文本(去掉起始码、校验码、停止码)
+ let text = '';
+ for (let i = 1; i < codes.length - 2; i++) {
+ const code = codes[i];
+ text += String.fromCharCode(code + 32);
+ }
+
+ ctx.fillStyle = config.lineColor;
+ ctx.font = `${config.fontSize}px ${config.fontFamily}`;
+ ctx.textAlign = config.textAlign;
+
+ let textX = canvasWidth / 2;
+ let textY;
+
+ if (config.textPosition === 'top') {
+ textY = config.marginTop - config.textMargin;
+ } else {
+ textY = canvasHeight - config.marginBottom + config.fontSize + config.textMargin;
+ }
+
+ ctx.fillText(text, textX, textY);
+ }
+}
+
+module.exports = JsBarcodeUtil;
\ No newline at end of file
diff --git a/utils/message-util.js b/utils/message-util.js
new file mode 100644
index 0000000000000000000000000000000000000000..fe1b7660405db222001a5edaba157f9fc1f3db1b
--- /dev/null
+++ b/utils/message-util.js
@@ -0,0 +1,139 @@
+// 消息提示工具类
+class MessageUtil {
+
+ // 成功提示(短提示)
+ static showSuccess(message, duration = 2000) {
+ wx.showToast({
+ title: message,
+ icon: 'success',
+ duration: duration
+ });
+ }
+
+ // 错误提示(根据内容长度选择合适的提示方式)
+ static showError(message, options = {}) {
+ // 如果消息较短,使用toast
+ if (message.length <= 8) {
+ wx.showToast({
+ title: message,
+ icon: 'error',
+ duration: options.duration || 2000
+ });
+ } else {
+ // 消息较长,使用modal
+ wx.showModal({
+ title: options.title || '提示',
+ content: message,
+ showCancel: false,
+ confirmText: '确定',
+ confirmColor: '#007AFF'
+ });
+ }
+ }
+
+ // 警告提示
+ static showWarning(message, options = {}) {
+ if (message.length <= 8) {
+ wx.showToast({
+ title: message,
+ icon: 'none',
+ duration: options.duration || 2000
+ });
+ } else {
+ wx.showModal({
+ title: options.title || '警告',
+ content: message,
+ showCancel: false,
+ confirmText: '确定',
+ confirmColor: '#FF9500'
+ });
+ }
+ }
+
+ // 信息提示
+ static showInfo(message, options = {}) {
+ if (message.length <= 8) {
+ wx.showToast({
+ title: message,
+ icon: 'none',
+ duration: options.duration || 2000
+ });
+ } else {
+ wx.showModal({
+ title: options.title || '信息',
+ content: message,
+ showCancel: false,
+ confirmText: '确定',
+ confirmColor: '#007AFF'
+ });
+ }
+ }
+
+ // 确认对话框
+ static showConfirm(message, options = {}) {
+ return new Promise((resolve) => {
+ wx.showModal({
+ title: options.title || '确认',
+ content: message,
+ showCancel: true,
+ cancelText: options.cancelText || '取消',
+ confirmText: options.confirmText || '确定',
+ cancelColor: '#000000',
+ confirmColor: '#007AFF',
+ success: (res) => {
+ resolve(res.confirm);
+ },
+ fail: () => {
+ resolve(false);
+ }
+ });
+ });
+ }
+
+ // 加载提示
+ static showLoading(message = '加载中...') {
+ wx.showLoading({
+ title: message,
+ mask: true
+ });
+ }
+
+ // 隐藏加载提示
+ static hideLoading() {
+ wx.hideLoading();
+ }
+
+ // 网络错误专用提示
+ static showNetworkError(error) {
+ let message = '网络连接失败';
+ let title = '网络错误';
+
+ if (error && error.message) {
+ if (error.message.includes('HTTP错误')) {
+ message = '服务器响应异常,请稍后重试';
+ title = '服务器错误';
+ } else if (error.message.includes('网络请求失败')) {
+ message = '网络连接失败,请检查网络设置';
+ title = '网络错误';
+ } else if (error.message.includes('服务异常')) {
+ message = '服务暂时不可用,请稍后重试';
+ title = '服务异常';
+ } else {
+ message = error.message;
+ }
+ }
+
+ this.showError(message, { title });
+ }
+
+ // 服务异常专用提示(简短版本)
+ static showServiceError() {
+ wx.showToast({
+ title: '服务异常',
+ icon: 'none',
+ duration: 2000
+ });
+ }
+}
+
+module.exports = MessageUtil;
\ No newline at end of file
diff --git a/utils/qrcode.js b/utils/qrcode.js
new file mode 100644
index 0000000000000000000000000000000000000000..9feff1ebf616604cc2dfc29dca661cccfd148e86
--- /dev/null
+++ b/utils/qrcode.js
@@ -0,0 +1,8 @@
+// 微信小程序二维码生成库包装
+// 兼容 CommonJS 模块系统
+
+// 导入原始库
+const weappQrcode = require('../assets/js/weapp.qrcode.esm.js');
+
+// 导出函数
+module.exports = weappQrcode.default || weappQrcode;
\ No newline at end of file
diff --git a/utils/util.js b/utils/util.js
new file mode 100644
index 0000000000000000000000000000000000000000..764bc2ce26ab9b55a21cbb069dcf084a8418dffd
--- /dev/null
+++ b/utils/util.js
@@ -0,0 +1,19 @@
+const formatTime = date => {
+ const year = date.getFullYear()
+ const month = date.getMonth() + 1
+ const day = date.getDate()
+ const hour = date.getHours()
+ const minute = date.getMinutes()
+ const second = date.getSeconds()
+
+ return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
+}
+
+const formatNumber = n => {
+ n = n.toString()
+ return n[1] ? n : `0${n}`
+}
+
+module.exports = {
+ formatTime
+}